mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-18 05:35:43 +02:00
Compare commits
399
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6853d045b6 | ||
|
|
d8cc9c9680 | ||
|
|
723d6f8a84 | ||
|
|
696a922241 | ||
|
|
c598fee7d6 | ||
|
|
addb491cfd | ||
|
|
c84f35eff5 | ||
|
|
f178e4205f | ||
|
|
3f4d1c66c2 | ||
|
|
2ee5b8e9d1 | ||
|
|
aadbcb443a | ||
|
|
a88794b74e | ||
|
|
556733c77c | ||
|
|
3d16acf3f5 | ||
|
|
87cbc4942d | ||
|
|
6d63300c9a | ||
|
|
a5dd181138 | ||
|
|
528d3946c6 | ||
|
|
037d9d1402 | ||
|
|
6aee213f3c | ||
|
|
f690f4244e | ||
|
|
d6856131b6 | ||
|
|
7d90440035 | ||
|
|
fed785eabd | ||
|
|
765b04adfd | ||
|
|
7085b149e5 | ||
|
|
11c71fef89 | ||
|
|
8be0fb675a | ||
|
|
1b4f335ffe | ||
|
|
9c36a7de85 | ||
|
|
d166dea4f0 | ||
|
|
8e922b859c | ||
|
|
9a4c30135f | ||
|
|
bc6651f34c | ||
|
|
23bb5369b9 | ||
|
|
85b81371f7 | ||
|
|
54ab833b74 | ||
|
|
4ea936eaf4 | ||
|
|
9d81ec9ffd | ||
|
|
46b652a74c | ||
|
|
7013ca9a3f | ||
|
|
0c04aec664 | ||
|
|
1650c8508e | ||
|
|
e176b98fe7 | ||
|
|
eb1e1aa010 | ||
|
|
77c833e1e5 | ||
|
|
0ac29434a7 | ||
|
|
43f5a17416 | ||
|
|
7d0857f263 | ||
|
|
b82d70a66a | ||
|
|
5fb037171d | ||
|
|
d3bb2b9aa0 | ||
|
|
ea765b4134 | ||
|
|
66ff83dca9 | ||
|
|
254e398345 | ||
|
|
c7567ea219 | ||
|
|
8c0306c3f4 | ||
|
|
992b05a196 | ||
|
|
893a9646d3 | ||
|
|
eaa37a2ce9 | ||
|
|
daee8d88bb | ||
|
|
eaa18cc2dd | ||
|
|
b2d9a36308 | ||
|
|
03fc695d60 | ||
|
|
9994b09304 | ||
|
|
53f8558914 | ||
|
|
4bfcd84cee | ||
|
|
cd1d7be05f | ||
|
|
939a426a2e | ||
|
|
94c815f226 | ||
|
|
ef345aac5f | ||
|
|
e306258525 | ||
|
|
05cd317486 | ||
|
|
6dfed31a5e | ||
|
|
ee8374c4c0 | ||
|
|
2066894b5f | ||
|
|
6a2d20fd5b | ||
|
|
5b8b9f1067 | ||
|
|
c9cb8165d4 | ||
|
|
1f0348a5ca | ||
|
|
442ef0788e | ||
|
|
73f9ef0ef8 | ||
|
|
1f7a380548 | ||
|
|
8959f2aec5 | ||
|
|
0e7869eba4 | ||
|
|
d3f8478054 | ||
|
|
0cb1893475 | ||
|
|
e779c8e0b1 | ||
|
|
18b82cb8e2 | ||
|
|
972ab1a935 | ||
|
|
9cc2f37cca | ||
|
|
a2d7631f47 | ||
|
|
1e767c0653 | ||
|
|
d4c569cb7c | ||
|
|
a146df7f6a | ||
|
|
c52cc03e4b | ||
|
|
f206cfad8f | ||
|
|
d4c8b219c4 | ||
|
|
00855999d2 | ||
|
|
24bd0e1c1f | ||
|
|
3b59055192 | ||
|
|
67a16bec53 | ||
|
|
f97802eed5 | ||
|
|
bccb796ccc | ||
|
|
4068e9d135 | ||
|
|
a951334f7f | ||
|
|
75a727877f | ||
|
|
1cece3228c | ||
|
|
7ba48d75c9 | ||
|
|
7fb0628957 | ||
|
|
6edf29f043 | ||
|
|
c8a605cbc8 | ||
|
|
fbec207446 | ||
|
|
2223c82606 | ||
|
|
06f2eef74c | ||
|
|
62aa66cd4b | ||
|
|
8ffe9634b7 | ||
|
|
4b1d6d2aeb | ||
|
|
199ab46429 | ||
|
|
c758954519 | ||
|
|
5bfb3bb882 | ||
|
|
68a5c3f4c7 | ||
|
|
b7fb8e6afb | ||
|
|
c34c798763 | ||
|
|
792cd805a7 | ||
|
|
764929afd9 | ||
|
|
1e751a2256 | ||
|
|
e6726802f7 | ||
|
|
a541376d10 | ||
|
|
f9780330a6 | ||
|
|
347ab0165e | ||
|
|
cb95821eb0 | ||
|
|
4b9cdb4107 | ||
|
|
b440b14fa7 | ||
|
|
24f7d7c439 | ||
|
|
9533d35a84 | ||
|
|
48de4a7234 | ||
|
|
67ac35c5bc | ||
|
|
12c7ddf3a3 | ||
|
|
d6caa3b00a | ||
|
|
3d48526c16 | ||
|
|
2f3bd69bf5 | ||
|
|
f10a0c6f32 | ||
|
|
2eba27b01f | ||
|
|
c0245a6ee9 | ||
|
|
9e82d23252 | ||
|
|
460c522902 | ||
|
|
298a19b573 | ||
|
|
d2ec46b927 | ||
|
|
22e4bf74fc | ||
|
|
96a0536ec4 | ||
|
|
8d33938173 | ||
|
|
1c73b1e45a | ||
|
|
465d5d648b | ||
|
|
b751e8bcee | ||
|
|
8ec3982056 | ||
|
|
f17f264a7a | ||
|
|
086443472f | ||
|
|
936e69404e | ||
|
|
b1a25abc73 | ||
|
|
0e70b8d94f | ||
|
|
e1aa1a4510 | ||
|
|
ae7dbd1fa5 | ||
|
|
3ec95153ce | ||
|
|
4836f8b18b | ||
|
|
e7fbdeeb13 | ||
|
|
ee650ab85f | ||
|
|
7a959f62cc | ||
|
|
82905297fd | ||
|
|
9b5549f759 | ||
|
|
fa96c0ac76 | ||
|
|
98b8ff904c | ||
|
|
951131c8ec | ||
|
|
8bcdba822e | ||
|
|
60fc49b448 | ||
|
|
1d21b4ba08 | ||
|
|
55ec0d3d2a | ||
|
|
c7dd7be030 | ||
|
|
47d38a3022 | ||
|
|
8e829f38af | ||
|
|
3f241d00a3 | ||
|
|
f0abf582dd | ||
|
|
2458f2d2e0 | ||
|
|
477a43dae0 | ||
|
|
fc8e6ec64f | ||
|
|
d6a457ef1d | ||
|
|
ce1077da40 | ||
|
|
969958695a | ||
|
|
dd16ae4ba5 | ||
|
|
e24e141253 | ||
|
|
eae1faa656 | ||
|
|
1976d6584c | ||
|
|
54e18445fc | ||
|
|
69dc29aaf9 | ||
|
|
aa5ff74845 | ||
|
|
6049aaa842 | ||
|
|
576aa1ca02 | ||
|
|
e28e97d5e0 | ||
|
|
59e7c63c93 | ||
|
|
be7dee1c3b | ||
|
|
0aafa04bac | ||
|
|
2e1adaa867 | ||
|
|
3f8b165592 | ||
|
|
9ed0fa196c | ||
|
|
0b9adc28c3 | ||
|
|
3b0255d1ef | ||
|
|
80c3ccba7b | ||
|
|
d4255a0645 | ||
|
|
80d61a2600 | ||
|
|
424f24720a | ||
|
|
2a71180c1d | ||
|
|
697f878e36 | ||
|
|
987b9da4ab | ||
|
|
4bff1df4b0 | ||
|
|
5104e31e35 | ||
|
|
7ae4739630 | ||
|
|
5db1949ae3 | ||
|
|
ddb29df667 | ||
|
|
fa467573d7 | ||
|
|
1a728a93c6 | ||
|
|
def69c59d2 | ||
|
|
aaa0cd6b51 | ||
|
|
a9c831c11b | ||
|
|
bad4d17c34 | ||
|
|
55219b23d8 | ||
|
|
8edbd39ad3 | ||
|
|
4b0fd834d8 | ||
|
|
0fd2748530 | ||
|
|
bc0a3419ed | ||
|
|
5cd47bac49 | ||
|
|
4c6d80a67f | ||
|
|
18ed044c27 | ||
|
|
394a9fa85f | ||
|
|
9741d9bdf0 | ||
|
|
06ca07432d | ||
|
|
e757a80001 | ||
|
|
a204444905 | ||
|
|
4cfdf8774a | ||
|
|
beb62fc053 | ||
|
|
857f3e4a38 | ||
|
|
6342cd1665 | ||
|
|
baedf91836 | ||
|
|
190b42850f | ||
|
|
678b512aed | ||
|
|
c85e246c32 | ||
|
|
98ebc45f31 | ||
|
|
5ca2f358f9 | ||
|
|
86169c1439 | ||
|
|
36d6eed468 | ||
|
|
ef50fed6fe | ||
|
|
c0abfc7df6 | ||
|
|
318889bc6c | ||
|
|
b9e3fd5f3e | ||
|
|
8729ebc40c | ||
|
|
919282fead | ||
|
|
cff4784ff8 | ||
|
|
9921e5210a | ||
|
|
048eff9f11 | ||
|
|
8a2765c8f2 | ||
|
|
ffbcdd1ecc | ||
|
|
108a041fa7 | ||
|
|
52e3c59f07 | ||
|
|
0e17988332 | ||
|
|
5f0d05099d | ||
|
|
a1739d3184 | ||
|
|
ab38cc2cc0 | ||
|
|
647833dcaa | ||
|
|
1f03735b7d | ||
|
|
d980cca59b | ||
|
|
944b93bf61 | ||
|
|
8aa59d002a | ||
|
|
ed533b32a8 | ||
|
|
d88f59eea4 | ||
|
|
fc5dde6c55 | ||
|
|
4c902d21a3 | ||
|
|
54804af06a | ||
|
|
312f026e9c | ||
|
|
6973b19cc7 | ||
|
|
45ed67856f | ||
|
|
74b2dbe1ea | ||
|
|
7f803df586 | ||
|
|
a5b43c933a | ||
|
|
aae2fb4b85 | ||
|
|
c20a50875d | ||
|
|
779553f4aa | ||
|
|
537e69608e | ||
|
|
208cd4d70e | ||
|
|
1352e58133 | ||
|
|
f1162ac898 | ||
|
|
4de8443c5c | ||
|
|
96dc39aeab | ||
|
|
316f8410fa | ||
|
|
1d3926af27 | ||
|
|
e566ed4b3f | ||
|
|
14c2241853 | ||
|
|
2c908f1557 | ||
|
|
5005d1c004 | ||
|
|
02a46c45c8 | ||
|
|
852a129881 | ||
|
|
78348d2d9f | ||
|
|
44af8d5257 | ||
|
|
84c956bc8c | ||
|
|
b86e6b82f2 | ||
|
|
b1de5be334 | ||
|
|
0751428422 | ||
|
|
b16f05405b | ||
|
|
ca8d92421a | ||
|
|
7aa9d3fd00 | ||
|
|
ed69f60f24 | ||
|
|
c55f1f12bf | ||
|
|
5d76b1d624 | ||
|
|
857fd3578f | ||
|
|
3a4af1e573 | ||
|
|
37344124e1 | ||
|
|
d0f4db6ddd | ||
|
|
a9800aab87 | ||
|
|
9bf3fc2d0f | ||
|
|
a6e4bd93ff | ||
|
|
0e9c41f480 | ||
|
|
d4368cfa97 | ||
|
|
3808302309 | ||
|
|
25019450e2 | ||
|
|
e4c7db180e | ||
|
|
3183146141 | ||
|
|
f070b1c805 | ||
|
|
141589a7e6 | ||
|
|
be37631181 | ||
|
|
51ddc792d6 | ||
|
|
52d4f73e39 | ||
|
|
4b25e28e3e | ||
|
|
263eab9f76 | ||
|
|
c3df8bd500 | ||
|
|
d86502421d | ||
|
|
cbf26a5d98 | ||
|
|
09bd5990d4 | ||
|
|
79595d43a5 | ||
|
|
49a6704bdc | ||
|
|
a430b7fcfb | ||
|
|
48c287d107 | ||
|
|
0fdc787597 | ||
|
|
3bd86fc6f5 | ||
|
|
f1024f7341 | ||
|
|
f9ac88012f | ||
|
|
9d0186b5bf | ||
|
|
40062c40df | ||
|
|
36bbe059df | ||
|
|
52627010cb | ||
|
|
f6781d19ab | ||
|
|
fcf134a452 | ||
|
|
80331b88e0 | ||
|
|
b37f894f38 | ||
|
|
79de3dbad3 | ||
|
|
677fd3ce28 | ||
|
|
4ee863f30d | ||
|
|
7e060f88a2 | ||
|
|
f49856af0b | ||
|
|
5a7d384e2f | ||
|
|
b73b34ddd5 | ||
|
|
c9ffd753f5 | ||
|
|
e85e157e8f | ||
|
|
88e7868885 | ||
|
|
e8dd682320 | ||
|
|
75143b966c | ||
|
|
490e1aab3b | ||
|
|
003226cef4 | ||
|
|
098a199cb9 | ||
|
|
97f6f45993 | ||
|
|
e8631c052a | ||
|
|
16a86c8b8e | ||
|
|
5fa6bb5f55 | ||
|
|
d29e9e22c7 | ||
|
|
6990e1fcf5 | ||
|
|
22e60c47cc | ||
|
|
d9396c38ea | ||
|
|
ff60ee8c9a | ||
|
|
8761721fb9 | ||
|
|
de85e7c246 | ||
|
|
d333f4438f | ||
|
|
af14a2abbc | ||
|
|
e466c2c90c | ||
|
|
815a67ef55 | ||
|
|
38f1b415a0 | ||
|
|
ed78174adf | ||
|
|
5da6971a95 | ||
|
|
256e92bfb3 | ||
|
|
3d4e5c0471 | ||
|
|
013a12334e | ||
|
|
c7211e03e9 | ||
|
|
ffc916e38c | ||
|
|
03bf149ebd | ||
|
|
137dcce5b5 | ||
|
|
48164a95da | ||
|
|
3926e83884 | ||
|
|
43709a16bf | ||
|
|
d98c7248dc | ||
|
|
ac2736f18e | ||
|
|
fc130a52ef | ||
|
|
b5a981d82d | ||
|
|
f679348327 |
@@ -54,7 +54,7 @@ jobs:
|
||||
if: steps.changed-files.outputs.all
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: poetry lock --check
|
||||
run: poetry check --lock
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.all
|
||||
|
||||
@@ -39,6 +39,12 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_RO_TOKEN }}
|
||||
|
||||
- name: Check Lock
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
poetry check --lock
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
@@ -114,6 +114,42 @@ jobs:
|
||||
- name: Run check_sdk_methods script
|
||||
run: python .github/scripts/check_sdk_methods.py
|
||||
|
||||
check-schema:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
name: "Check CLI schema hasn't changed #${{ matrix.python-version }}"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.11"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
|
||||
uses: "./.github/actions/poetry_setup"
|
||||
with:
|
||||
python-version: "3.11"
|
||||
poetry-version: ${{ env.POETRY_VERSION }}
|
||||
cache-key: schema-check-cli
|
||||
- name: Install CLI dependencies
|
||||
run: |
|
||||
cd libs/cli
|
||||
poetry install
|
||||
- name: Generate schema and check for changes
|
||||
run: |
|
||||
cd libs/cli
|
||||
# Create a temporary copy of the current schema
|
||||
cp schemas/schema.json schemas/schema.current.json
|
||||
# Generate new schema
|
||||
poetry run python generate_schema.py
|
||||
# Compare the new schema with the original
|
||||
if ! diff -q schemas/schema.json schemas/schema.current.json > /dev/null; then
|
||||
echo "Error: Langgraph.json configuration schema has changed. Please run 'poetry run python generate_schema.py' in the libs/cli directory and commit the changes."
|
||||
diff schemas/schema.json schemas/schema.current.json
|
||||
exit 1
|
||||
fi
|
||||
echo "Schema check passed - no changes detected"
|
||||
|
||||
integration-test:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
@@ -180,6 +216,8 @@ jobs:
|
||||
test,
|
||||
test-langgraph,
|
||||
test-scheduler-kafka,
|
||||
check-sdk-methods,
|
||||
check-schema,
|
||||
integration-test,
|
||||
test-js,
|
||||
]
|
||||
|
||||
@@ -102,7 +102,14 @@ jobs:
|
||||
- name: Build llms-text
|
||||
run: make llms-text
|
||||
- name: Build site
|
||||
run: make build-docs
|
||||
run: |
|
||||
# If this is main branch, then we want to download stats. we do this
|
||||
# with the env variable DOWNLOAD_STATS=true
|
||||
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
|
||||
DOWNLOAD_STATS=true make build-docs
|
||||
else
|
||||
make build-docs
|
||||
fi
|
||||
env:
|
||||
MKDOCS_GIT_COMMITTERS_APIKEY: ${{ secrets.MKDOCS_GIT_COMMITTERS_APIKEY }}
|
||||
OPENAI_API_KEY: sf-proj-1234567890 # fake placeholder, shouldn't actually be used
|
||||
@@ -127,6 +134,7 @@ jobs:
|
||||
--check-links-ignore "https://openai\.com/.*" \
|
||||
--check-links-ignore "https://www\.uber\.com/.*" \
|
||||
--check-links-ignore "https://pepy\.tech/.*" \
|
||||
--check-links-ignore "docs/docs/static/wordmark_*" \
|
||||
--check-links $(find site -name "index.html" | grep -v 'storm/index.html')
|
||||
|
||||
else
|
||||
@@ -147,6 +155,7 @@ jobs:
|
||||
--check-links-ignore "https://twitter.com/.*" \
|
||||
--check-links-ignore "https://github\.com/.*" \
|
||||
--check-links-ignore "/.*\.(ipynb|html)$" \
|
||||
--check-links-ignore "docs/docs/static/wordmark_*" \
|
||||
--check-links ${CHANGED_FILES} \
|
||||
|| ([ $? = 5 ] && exit 0 || exit $?)
|
||||
else
|
||||
|
||||
@@ -57,13 +57,13 @@ jobs:
|
||||
env:
|
||||
# these won't actually be used because of the VCR cassettes
|
||||
# but need to set them to avoid triggering getpass()
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }}
|
||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||
NOMIC_API_KEY: ${{ secrets.NOMIC_API_KEY }}
|
||||
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
|
||||
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
||||
OPENAI_API_KEY: "very-secret-key"
|
||||
ANTHROPIC_API_KEY: "very-secret-key"
|
||||
TAVILY_API_KEY: "very-secret-key"
|
||||
LANGSMITH_API_KEY: "very-secret-key"
|
||||
NOMIC_API_KEY: "very-secret-key"
|
||||
COHERE_API_KEY: "very-secret-key"
|
||||
FIREWORKS_API_KEY: "very-secret-key"
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "schedule" ]; then
|
||||
echo "Running all notebooks"
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
name: Check File Size
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
file-size-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
- name: Filter by size
|
||||
# TODO: roll back the web voyager hack
|
||||
run: |
|
||||
large_added_files=$(find ${{ steps.changed-files.outputs.added_files }} -maxdepth 0 -size +1M | grep -v "web_voyager" || true)
|
||||
if [ -n "$large_added_files" ]; then
|
||||
echo "Large files added: $large_added_files"
|
||||
echo "# Large files added:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "$large_added_files" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,339 +1,87 @@
|
||||
# 🦜🕸️LangGraph
|
||||
<picture class="github-only">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://langchain-ai.github.io/langgraph/static/wordmark_dark.svg">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://langchain-ai.github.io/langgraph/static/wordmark_light.svg">
|
||||
<img alt="LangGraph Logo" src="https://langchain-ai.github.io/langgraph/static/wordmark_dark.svg" width="80%">
|
||||
</picture>
|
||||
|
||||

|
||||
<div>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
[](https://pypi.org/project/langgraph/)
|
||||
[](https://pepy.tech/project/langgraph)
|
||||
[](https://github.com/langchain-ai/langgraph/issues)
|
||||
[](https://langchain-ai.github.io/langgraph/)
|
||||
|
||||
⚡ Building language agents as graphs ⚡
|
||||
|
||||
> [!NOTE]
|
||||
> Looking for the JS version? See the [JS repo](https://github.com/langchain-ai/langgraphjs) and the [JS docs](https://langchain-ai.github.io/langgraphjs/).
|
||||
|
||||
## Overview
|
||||
LangGraph — used by Replit, Uber, LinkedIn, GitLab and more — is a low-level orchestration framework for building controllable agents. While langchain provides integrations and composable components to streamline LLM application development, the LangGraph library enables agent orchestration — offering customizable architectures, long-term memory, and human-in-the-loop to reliably handle complex tasks.
|
||||
|
||||
[LangGraph](https://langchain-ai.github.io/langgraph/) is a library for building
|
||||
stateful, multi-actor applications with LLMs, used to create agent and multi-agent
|
||||
workflows. Check out an introductory tutorial [here](https://langchain-ai.github.io/langgraph/tutorials/introduction/).
|
||||
|
||||
|
||||
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
|
||||
|
||||
### Why use LangGraph?
|
||||
|
||||
LangGraph powers [production-grade agents](https://www.langchain.com/built-with-langgraph), trusted by Linkedin, Uber, Klarna, GitLab, and many more. LangGraph provides fine-grained control over both the flow and state of your agent applications. It implements a central [persistence layer](https://langchain-ai.github.io/langgraph/concepts/persistence/), enabling features that are common to most agent architectures:
|
||||
|
||||
- **Memory**: LangGraph persists arbitrary aspects of your application's state,
|
||||
supporting memory of conversations and other updates within and across user
|
||||
interactions;
|
||||
- **Human-in-the-loop**: Because state is checkpointed, execution can be interrupted
|
||||
and resumed, allowing for decisions, validation, and corrections at key stages via
|
||||
human input.
|
||||
|
||||
Standardizing these components allows individuals and teams to focus on the behavior
|
||||
of their agent, instead of its supporting infrastructure.
|
||||
|
||||
Through [LangGraph Platform](#langgraph-platform), LangGraph also provides tooling for
|
||||
the development, deployment, debugging, and monitoring of your applications.
|
||||
|
||||
LangGraph integrates seamlessly with
|
||||
[LangChain](https://python.langchain.com/docs/introduction/) and
|
||||
[LangSmith](https://docs.smith.langchain.com/) (but does not require them).
|
||||
|
||||
To learn more about LangGraph, check out our first LangChain Academy
|
||||
course, *Introduction to LangGraph*, available for free
|
||||
[here](https://academy.langchain.com/courses/intro-to-langgraph).
|
||||
|
||||
### LangGraph Platform
|
||||
|
||||
[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform) is infrastructure for deploying LangGraph agents. It is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework. The LangGraph Platform consists of several components that work together to support the development, deployment, debugging, and monitoring of LangGraph applications: [LangGraph Server](https://langchain-ai.github.io/langgraph/concepts/langgraph_server) (APIs), [LangGraph SDKs](https://langchain-ai.github.io/langgraph/concepts/sdk) (clients for the APIs), [LangGraph CLI](https://langchain-ai.github.io/langgraph/concepts/langgraph_cli) (command line tool for building the server), and [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio) (UI/debugger).
|
||||
|
||||
See deployment options [here](https://langchain-ai.github.io/langgraph/concepts/deployment_options/)
|
||||
(includes a free tier).
|
||||
|
||||
Here are some common issues that arise in complex deployments, which LangGraph Platform addresses:
|
||||
|
||||
- **Streaming support**: LangGraph Server provides [multiple streaming modes](https://langchain-ai.github.io/langgraph/concepts/streaming) optimized for various application needs
|
||||
- **Background runs**: Runs agents asynchronously in the background
|
||||
- **Support for long running agents**: Infrastructure that can handle long running processes
|
||||
- **[Double texting](https://langchain-ai.github.io/langgraph/concepts/double_texting)**: Handle the case where you get two messages from the user before the agent can respond
|
||||
- **Handle burstiness**: Task queue for ensuring requests are handled consistently without loss, even under heavy loads
|
||||
|
||||
## Installation
|
||||
|
||||
```shell
|
||||
```bash
|
||||
pip install -U langgraph
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
Let's build a tool-calling [ReAct-style](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#react-implementation) agent that uses a search tool!
|
||||
|
||||
```shell
|
||||
pip install langchain-anthropic
|
||||
```
|
||||
|
||||
```shell
|
||||
export ANTHROPIC_API_KEY=sk-...
|
||||
```
|
||||
|
||||
Optionally, we can set up [LangSmith](https://docs.smith.langchain.com/) for best-in-class observability.
|
||||
|
||||
```shell
|
||||
export LANGSMITH_TRACING=true
|
||||
export LANGSMITH_API_KEY=lsv2_sk_...
|
||||
```
|
||||
|
||||
The simplest way to create a tool-calling agent in LangGraph is to use `create_react_agent`:
|
||||
|
||||
<details open>
|
||||
<summary>High-level implementation</summary>
|
||||
To learn more about how to use LangGraph, check out [the docs](https://langchain-ai.github.io/langgraph/). We show a simple example below of how to create a ReAct agent.
|
||||
|
||||
```python
|
||||
# This code depends on pip install langchain[anthropic]
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.tools import tool
|
||||
|
||||
# Define the tools for the agent to use
|
||||
@tool
|
||||
def search(query: str):
|
||||
"""Call to surf the web."""
|
||||
# This is a placeholder, but don't tell the LLM that...
|
||||
if "sf" in query.lower() or "san francisco" in query.lower():
|
||||
return "It's 60 degrees and foggy."
|
||||
return "It's 90 degrees and sunny."
|
||||
|
||||
|
||||
tools = [search]
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0)
|
||||
|
||||
# Initialize memory to persist state between graph runs
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
app = create_react_agent(model, tools, checkpointer=checkpointer)
|
||||
|
||||
# Use the agent
|
||||
final_state = app.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
config={"configurable": {"thread_id": 42}}
|
||||
agent = create_react_agent("anthropic:claude-3-7-sonnet-latest", tools=[search])
|
||||
agent.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]}
|
||||
)
|
||||
final_state["messages"][-1].content
|
||||
```
|
||||
```
|
||||
"Based on the search results, I can tell you that the current weather in San Francisco is:\n\nTemperature: 60 degrees Fahrenheit\nConditions: Foggy\n\nSan Francisco is known for its microclimates and frequent fog, especially during the summer months. The temperature of 60°F (about 15.5°C) is quite typical for the city, which tends to have mild temperatures year-round. The fog, often referred to as "Karl the Fog" by locals, is a characteristic feature of San Francisco\'s weather, particularly in the mornings and evenings.\n\nIs there anything else you\'d like to know about the weather in San Francisco or any other location?"
|
||||
```
|
||||
|
||||
Now when we pass the same <code>"thread_id"</code>, the conversation context is retained via the saved state (i.e. stored list of messages)
|
||||
## Why use LangGraph?
|
||||
|
||||
```python
|
||||
final_state = app.invoke(
|
||||
{"messages": [{"role": "user", "content": "what about ny"}]},
|
||||
config={"configurable": {"thread_id": 42}}
|
||||
)
|
||||
final_state["messages"][-1].content
|
||||
```
|
||||
LangGraph is built for developers who want to build powerful, adaptable AI agents. Developers choose LangGraph for:
|
||||
|
||||
```
|
||||
"Based on the search results, I can tell you that the current weather in New York City is:\n\nTemperature: 90 degrees Fahrenheit (approximately 32.2 degrees Celsius)\nConditions: Sunny\n\nThis weather is quite different from what we just saw in San Francisco. New York is experiencing much warmer temperatures right now. Here are a few points to note:\n\n1. The temperature of 90°F is quite hot, typical of summer weather in New York City.\n2. The sunny conditions suggest clear skies, which is great for outdoor activities but also means it might feel even hotter due to direct sunlight.\n3. This kind of weather in New York often comes with high humidity, which can make it feel even warmer than the actual temperature suggests.\n\nIt's interesting to see the stark contrast between San Francisco's mild, foggy weather and New York's hot, sunny conditions. This difference illustrates how varied weather can be across different parts of the United States, even on the same day.\n\nIs there anything else you'd like to know about the weather in New York or any other location?"
|
||||
```
|
||||
</details>
|
||||
- **Reliability and controllability.** Steer agent actions with moderation checks and human-in-the-loop approvals. LangGraph persists context for long-running workflows, keeping your agents on course.
|
||||
- **Low-level and extensible.** Build custom agents with fully descriptive, low-level primitives – free from rigid abstractions that limit customization. Design scalable multi-agent systems, with each agent serving a specific role tailored to your use case.
|
||||
- **First-class streaming support.** With token-by-token streaming and streaming of intermediate steps, LangGraph gives users clear visibility into agent reasoning and actions as they unfold in real time.
|
||||
|
||||
> [!TIP]
|
||||
> LangGraph is a **low-level** framework that allows you to implement any custom agent
|
||||
architectures. Click on the low-level implementation below to see how to implement a
|
||||
tool-calling agent from scratch.
|
||||
LangGraph is trusted in production and powering agents for companies like:
|
||||
|
||||
<details>
|
||||
<summary>Low-level implementation</summary>
|
||||
- [Klarna](https://blog.langchain.dev/customers-klarna/): Customer support bot for 85 million active users
|
||||
- [Elastic](https://www.elastic.co/blog/elastic-security-generative-ai-features): Security AI assistant for threat detection
|
||||
- [Uber](https://dpe.org/sessions/ty-smith-adam-huda/this-year-in-ubers-ai-driven-developer-productivity-revolution/): Automated unit test generation
|
||||
- [Replit](https://www.langchain.com/breakoutagents/replit): Code generation
|
||||
- And many more ([see list here](https://www.langchain.com/built-with-langgraph))
|
||||
|
||||
```python
|
||||
from typing import Literal
|
||||
## LangGraph’s ecosystem
|
||||
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.tools import tool
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.graph import END, START, StateGraph, MessagesState
|
||||
from langgraph.prebuilt import ToolNode
|
||||
While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:
|
||||
|
||||
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
|
||||
- [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
|
||||
|
||||
# Define the tools for the agent to use
|
||||
@tool
|
||||
def search(query: str):
|
||||
"""Call to surf the web."""
|
||||
# This is a placeholder, but don't tell the LLM that...
|
||||
if "sf" in query.lower() or "san francisco" in query.lower():
|
||||
return "It's 60 degrees and foggy."
|
||||
return "It's 90 degrees and sunny."
|
||||
## Pairing with LangGraph Platform
|
||||
|
||||
While LangGraph is our open-source agent orchestration framework, enterprises that need scalable agent deployment can benefit from [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/).
|
||||
|
||||
tools = [search]
|
||||
LangGraph Platform can help engineering teams:
|
||||
|
||||
tool_node = ToolNode(tools)
|
||||
- **Accelerate agent development**: Quickly create agent UXs with configurable templates and [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/) for visualizing and debugging agent interactions.
|
||||
- **Deploy seamlessly**: We handle the complexity of deploying your agent. LangGraph Platform includes robust APIs for memory, threads, and cron jobs plus auto-scaling task queues & servers.
|
||||
- **Centralize agent management & reusability**: Discover, reuse, and manage agents across the organization. Business users can also modify agents without coding.
|
||||
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0).bind_tools(tools)
|
||||
## Additional resources
|
||||
|
||||
# Define the function that determines whether to continue or not
|
||||
def should_continue(state: MessagesState) -> Literal["tools", END]:
|
||||
messages = state['messages']
|
||||
last_message = messages[-1]
|
||||
# If the LLM makes a tool call, then we route to the "tools" node
|
||||
if last_message.tool_calls:
|
||||
return "tools"
|
||||
# Otherwise, we stop (reply to the user)
|
||||
return END
|
||||
- [LangChain Academy](https://academy.langchain.com/courses/intro-to-langgraph): Learn the basics of LangGraph in our free, structured course.
|
||||
- [Tutorials](https://langchain-ai.github.io/langgraph/tutorials/): Simple walkthroughs with guided examples on getting started with LangGraph.
|
||||
- [Templates](https://langchain-ai.github.io/langgraph/concepts/template_applications/): Pre-built reference apps for common agentic workflows (e.g. ReAct agent, memory, retrieval etc.) that can be cloned and adapted.
|
||||
- [How-to Guides](https://langchain-ai.github.io/langgraph/how-tos/): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
|
||||
- [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components.
|
||||
- [Built with LangGraph](https://www.langchain.com/built-with-langgraph): Hear how industry leaders use LangGraph to ship powerful, production-ready AI applications.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
# Define the function that calls the model
|
||||
def call_model(state: MessagesState):
|
||||
messages = state['messages']
|
||||
response = model.invoke(messages)
|
||||
# We return a list, because this will get added to the existing list
|
||||
return {"messages": [response]}
|
||||
|
||||
|
||||
# Define a new graph
|
||||
workflow = StateGraph(MessagesState)
|
||||
|
||||
# Define the two nodes we will cycle between
|
||||
workflow.add_node("agent", call_model)
|
||||
workflow.add_node("tools", tool_node)
|
||||
|
||||
# Set the entrypoint as `agent`
|
||||
# This means that this node is the first one called
|
||||
workflow.add_edge(START, "agent")
|
||||
|
||||
# We now add a conditional edge
|
||||
workflow.add_conditional_edges(
|
||||
# First, we define the start node. We use `agent`.
|
||||
# This means these are the edges taken after the `agent` node is called.
|
||||
"agent",
|
||||
# Next, we pass in the function that will determine which node is called next.
|
||||
should_continue,
|
||||
)
|
||||
|
||||
# We now add a normal edge from `tools` to `agent`.
|
||||
# This means that after `tools` is called, `agent` node is called next.
|
||||
workflow.add_edge("tools", 'agent')
|
||||
|
||||
# Initialize memory to persist state between graph runs
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
# Finally, we compile it!
|
||||
# This compiles it into a LangChain Runnable,
|
||||
# meaning you can use it as you would any other runnable.
|
||||
# Note that we're (optionally) passing the memory when compiling the graph
|
||||
app = workflow.compile(checkpointer=checkpointer)
|
||||
|
||||
# Use the agent
|
||||
final_state = app.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
config={"configurable": {"thread_id": 42}}
|
||||
)
|
||||
final_state["messages"][-1].content
|
||||
```
|
||||
|
||||
<b>Step-by-step Breakdown</b>:
|
||||
|
||||
<details>
|
||||
<summary>Initialize the model and tools.</summary>
|
||||
<ul>
|
||||
<li>
|
||||
We use <code>ChatAnthropic</code> as our LLM. <strong>NOTE:</strong> we need to make sure the model knows that it has these tools available to call. We can do this by converting the LangChain tools into the format for OpenAI tool calling using the <code>.bind_tools()</code> method.
|
||||
</li>
|
||||
<li>
|
||||
We define the tools we want to use - a search tool in our case. It is really easy to create your own tools - see documentation here on how to do that <a href="https://python.langchain.com/docs/how_to/custom_tools/">here</a>.
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Initialize graph with state.</summary>
|
||||
|
||||
<ul>
|
||||
<li>We initialize graph (<code>StateGraph</code>) by passing state schema (in our case <code>MessagesState</code>)</li>
|
||||
<li><code>MessagesState</code> is a prebuilt state schema that has one attribute -- a list of LangChain <code>Message</code> objects, as well as logic for merging the updates from each node into the state.</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Define graph nodes.</summary>
|
||||
|
||||
There are two main nodes we need:
|
||||
|
||||
<ul>
|
||||
<li>The <code>agent</code> node: responsible for deciding what (if any) actions to take.</li>
|
||||
<li>The <code>tools</code> node that invokes tools: if the agent decides to take an action, this node will then execute that action.</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Define entry point and graph edges.</summary>
|
||||
|
||||
First, we need to set the entry point for graph execution - <code>agent</code> node.
|
||||
|
||||
Then we define one normal and one conditional edge. Conditional edge means that the destination depends on the contents of the graph's state (<code>MessagesState</code>). In our case, the destination is not known until the agent (LLM) decides.
|
||||
|
||||
<ul>
|
||||
<li>Conditional edge: after the agent is called, we should either:
|
||||
<ul>
|
||||
<li>a. Run tools if the agent said to take an action, OR</li>
|
||||
<li>b. Finish (respond to the user) if the agent did not ask to run tools</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Normal edge: after the tools are invoked, the graph should always return to the agent to decide what to do next</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Compile the graph.</summary>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
When we compile the graph, we turn it into a LangChain
|
||||
<a href="https://python.langchain.com/docs/concepts/runnables/">Runnable</a>,
|
||||
which automatically enables calling <code>.invoke()</code>, <code>.stream()</code> and <code>.batch()</code>
|
||||
with your inputs
|
||||
</li>
|
||||
<li>
|
||||
We can also optionally pass checkpointer object for persisting state between graph runs, and enabling memory,
|
||||
human-in-the-loop workflows, time travel and more. In our case we use <code>MemorySaver</code> -
|
||||
a simple in-memory checkpointer
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Execute the graph.</summary>
|
||||
|
||||
<ol>
|
||||
<li>LangGraph adds the input message to the internal state, then passes the state to the entrypoint node, <code>"agent"</code>.</li>
|
||||
<li>The <code>"agent"</code> node executes, invoking the chat model.</li>
|
||||
<li>The chat model returns an <code>AIMessage</code>. LangGraph adds this to the state.</li>
|
||||
<li>Graph cycles the following steps until there are no more <code>tool_calls</code> on <code>AIMessage</code>:
|
||||
<ul>
|
||||
<li>If <code>AIMessage</code> has <code>tool_calls</code>, <code>"tools"</code> node executes</li>
|
||||
<li>The <code>"agent"</code> node executes again and returns <code>AIMessage</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Execution progresses to the special <code>END</code> value and outputs the final state. And as a result, we get a list of all our chat messages as output.</li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
</details>
|
||||
|
||||
## Documentation
|
||||
|
||||
* [Tutorials](https://langchain-ai.github.io/langgraph/tutorials/): Learn to build with LangGraph through guided examples.
|
||||
* [How-to Guides](https://langchain-ai.github.io/langgraph/how-tos/): Accomplish specific things within LangGraph, from streaming, to adding memory & persistence, to common design patterns (branching, subgraphs, etc.), these are the place to go if you want to copy and run a specific code snippet.
|
||||
* [Conceptual Guides](https://langchain-ai.github.io/langgraph/concepts/high_level/): In-depth explanations of the key concepts and principles behind LangGraph, such as nodes, edges, state and more.
|
||||
* [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Review important classes and methods, simple examples of how to use the graph and checkpointing APIs, higher-level prebuilt components and more.
|
||||
* [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform): LangGraph Platform is a commercial solution for deploying agentic applications in production, built on the open-source LangGraph framework.
|
||||
|
||||
## Resources
|
||||
|
||||
* [Built with LangGraph](https://www.langchain.com/built-with-langgraph): Hear how industry leaders use LangGraph to ship powerful, production-ready AI applications.
|
||||
|
||||
## Contributing
|
||||
|
||||
For more information on how to contribute, see [here](https://github.com/langchain-ai/langgraph/blob/main/CONTRIBUTING.md).
|
||||
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
|
||||
+9
-1
@@ -10,7 +10,15 @@ 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
|
||||
@if [ "$(DOWNLOAD_STATS)" = "true" ]; then \
|
||||
set -x; \
|
||||
poetry run python -m _scripts.third_party_page.get_download_stats stats.yml; \
|
||||
set +x; \
|
||||
else \
|
||||
set -x; \
|
||||
poetry run python -m _scripts.third_party_page.get_download_stats --fake stats.yml; \
|
||||
set +x; \
|
||||
fi
|
||||
poetry run python -m _scripts.third_party_page.create_third_party_page stats.yml docs/prebuilt.md --language python
|
||||
|
||||
build-docs: build-typedoc build-prebuilt
|
||||
|
||||
@@ -14,6 +14,8 @@ To run the documentation server locally you can run:
|
||||
make serve-docs
|
||||
```
|
||||
|
||||
This will start the documentation server on [http://127.0.0.1:8000/langgraph/](http://127.0.0.1:8000/langgraph/).
|
||||
|
||||
## Execute notebooks
|
||||
|
||||
If you would like to automatically execute all of the notebooks, to mimic the "Run notebooks" GHA, you can run:
|
||||
|
||||
@@ -186,7 +186,7 @@ def _on_page_markdown_with_config(
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -30,10 +30,23 @@ PACKAGES_FILE = HERE / "packages.yml"
|
||||
PACKAGES = yaml.safe_load(PACKAGES_FILE.read_text())['packages']
|
||||
|
||||
|
||||
def _get_weekly_downloads(packages: list[Package]) -> list[ResolvedPackage]:
|
||||
def _get_weekly_downloads(packages: list[Package], fake: bool) -> list[ResolvedPackage]:
|
||||
"""Retrieve the monthly download count for a list of packages from PyPIStats."""
|
||||
resolved_packages: list[ResolvedPackage] = []
|
||||
|
||||
if fake:
|
||||
# To avoid making network requests during testing, return fake download counts
|
||||
for package in packages:
|
||||
resolved_packages.append(
|
||||
{
|
||||
"name": package["name"],
|
||||
"repo": package["repo"],
|
||||
"weekly_downloads": -12345,
|
||||
"description": package["description"],
|
||||
}
|
||||
)
|
||||
return resolved_packages
|
||||
|
||||
for package in packages:
|
||||
# First check if package exists on PyPI
|
||||
pypi_url = f"https://pypi.org/pypi/{package['name']}/json"
|
||||
@@ -88,13 +101,13 @@ def _get_weekly_downloads(packages: list[Package]) -> list[ResolvedPackage]:
|
||||
|
||||
|
||||
|
||||
def main(output_file: str) -> None:
|
||||
def main(output_file: str, fake: bool) -> None:
|
||||
"""Main function to generate package download information.
|
||||
|
||||
Args:
|
||||
output_file: Path to the output YAML file.
|
||||
"""
|
||||
resolved_packages: list[ResolvedPackage] = _get_weekly_downloads(PACKAGES)
|
||||
resolved_packages: list[ResolvedPackage] = _get_weekly_downloads(PACKAGES, fake)
|
||||
|
||||
if not output_file.endswith(".yml"):
|
||||
raise ValueError("Output file must have a .yml extension")
|
||||
@@ -115,6 +128,15 @@ if __name__ == "__main__":
|
||||
"downloads.yml"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fake",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help=(
|
||||
"Generate fake download counts for testing purposes. "
|
||||
"This option will not make any network requests."
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args.output_file)
|
||||
main(args.output_file, args.fake)
|
||||
|
||||
@@ -23,4 +23,19 @@ packages:
|
||||
description: "Build swarm-style multi-agent systems using LangGraph."
|
||||
- name: "delve-taxonomy-generator"
|
||||
repo: "andrestorres123/delve"
|
||||
description: "A taxonomy generator for unstructured data"
|
||||
description: "A taxonomy generator for unstructured data"
|
||||
- name: "nodeology"
|
||||
repo: "xyin-anl/Nodeology"
|
||||
description: "Enable researcher to build scientific workflows easily with simplified interface."
|
||||
- name: "langgraph-bigtool"
|
||||
repo: "langchain-ai/langgraph-bigtool"
|
||||
description: "Build LangGraph agents with large numbers of tools."
|
||||
- name: "ai-data-science-team"
|
||||
repo: "business-science/ai-data-science-team"
|
||||
description: "An AI-powered data science team of agents to help you perform common data science tasks 10X faster."
|
||||
- name: "langgraph-reflection"
|
||||
repo: "langchain-ai/langgraph-reflection"
|
||||
description: "LangGraph agent that runs a reflection step."
|
||||
- name: "langmanus"
|
||||
repo: "langmanus/langmanus"
|
||||
description: "A community-driven AI automation framework that builds upon the incredible work of the open source community. Our goal is to combine language models with specialized tools for tasks like web search, crawling, and Python code execution, while giving back to the community that made this possible."
|
||||
|
||||
@@ -17,7 +17,7 @@ This guide explains how to add semantic search to your LangGraph deployment's cr
|
||||
...
|
||||
"store": {
|
||||
"index": {
|
||||
"embed": "openai:text-embeddings-3-small",
|
||||
"embed": "openai:text-embedding-3-small",
|
||||
"dims": 1536,
|
||||
"fields": ["$"]
|
||||
}
|
||||
@@ -27,7 +27,7 @@ This guide explains how to add semantic search to your LangGraph deployment's cr
|
||||
|
||||
This configuration:
|
||||
|
||||
- Uses OpenAI's text-embeddings-3-small model for generating embeddings
|
||||
- Uses OpenAI's text-embedding-3-small model for generating embeddings
|
||||
- Sets the embedding dimension to 1536 (matching the model's output)
|
||||
- Indexes all fields in your stored data (`["$"]` means index everything, or specify specific fields like `["text", "metadata.title"]`)
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ license = "MIT"
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9.0,<3.13"
|
||||
python = ">=3.9"
|
||||
langgraph = "^0.2.0"
|
||||
langchain-fireworks = "^0.1.3"
|
||||
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
# How to implement Generative User Interfaces with LangGraph
|
||||
|
||||
!!! info "Prerequisites"
|
||||
|
||||
- [LangGraph Platform](../../concepts/langgraph_platform.md)
|
||||
- [LangGraph Server](../../concepts/langgraph_server.md)
|
||||
- [`useStream()` React Hook](./use_stream_react.md)
|
||||
|
||||
Generative user interfaces (Generative UI) allows agents to go beyond text and generate rich user interfaces. This enables creating more interactive and context-aware applications where the UI adapts based on the conversation flow and AI responses.
|
||||
|
||||

|
||||
|
||||
LangGraph Platform supports colocating your React components with your graph code. This allows you to focus on building specific UI components for your graph while easily plugging into existing chat interfaces such as [Agent Chat](https://agentchat.vercel.app) and loading the code only when actually needed.
|
||||
|
||||
!!! warning "LangGraph.js only"
|
||||
|
||||
Currently only LangGraph.js supports Generative UI. Support for Python is coming soon.
|
||||
|
||||
## Tutorial
|
||||
|
||||
### 1. Define and configure UI components
|
||||
|
||||
First, create your first UI component. For each component you need to provide an unique identifier that will be used to reference the component in your graph code.
|
||||
|
||||
```tsx title="src/agent/ui.tsx"
|
||||
const WeatherComponent = (props: { city: string }) => {
|
||||
return <div>Weather for {props.city}</div>;
|
||||
};
|
||||
|
||||
export default {
|
||||
weather: WeatherComponent,
|
||||
};
|
||||
```
|
||||
|
||||
Next, define your UI components in your `langgraph.json` configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"node_version": "20",
|
||||
"graphs": {
|
||||
"agent": "./src/agent/index.ts:graph"
|
||||
},
|
||||
"ui": {
|
||||
"agent": "./src/agent/ui.tsx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `ui` section points to the UI components that will be used by graphs. By default, we recommend using the same key as the graph name, but you can split out the components however you like, see [Customise the namespace of UI components](#customise-the-namespace-of-ui-components) for more details.
|
||||
|
||||
LangGraph Platform will automatically bundle your UI components code and styles and serve them as external assets that can be loaded by the `LoadExternalComponent` component. Some dependencies such as `react` and `react-dom` will be automatically excluded from the bundle.
|
||||
|
||||
CSS and Tailwind 4.x is also supported out of the box, so you can freely use Tailwind classes as well as `shadcn/ui` in your UI components.
|
||||
|
||||
=== "`src/agent/ui.tsx`"
|
||||
|
||||
```tsx
|
||||
import "./styles.css";
|
||||
|
||||
const WeatherComponent = (props: { city: string }) => {
|
||||
return <div className="bg-red-500">Weather for {props.city}</div>;
|
||||
};
|
||||
|
||||
export default {
|
||||
weather: WeatherComponent,
|
||||
};
|
||||
```
|
||||
|
||||
=== "`src/agent/styles.css`"
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
```
|
||||
|
||||
### 2. Send the UI components in your graph
|
||||
|
||||
Use the `typedUi` utility to emit UI elements from your agent nodes:
|
||||
|
||||
```typescript title="src/agent/index.ts"
|
||||
import {
|
||||
typedUi,
|
||||
uiMessageReducer,
|
||||
} from "@langchain/langgraph-sdk/react-ui/server";
|
||||
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
import type ComponentMap from "./ui.js";
|
||||
|
||||
import {
|
||||
Annotation,
|
||||
MessagesAnnotation,
|
||||
StateGraph,
|
||||
type LangGraphRunnableConfig,
|
||||
} from "@langchain/langgraph";
|
||||
|
||||
const AgentState = Annotation.Root({
|
||||
...MessagesAnnotation.spec,
|
||||
ui: Annotation({ reducer: uiMessageReducer, default: () => [] }),
|
||||
});
|
||||
|
||||
export const graph = new StateGraph(AgentState)
|
||||
.addNode("weather", async (state, config) => {
|
||||
// Provide the type of the component map to ensure
|
||||
// type safety of `ui.push()` calls as well as
|
||||
// pushing the messages to the `ui` and sending a custom event as well.
|
||||
const ui = typedUi<typeof ComponentMap>(config);
|
||||
|
||||
const weather = await new ChatOpenAI({ model: "gpt-4o-mini" })
|
||||
.withStructuredOutput(z.object({ city: z.string() }))
|
||||
.withConfig({ tags: ["langsmith:nostream"] })
|
||||
.invoke(state.messages);
|
||||
|
||||
const response = {
|
||||
id: uuidv4(),
|
||||
type: "ai",
|
||||
content: `Here's the weather for ${weather.city}`,
|
||||
};
|
||||
|
||||
// Emit UI elements with associated AI message
|
||||
ui.push({ name: "weather", props: weather }, { message: response });
|
||||
|
||||
return { messages: [response] };
|
||||
})
|
||||
.addEdge("__start__", "weather")
|
||||
.compile();
|
||||
```
|
||||
|
||||
### 3. Handle UI elements in your React application
|
||||
|
||||
On the client side, you can use `useStream()` and `LoadExternalComponent` to display the UI elements.
|
||||
|
||||
```tsx title="src/app/page.tsx"
|
||||
"use client";
|
||||
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
import { LoadExternalComponent } from "@langchain/langgraph-sdk/react-ui";
|
||||
|
||||
export default function Page() {
|
||||
const { thread, values } = useStream({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{thread.messages.map((message) => (
|
||||
<div key={message.id}>
|
||||
{message.content}
|
||||
{values.ui
|
||||
?.filter((ui) => ui.metadata?.message_id === message.id)
|
||||
.map((ui) => (
|
||||
<LoadExternalComponent key={ui.id} stream={thread} message={ui} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Behind the scenes, `LoadExternalComponent` will fetch the JS and CSS for the UI components from LangGraph Platform and render them in a shadow DOM, thus ensuring style isolation from the rest of your application.
|
||||
|
||||
## How-to guides
|
||||
|
||||
### Show loading UI when components are loading
|
||||
|
||||
You can provide a fallback UI to be rendered when the components are loading.
|
||||
|
||||
```tsx
|
||||
<LoadExternalComponent
|
||||
stream={thread}
|
||||
message={ui}
|
||||
fallback={<div>Loading...</div>}
|
||||
/>
|
||||
```
|
||||
|
||||
### Provide custom components on the client side
|
||||
|
||||
If you already have the components loaded in your client application, you can provide a map of such components to be rendered directly without fetching the UI code from LangGraph Platform.
|
||||
|
||||
```tsx
|
||||
const clientComponents = {
|
||||
weather: WeatherComponent,
|
||||
};
|
||||
|
||||
<LoadExternalComponent
|
||||
stream={thread}
|
||||
message={ui}
|
||||
components={clientComponents}
|
||||
/>;
|
||||
```
|
||||
|
||||
### Customise the namespace of UI components.
|
||||
|
||||
By default `LoadExternalComponent` will use the `assistantId` from `useStream()` hook to fetch the code for UI components. You can customise this by providing a `namespace` prop to the `LoadExternalComponent` component.
|
||||
|
||||
=== "`src/app/page.tsx`"
|
||||
|
||||
```tsx
|
||||
<LoadExternalComponent
|
||||
stream={thread}
|
||||
message={ui}
|
||||
namespace="custom-namespace"
|
||||
/>
|
||||
```
|
||||
|
||||
=== "`langgraph.json`"
|
||||
|
||||
```json
|
||||
{
|
||||
"ui": {
|
||||
"custom-namespace": "./src/agent/ui.tsx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Access and interact with the thread state from the UI component
|
||||
|
||||
You can access the thread state inside the UI component by using the `useStreamContext` hook.
|
||||
|
||||
```tsx
|
||||
import { useStreamContext } from "@langchain/langgraph-sdk/react-ui";
|
||||
|
||||
const WeatherComponent = (props: { city: string }) => {
|
||||
const { thread, submit } = useStreamContext();
|
||||
return (
|
||||
<>
|
||||
<div>Weather for {props.city}</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
const newMessage = {
|
||||
type: "human",
|
||||
content: `What's the weather in ${props.city}?`,
|
||||
};
|
||||
|
||||
submit({ messages: [newMessage] });
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Pass additional context to the client components
|
||||
|
||||
You can pass additional context to the client components by providing a `meta` prop to the `LoadExternalComponent` component.
|
||||
|
||||
```tsx
|
||||
<LoadExternalComponent stream={thread} message={ui} meta={{ userId: "123" }} />
|
||||
```
|
||||
|
||||
Then, you can access the `meta` prop in the UI component by using the `useStreamContext` hook.
|
||||
|
||||
```tsx
|
||||
import { useStreamContext } from "@langchain/langgraph-sdk/react-ui";
|
||||
|
||||
const WeatherComponent = (props: { city: string }) => {
|
||||
const { meta } = useStreamContext<
|
||||
{ city: string },
|
||||
{ MetaType: { userId?: string } }
|
||||
>();
|
||||
|
||||
return (
|
||||
<div>
|
||||
Weather for {props.city} (user: {meta?.userId})
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Streaming UI updates before the node execution is finished
|
||||
|
||||
You can stream UI updates before the node execution is finished by using the `onCustomEvent` callback of the `useStream()` hook.
|
||||
|
||||
```tsx
|
||||
import { uiMessageReducer } from "@langchain/langgraph-sdk/react-ui";
|
||||
|
||||
const { thread, submit } = useStream({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
onCustomEvent: (event, options) => {
|
||||
options.mutate((prev) => {
|
||||
const ui = uiMessageReducer(prev.ui ?? [], event);
|
||||
return { ...prev, ui };
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Remove UI messages from state
|
||||
|
||||
Similar to how messages can be removed from the state by appending a RemoveMessage you can remove an UI message from the state by calling `ui.delete` with the ID of the UI message.
|
||||
|
||||
```tsx
|
||||
// pushed message
|
||||
const message = ui.push({ name: "weather", props: { city: "London" } });
|
||||
|
||||
// remove said message
|
||||
ui.delete(message.id);
|
||||
|
||||
// return new state to persist changes
|
||||
return { ui: ui.items };
|
||||
```
|
||||
|
||||
## Learn more
|
||||
|
||||
- [JS/TS SDK Reference](../reference/sdk/js_ts_sdk_ref.md)
|
||||
@@ -63,7 +63,7 @@ Now, let's invoke our graph by interrupting before `ask_human` node:
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Use the search tool to ask the user where they are, then look up the weather there",
|
||||
"content": "Ask the user where they are, then look up the weather there",
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -85,8 +85,7 @@ Now, let's invoke our graph by interrupting before `ask_human` node:
|
||||
messages: [
|
||||
{
|
||||
role: "human",
|
||||
content: "Use the search tool to ask the user where they are, then look up the weather there"
|
||||
}
|
||||
content: "Ask the user where they are, then look up the weather there" }
|
||||
]
|
||||
};
|
||||
|
||||
@@ -115,7 +114,7 @@ Now, let's invoke our graph by interrupting before `ask_human` node:
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"Use the search tool to ask the user where they are, then look up the weather there\"}]},
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"Ask the user where they are, then look up the weather there\"}]},
|
||||
\"interrupt_before\": [\"ask_human\"],
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
@@ -1,6 +1,133 @@
|
||||
# Prompt Engineering in LangGraph Studio
|
||||
|
||||
In LangGraph Studio you can iterate on the prompts used within your graph by utilizing the LangSmith Playground. To do so:
|
||||
## Overview
|
||||
|
||||
A central aspect of agent development is prompt engineering. LangGraph Studio makes it easy to iterate on the prompts used within your graph directly within the UI.
|
||||
|
||||
## Setup
|
||||
|
||||
The first step is to define your [configuration](https://langchain-ai.github.io/langgraph/how-tos/configuration/) such that LangGraph Studio is aware of the prompts you want to iterate on and which nodes they are associated with.
|
||||
|
||||
### Reference
|
||||
|
||||
When defining your configuration, you can use special metadata keys to instruct LangGraph Studio how to handle different fields. Here's a reference for the available configuration options:
|
||||
|
||||
#### `langgraph_nodes`
|
||||
|
||||
- **Description**: Specifies which graph nodes a configuration field is associated with.
|
||||
- **Value Type**: Array of strings, where each string is the name of a node in your graph.
|
||||
- **Usage Context**: Include in the `json_schema_extra` dictionary for Pydantic models or the `metadata["json_schema_extra"]` dictionary for dataclasses.
|
||||
- **Required**: No, but necessary if you want a field to be editable for specific nodes in the UI.
|
||||
- **Example**:
|
||||
```python
|
||||
system_prompt: str = Field(
|
||||
default="You are a helpful AI assistant.",
|
||||
json_schema_extra={"langgraph_nodes": ["call_model", "other_node"]},
|
||||
)
|
||||
```
|
||||
|
||||
#### `langgraph_type`
|
||||
|
||||
- **Description**: Specifies the type of configuration field, which determines how it's handled in the UI.
|
||||
- **Value Type**: String
|
||||
- **Supported Values**:
|
||||
- `"prompt"`: Indicates the field contains prompt text that should be treated specially in the UI.
|
||||
- **Usage Context**: Include in the `json_schema_extra` dictionary for Pydantic models or the `metadata["json_schema_extra"]` dictionary for dataclasses.
|
||||
- **Required**: No, but helpful for prompt fields to enable special handling.
|
||||
- **Example**:
|
||||
```python
|
||||
system_prompt: str = Field(
|
||||
default="You are a helpful AI assistant.",
|
||||
json_schema_extra={
|
||||
"langgraph_nodes": ["call_model"],
|
||||
"langgraph_type": "prompt",
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
For example, if you have a node called `call_model` whose system prompt you want to iterate on, you can define a configuration like the following.
|
||||
|
||||
```python
|
||||
## Using Pydantic
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Annotated, Literal
|
||||
|
||||
class Configuration(BaseModel):
|
||||
"""The configuration for the agent."""
|
||||
|
||||
system_prompt: str = Field(
|
||||
default="You are a helpful AI assistant.",
|
||||
description="The system prompt to use for the agent's interactions. "
|
||||
"This prompt sets the context and behavior for the agent.",
|
||||
json_schema_extra={
|
||||
"langgraph_nodes": ["call_model"],
|
||||
"langgraph_type": "prompt",
|
||||
},
|
||||
)
|
||||
|
||||
model: Annotated[
|
||||
Literal[
|
||||
"anthropic/claude-3-7-sonnet-latest",
|
||||
"anthropic/claude-3-5-haiku-latest",
|
||||
"openai/o1",
|
||||
"openai/gpt-4o-mini",
|
||||
"openai/o1-mini",
|
||||
"openai/o3-mini",
|
||||
],
|
||||
{"__template_metadata__": {"kind": "llm"}},
|
||||
] = Field(
|
||||
default="openai/gpt-4o-mini",
|
||||
description="The name of the language model to use for the agent's main interactions. "
|
||||
"Should be in the form: provider/model-name.",
|
||||
json_schema_extra={"langgraph_nodes": ["call_model"]},
|
||||
)
|
||||
|
||||
## Using Dataclasses
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Configuration:
|
||||
"""The configuration for the agent."""
|
||||
|
||||
system_prompt: str = field(
|
||||
default="You are a helpful AI assistant.",
|
||||
metadata={
|
||||
"description": "The system prompt to use for the agent's interactions. "
|
||||
"This prompt sets the context and behavior for the agent.",
|
||||
"json_schema_extra": {"langgraph_nodes": ["call_model"]},
|
||||
},
|
||||
)
|
||||
|
||||
model: Annotated[str, {"__template_metadata__": {"kind": "llm"}}] = field(
|
||||
default="anthropic/claude-3-5-sonnet-20240620",
|
||||
metadata={
|
||||
"description": "The name of the language model to use for the agent's main interactions. "
|
||||
"Should be in the form: provider/model-name.",
|
||||
"json_schema_extra": {"langgraph_nodes": ["call_model"]},
|
||||
},
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
## Iterating on prompts
|
||||
|
||||
### Node Configuration
|
||||
|
||||
With this set up, running your graph and viewing in LangGraph Studio will result in the graph rendering like such.
|
||||
|
||||
**Note the configuration icon in the top right corner of the `call_model` node**:
|
||||
|
||||
{width=1200}
|
||||
|
||||
Clicking this icon will open a modal where you can edit the configuration for all of the fields associated with the `call_model` node. From here, you can save your changes and apply them to the graph. Note that these values reflect the currently active assistant, and saving will update the assistant with the new values.
|
||||
|
||||
{width=1200}
|
||||
|
||||
### Playground
|
||||
|
||||
LangGraph Studio also supports prompt engineering through an integration with the LangSmith Playground. To do so:
|
||||
|
||||
1. Open an existing thread or create a new one.
|
||||
2. Within the thread log, any nodes that have made an LLM call will have a "View LLM Runs" button. Clicking this will open a popover with the LLM runs for that node.
|
||||
@@ -8,8 +135,6 @@ In LangGraph Studio you can iterate on the prompts used within your graph by uti
|
||||
|
||||
{width=1200}
|
||||
|
||||
|
||||
|
||||
From here you can edit the prompt, test different model configurations and re-run just this LLM call without having to re-run the entire graph. When you are happy with your changes, you can copy the updated prompt back into your graph.
|
||||
|
||||
For more information on how to use the LangSmith Playground, see the [LangSmith Playground documentation](https://docs.smith.langchain.com/prompt_engineering/how_to_guides#playground).
|
||||
|
||||
@@ -14,7 +14,7 @@ As a result, there are many different types of [agent architectures](https://blo
|
||||
|
||||
## Router
|
||||
|
||||
A router allows an LLM to select a single step from a specified set of options. This is an agent architecture that exhibits a relatively limited level of control because the LLM usually focuses on making a single decision and produces a specific output from limited set of pre-defined options. Routers typically employ a few different concepts to achieve this.
|
||||
A router allows an LLM to select a single step from a specified set of options. This is an agent architecture that exhibits a relatively limited level of control because the LLM usually focuses on making a single decision and produces a specific output from a limited set of pre-defined options. Routers typically employ a few different concepts to achieve this.
|
||||
|
||||
### Structured Output
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## LLM applications
|
||||
|
||||
LLMs make it possible to embed intelligence into a new class of applications. There are many patterns for building applications that use LLMs. [Workflows](https://www.anthropic.com/research/building-effective-agents) have scaffolding of predefined code paths around LLM calls. LLMs can direct the control flow through these predefined code paths, which some consider to be an "[agentic system](https://www.anthropic.com/research/building-effective-agents)". In other cases, it's possible to remove this scaffolding, creating autonomous agents that can [plan](https://huyenchip.com/2025/01/07/agents.html), take actions via [tool calls](https://python.langchain.com/docs/concepts/tool_calling/), and directly respond [to the feedback from their own actions](https://research.google/blog/react-synergizing-reasoning-and-acting-in-language-models/) with further actions.
|
||||
LLMs make it possible to embed intelligence into a new class of applications. There are many patterns for building applications that use LLMs. Workflows have scaffolding of predefined code paths around LLM calls. LLMs can direct the control flow through these predefined code paths, which some consider to be an "agentic system". In other cases, it's possible to remove this scaffolding, creating autonomous agents that can [plan](https://huyenchip.com/2025/01/07/agents.html), take actions via [tool calls](https://python.langchain.com/docs/concepts/tool_calling/), and directly respond [to the feedback from their own actions](https://research.google/blog/react-synergizing-reasoning-and-acting-in-language-models/) with further actions.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
search:
|
||||
boost: 2
|
||||
---
|
||||
|
||||
# LangGraph Platform
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -360,7 +360,7 @@ Use [conditional edges](#conditional-edges) to route between nodes conditionally
|
||||
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"]]:
|
||||
def my_node(state: State) -> Command[Literal["other_subgraph"]]:
|
||||
return Command(
|
||||
update={"foo": "bar"},
|
||||
goto="other_subgraph", # where `other_subgraph` is a node in the parent graph
|
||||
|
||||
@@ -275,7 +275,7 @@ See this how-to [video](https://www.youtube.com/watch?v=37VaU7e7t5o) for example
|
||||
|
||||
[Procedural memory](https://en.wikipedia.org/wiki/Procedural_memory), in both humans and AI agents, involves remembering the rules used to perform tasks. In humans, procedural memory is like the internalized knowledge of how to perform tasks, such as riding a bike via basic motor skills and balance. Episodic memory, on the other hand, involves recalling specific experiences, such as the first time you successfully rode a bike without training wheels or a memorable bike ride through a scenic route. For AI agents, procedural memory is a combination of model weights, agent code, and agent's prompt that collectively determine the agent's functionality.
|
||||
|
||||
In practice, it is fairly uncommon for agents to modify their model weights or rewrite their code. However, it is more common for agents to [modify their own prompts](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/prompt-generator).
|
||||
In practice, it is fairly uncommon for agents to modify their model weights or rewrite their code. However, it is more common for agents to modify their own prompts.
|
||||
|
||||
One effective approach to refining an agent's instructions is through ["Reflection"](https://blog.langchain.dev/reflection-agents/) or meta-prompting. This involves prompting the agent with its current instructions (e.g., the system prompt) along with recent conversations or explicit user feedback. The agent then refines its own instructions based on this input. This method is particularly useful for tasks where instructions are challenging to specify upfront, as it allows the agent to learn and adapt from its interactions.
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ def transfer_to_bob(state):
|
||||
)
|
||||
```
|
||||
|
||||
This is a special case of updating the graph state from tools where in addition the state update, the control flow is included as well.
|
||||
This is a special case of updating the graph state from tools where, in addition to the state update, the control flow is included as well.
|
||||
|
||||
!!! important
|
||||
|
||||
@@ -235,7 +235,7 @@ supervisor = create_react_agent(model, tools)
|
||||
|
||||
### Hierarchical
|
||||
|
||||
As you add more agents to your system, it might become too hard for the supervisor to manage all of them. The supervisor might start making poor decisions about which agent to call next, the context might become too complex for a single supervisor to keep track of. In other words, you end up with the same problems that motivated the multi-agent architecture in the first place.
|
||||
As you add more agents to your system, it might become too hard for the supervisor to manage all of them. The supervisor might start making poor decisions about which agent to call next, or the context might become too complex for a single supervisor to keep track of. In other words, you end up with the same problems that motivated the multi-agent architecture in the first place.
|
||||
|
||||
To address this, you can design your system _hierarchically_. For example, you can create separate, specialized teams of agents managed by individual supervisors, and a top-level supervisor to manage the teams.
|
||||
|
||||
@@ -339,9 +339,9 @@ builder.add_edge("agent_1", "agent_2")
|
||||
|
||||
## Communication between agents
|
||||
|
||||
The most important thing when building multi-agent systems is figuring out how the agents communicate. There are few different considerations:
|
||||
The most important thing when building multi-agent systems is figuring out how the agents communicate. There are a few different considerations:
|
||||
|
||||
- Do agents communicate via [**via graph state or via tool calls**](#graph-state-vs-tool-calls)?
|
||||
- Do agents communicate [**via graph state or via tool calls**](#graph-state-vs-tool-calls)?
|
||||
- What if two agents have [**different state schemas**](#different-state-schemas)?
|
||||
- How to communicate over a [**shared message list**](#shared-message-list)?
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ from typing_extensions import TypedDict
|
||||
from operator import add
|
||||
|
||||
class State(TypedDict):
|
||||
foo: int
|
||||
foo: str
|
||||
bar: Annotated[list[str], add]
|
||||
|
||||
def node_a(state: State):
|
||||
@@ -232,7 +232,7 @@ from langgraph.store.memory import InMemoryStore
|
||||
in_memory_store = InMemoryStore()
|
||||
```
|
||||
|
||||
Memories are namespaced by a `tuple`, which in this specific example will be `(<user_id>, "memories")`. The namespace can be any length and represent anything, does not have be user specific.
|
||||
Memories are namespaced by a `tuple`, which in this specific example will be `(<user_id>, "memories")`. The namespace can be any length and represent anything, does not have to be user specific.
|
||||
|
||||
```python
|
||||
user_id = "1"
|
||||
@@ -387,6 +387,9 @@ We can access the memories and use them in our model call.
|
||||
def call_model(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
|
||||
# Get the user id from the config
|
||||
user_id = config["configurable"]["user_id"]
|
||||
|
||||
# Namespace the memory
|
||||
namespace = (user_id, "memories")
|
||||
|
||||
# Search based on the most recent message
|
||||
memories = store.search(
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
search:
|
||||
exclude: true
|
||||
---
|
||||
|
||||
# Human-in-the-loop
|
||||
|
||||
!!! note "Use the `interrupt` function instead."
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
" )\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"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`:\n",
|
||||
"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`:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"def my_node(state: State) -> Command[Literal[\"my_other_node\"]]:\n",
|
||||
|
||||
@@ -122,20 +122,18 @@
|
||||
"\n",
|
||||
"# For this tutorial we will use custom tool that returns pre-defined values for weather in two cities (NYC & SF)\n",
|
||||
"\n",
|
||||
"from typing import Literal\n",
|
||||
"\n",
|
||||
"from langchain_core.tools import tool\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def get_weather(city: Literal[\"nyc\", \"sf\"]):\n",
|
||||
"def get_weather(location: str) -> str:\n",
|
||||
" \"\"\"Use this to get weather information.\"\"\"\n",
|
||||
" if city == \"nyc\":\n",
|
||||
" if any([city in location.lower() for city in [\"nyc\", \"new york city\"]]):\n",
|
||||
" return \"It might be cloudy in nyc\"\n",
|
||||
" elif city == \"sf\":\n",
|
||||
" elif any([city in location.lower() for city in [\"sf\", \"san francisco\"]]):\n",
|
||||
" return \"It's always sunny in sf\"\n",
|
||||
" else:\n",
|
||||
" raise AssertionError(\"Unknown city\")\n",
|
||||
" return f\"I am not sure what the weather is in {location}\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"tools = [get_weather]\n",
|
||||
@@ -220,7 +218,7 @@
|
||||
"id": "838a043f-90ad-4e69-9d1d-6e22db2c346c",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Notice that when we pass the same the same thread ID, the chat history is preserved"
|
||||
"Notice that when we pass the same thread ID, the chat history is preserved."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -397,7 +397,8 @@
|
||||
"# We define a fake node to ask the human\n",
|
||||
"def ask_human(state):\n",
|
||||
" tool_call_id = state[\"messages\"][-1].tool_calls[0][\"id\"]\n",
|
||||
" location = interrupt(\"Please provide your location:\")\n",
|
||||
" ask = AskHuman.model_validate(state[\"messages\"][-1].tool_calls[0][\"args\"])\n",
|
||||
" location = interrupt(ask.question)\n",
|
||||
" tool_message = [{\"tool_call_id\": tool_call_id, \"type\": \"tool\", \"content\": location}]\n",
|
||||
" return {\"messages\": tool_message}\n",
|
||||
"\n",
|
||||
@@ -491,7 +492,7 @@
|
||||
" \"messages\": [\n",
|
||||
" (\n",
|
||||
" \"user\",\n",
|
||||
" \"Use the search tool to ask the user where they are, then look up the weather there\",\n",
|
||||
" \"Ask the user where they are, then look up the weather there\",\n",
|
||||
" )\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
|
||||
@@ -198,7 +198,6 @@ 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
|
||||
|
||||
@@ -257,6 +256,13 @@ Streaming the results of your LLM application is vital for ensuring a good user
|
||||
- [How to stream in debug mode](../cloud/how-tos/stream_debug.md)
|
||||
- [How to stream multiple modes](../cloud/how-tos/stream_multiple.md)
|
||||
|
||||
### Frontend and Generative UI
|
||||
|
||||
With LangGraph Platform you can integrate LangGraph agents into your React applications and colocate UI components with your agent code.
|
||||
|
||||
- [How to integrate LangGraph into your React application](../cloud/how-tos/use_stream_react.md)
|
||||
- [How to implement Generative User Interfaces with LangGraph](../cloud/how-tos/generative_ui_react.md)
|
||||
|
||||
### Human-in-the-loop
|
||||
|
||||
When designing complex graphs, relying entirely on the LLM for decision-making can be risky, particularly when it involves tools that interact with files, APIs, or databases. These interactions may lead to unintended data access or modifications, depending on the use case. To mitigate these risks, LangGraph allows you to integrate human-in-the-loop behavior, ensuring your LLM applications operate as intended without undesirable outcomes.
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"One of the most common use cases for persistence is to use it to keep track of conversation history. This is great - it makes it easy to continue conversations. As conversations get longer and longer, however, this conversation history can build up and take up more and more of the context window. This can often be undesirable as it leads to more expensive and longer calls to the LLM, and potentially ones that error. One way to work around that is to create a summary of the conversation to date, and use that with the past N messages. This guide will go through an example of how to do that.\n",
|
||||
"\n",
|
||||
"This will involve a few steps:\n",
|
||||
"\n",
|
||||
"- Check if the conversation is too long (can be done by checking number of messages or length of messages)\n",
|
||||
"- If yes, the create summary (will need a prompt for this)\n",
|
||||
"- Then remove all except the last N messages\n",
|
||||
@@ -98,7 +99,7 @@
|
||||
"from typing import Literal\n",
|
||||
"\n",
|
||||
"from langchain_anthropic import ChatAnthropic\n",
|
||||
"from langchain_core.messages import SystemMessage, RemoveMessage\n",
|
||||
"from langchain_core.messages import SystemMessage, RemoveMessage, HumanMessage\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.graph import MessagesState, StateGraph, START, END\n",
|
||||
"\n",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"source": [
|
||||
"# How to manage conversation history\n",
|
||||
"\n",
|
||||
"One of the most common use cases for persistence is to use it to keep track of conversation history. This is great - it makes it easy to continue conversations. As conversations get longer and longer, however, this conversation history can build up and take up more and more of the context window. This can often be undesirable as it leads to more expensive and longer calls to the LLM, and potentially ones that error. In order to prevent this from happening, you need to probably manage the conversation history.\n",
|
||||
"One of the most common use cases for persistence is to use it to keep track of conversation history. This is great - it makes it easy to continue conversations. As conversations get longer and longer, however, this conversation history can build up and take up more and more of the context window. This can often be undesirable as it leads to more expensive and longer calls to the LLM, and potentially ones that error. In order to prevent this from happening, you need to properly manage the conversation history.\n",
|
||||
"\n",
|
||||
"Note: this guide focuses on how to do this in LangGraph, where you can fully customize how this is done. If you want a more off-the-shelf solution, you can look into functionality provided in LangChain:\n",
|
||||
"\n",
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
" </p>\n",
|
||||
"</div> \n",
|
||||
"\n",
|
||||
"The core technique the examples below is to **annotate** a parameter as \"injected\", meaning it will be injected by your program and should not be seen or populated by the LLM. Let the following codesnippet serve as a tl;dr:\n",
|
||||
"The core technique in the examples below is to **annotate** a parameter as \"injected\", meaning it will be injected by your program and should not be seen or populated by the LLM. Let the following codesnippet serve as a tl;dr:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"from typing import Annotated\n",
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"\n",
|
||||
"**Pros and Cons**\n",
|
||||
"\n",
|
||||
"The benefit to this format is that you only need one LLM, and can save money and latency because of this. The downside to this option is that it isn't guaranteed that the single LLM will call the correct tool when you want it to. We can help the LLM by setting `tool_choice` to `any` when we use `bind_tools` which forces the LLM to select at least one tool at every turn, but this is far from a fool proof strategy. In addition, another downside is that the agent might call *multiple* tools, so we need to check for this explicitly in our routing function (or if we are using OpenAI we an set `parallell_tool_calling=False` to ensure only one tool is called at a time).\n",
|
||||
"The benefit to this format is that you only need one LLM, and can save money and latency because of this. The downside to this option is that it isn't guaranteed that the single LLM will call the correct tool when you want it to. We can help the LLM by setting `tool_choice` to `any` when we use `bind_tools` which forces the LLM to select at least one tool at every turn, but this is far from a foolproof strategy. In addition, another downside is that the agent might call *multiple* tools, so we need to check for this explicitly in our routing function (or if we are using OpenAI we can set `parallell_tool_calling=False` to ensure only one tool is called at a time).\n",
|
||||
"\n",
|
||||
"**Option 2**\n",
|
||||
"\n",
|
||||
|
||||
@@ -266,6 +266,235 @@
|
||||
" print(\"An exception was raised because bad_node sets `a` to an integer.\")\n",
|
||||
" print(e)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "2270bc3c",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Multiple Nodes\n",
|
||||
"\n",
|
||||
"Run-time validation will also work in a multi-node graph. In the example below `bad_node` updates `a` to an integer. \n",
|
||||
"\n",
|
||||
"Because run-time validation occurs on **inputs**, the validation error will occur when `ok_node` is called (not when `bad_node` returns an update to the state which is inconsistent with the schema)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "d832cdcc",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"from pydantic import BaseModel\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# The overall state of the graph (this is the public state shared across nodes)\n",
|
||||
"class OverallState(BaseModel):\n",
|
||||
" a: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def bad_node(state: OverallState):\n",
|
||||
" return {\n",
|
||||
" \"a\": 123 # Invalid\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def ok_node(state: OverallState):\n",
|
||||
" return {\"a\": \"goodbye\"}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Build the state graph\n",
|
||||
"builder = StateGraph(OverallState)\n",
|
||||
"builder.add_node(bad_node)\n",
|
||||
"builder.add_node(ok_node)\n",
|
||||
"builder.add_edge(START, \"bad_node\")\n",
|
||||
"builder.add_edge(\"bad_node\", \"ok_node\")\n",
|
||||
"builder.add_edge(\"ok_node\", END)\n",
|
||||
"graph = builder.compile()\n",
|
||||
"\n",
|
||||
"# Test the graph with a valid input\n",
|
||||
"try:\n",
|
||||
" graph.invoke({\"a\": \"hello\"})\n",
|
||||
"except Exception as e:\n",
|
||||
" print(\"An exception was raised because bad_node sets `a` to an integer.\")\n",
|
||||
" print(e)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "456b1f77",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Advanced Pydantic Model Usage\n",
|
||||
"\n",
|
||||
"This section covers more advanced topics when using Pydantic models with LangGraph.\n",
|
||||
"\n",
|
||||
"### Serialization Behavior\n",
|
||||
"\n",
|
||||
"When using Pydantic models as state schemas, it's important to understand how serialization works, especially when:\n",
|
||||
"- Passing Pydantic objects as inputs\n",
|
||||
"- Receiving outputs from the graph\n",
|
||||
"- Working with nested Pydantic models\n",
|
||||
"\n",
|
||||
"Let's see these behaviors in action:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "0e919cdc",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"from pydantic import BaseModel\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class NestedModel(BaseModel):\n",
|
||||
" value: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class ComplexState(BaseModel):\n",
|
||||
" text: str\n",
|
||||
" count: int\n",
|
||||
" nested: NestedModel\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def process_node(state: ComplexState):\n",
|
||||
" # Node receives a validated Pydantic object\n",
|
||||
" print(f\"Input state type: {type(state)}\")\n",
|
||||
" print(f\"Nested type: {type(state.nested)}\")\n",
|
||||
"\n",
|
||||
" # Return a dictionary update\n",
|
||||
" return {\"text\": state.text + \" processed\", \"count\": state.count + 1}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Build the graph\n",
|
||||
"builder = StateGraph(ComplexState)\n",
|
||||
"builder.add_node(\"process\", process_node)\n",
|
||||
"builder.add_edge(START, \"process\")\n",
|
||||
"builder.add_edge(\"process\", END)\n",
|
||||
"graph = builder.compile()\n",
|
||||
"\n",
|
||||
"# Create a Pydantic instance for input\n",
|
||||
"input_state = ComplexState(text=\"hello\", count=0, nested=NestedModel(value=\"test\"))\n",
|
||||
"print(f\"Input object type: {type(input_state)}\")\n",
|
||||
"\n",
|
||||
"# Invoke graph with a Pydantic instance\n",
|
||||
"result = graph.invoke(input_state)\n",
|
||||
"print(f\"Output type: {type(result)}\")\n",
|
||||
"print(f\"Output content: {result}\")\n",
|
||||
"\n",
|
||||
"# Convert back to Pydantic model if needed\n",
|
||||
"output_model = ComplexState(**result)\n",
|
||||
"print(f\"Converted back to Pydantic: {type(output_model)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "f13f28ce",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Runtime Type Coercion\n",
|
||||
"\n",
|
||||
"Pydantic performs runtime type coercion for certain data types. This can be helpful but also lead to unexpected behavior if you're not aware of it."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "faf59316",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"from pydantic import BaseModel\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class CoercionExample(BaseModel):\n",
|
||||
" # Pydantic will coerce string numbers to integers\n",
|
||||
" number: int\n",
|
||||
" # Pydantic will parse string booleans to bool\n",
|
||||
" flag: bool\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def inspect_node(state: CoercionExample):\n",
|
||||
" print(f\"number: {state.number} (type: {type(state.number)})\")\n",
|
||||
" print(f\"flag: {state.flag} (type: {type(state.flag)})\")\n",
|
||||
" return {}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"builder = StateGraph(CoercionExample)\n",
|
||||
"builder.add_node(\"inspect\", inspect_node)\n",
|
||||
"builder.add_edge(START, \"inspect\")\n",
|
||||
"builder.add_edge(\"inspect\", END)\n",
|
||||
"graph = builder.compile()\n",
|
||||
"\n",
|
||||
"# Demonstrate coercion with string inputs that will be converted\n",
|
||||
"result = graph.invoke({\"number\": \"42\", \"flag\": \"true\"})\n",
|
||||
"\n",
|
||||
"# This would fail with a validation error\n",
|
||||
"try:\n",
|
||||
" graph.invoke({\"number\": \"not-a-number\", \"flag\": \"true\"})\n",
|
||||
"except Exception as e:\n",
|
||||
" print(f\"\\nExpected validation error: {e}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "2844475b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Working with Message Models\n",
|
||||
"\n",
|
||||
"When working with LangChain message types in your state schema, there are important considerations for serialization. You should use `AnyMessage` (rather than `BaseMessage`) for proper serialization/deserialization when using message objects over the wire:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "bd0734b0",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"from pydantic import BaseModel\n",
|
||||
"from langchain_core.messages import HumanMessage, AIMessage, AnyMessage\n",
|
||||
"from typing import List\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class ChatState(BaseModel):\n",
|
||||
" messages: List[AnyMessage]\n",
|
||||
" context: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def add_message(state: ChatState):\n",
|
||||
" return {\"messages\": state.messages + [AIMessage(content=\"Hello there!\")]}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"builder = StateGraph(ChatState)\n",
|
||||
"builder.add_node(\"add_message\", add_message)\n",
|
||||
"builder.add_edge(START, \"add_message\")\n",
|
||||
"builder.add_edge(\"add_message\", END)\n",
|
||||
"graph = builder.compile()\n",
|
||||
"\n",
|
||||
"# Create input with a message\n",
|
||||
"initial_state = ChatState(\n",
|
||||
" messages=[HumanMessage(content=\"Hi\")], context=\"Customer support chat\"\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"result = graph.invoke(initial_state)\n",
|
||||
"print(f\"Output: {result}\")\n",
|
||||
"\n",
|
||||
"# Convert back to Pydantic model to see message types\n",
|
||||
"output_model = ChatState(**result)\n",
|
||||
"for i, msg in enumerate(output_model.messages):\n",
|
||||
" print(f\"Message {i}: {type(msg).__name__} - {msg.content}\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -210,7 +210,7 @@
|
||||
"id": "cbb06aea-6654-4245-91f8-af6e8f2b5377",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's now add personalization: we'll respond differently to the user based on the state values AFTER the state has been updated from the tool. To achieve this, let's define a function that will dynamically construct the system prompt based on the graph state. It will be called ever time the LLM is called and the function output will be passed to the LLM:"
|
||||
"Let's now add personalization: we'll respond differently to the user based on the state values AFTER the state has been updated from the tool. To achieve this, let's define a function that will dynamically construct the system prompt based on the graph state. It will be called every time the LLM is called and the function output will be passed to the LLM:"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,4 +3,26 @@ hide_comments: true
|
||||
title: Home
|
||||
---
|
||||
|
||||
<script>
|
||||
// This script only runs in MkDocs, not on GitHub
|
||||
var hideGitHubVersion = function() {
|
||||
document.querySelectorAll('.github-only').forEach(el => el.style.display = 'none');
|
||||
};
|
||||
|
||||
// Handle both initial load and subsequent navigation
|
||||
document.addEventListener('DOMContentLoaded', hideGitHubVersion);
|
||||
document$.subscribe(hideGitHubVersion);
|
||||
</script>
|
||||
|
||||
<p class="mkdocs-only">
|
||||
<img class="logo-light" src="static/wordmark_dark.svg" alt="LangGraph Logo" width="80%">
|
||||
<img class="logo-dark" src="static/wordmark_light.svg" alt="LangGraph Logo" width="80%">
|
||||
</p>
|
||||
|
||||
<style>
|
||||
.md-content h1 {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
{!../README.md!}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# LLMs-txt for LangGraph
|
||||
|
||||
## Overview
|
||||
|
||||
LangGraph provides documentation files in the [`llms.txt`](https://llmstxt.org/) format, specifically `llms.txt` and `llms-full.txt`. These files allow large language models (LLMs) and agents to access programming documentation and APIs, particularly useful within integrated development environments (IDEs).
|
||||
|
||||
| Language Version | llms.txt | llms-full.txt |
|
||||
|------------------|------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|
|
||||
| LangGraph Python | [https://langchain-ai.github.io/langgraph/llms.txt](https://langchain-ai.github.io/langgraph/llms.txt) | [https://langchain-ai.github.io/langgraph/llms-full.txt](https://langchain-ai.github.io/langgraph/llms-full.txt) |
|
||||
| LangGraph JS | [https://langchain-ai.github.io/langgraphjs/llms.txt](https://langchain-ai.github.io/langgraphjs/llms.txt) | [https://langchain-ai.github.io/langgraphjs/llms-full.txt](https://langchain-ai.github.io/langgraphjs/llms-full.txt) |
|
||||
|
||||
## Differences Between `llms.txt` and `llms-full.txt`
|
||||
|
||||
- **`llms.txt`** is an index file containing links with brief descriptions of the content. An LLM or agent must follow these links to access detailed information.
|
||||
|
||||
- **`llms-full.txt`** includes all the detailed content directly in a single file, eliminating the need for additional navigation.
|
||||
|
||||
A key consideration when using `llms-full.txt` is its size. For extensive documentation, this file may become too large to fit into an LLM's context window.
|
||||
|
||||
## Using `llms.txt` via an MCP Server
|
||||
|
||||
As of March 9, 2025, IDEs [do not yet have robust native support for `llms.txt`](https://x.com/jeremyphoward/status/1902109312216129905?t=1eHFv2vdNdAckajnug0_Vw&s=19). However, you can utilize `llms.txt` effectively through an MCP server.
|
||||
|
||||
We provide an MCP server specifically designed to serve documentation, called [`mcpdoc`](https://github.com/langchain-ai/mcpdoc). This setup is compatible with IDEs and platforms such as Cursor, Windsurf, Claude, and Claude Code. Instructions for using `mcpdoc` with these tools are available in the repository.
|
||||
|
||||
## Using `llms-full.txt`
|
||||
|
||||
The LangGraph `llms-full.txt` file typically contains several hundred thousand tokens, exceeding the context window limitations of most LLMs. To effectively use this file:
|
||||
|
||||
1. **With IDEs (e.g., Cursor, Windsurf)**:
|
||||
- Add the `llms-full.txt` as custom documentation. The IDE will automatically chunk and index the content, implementing Retrieval-Augmented Generation (RAG).
|
||||
|
||||
2. **Without IDE support**:
|
||||
- Use a chat model with a large context window.
|
||||
- Implement a RAG strategy to manage and query the documentation efficiently.
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
# LangGraph
|
||||
|
||||
## Tutorials
|
||||
|
||||
[Learn the basics](https://langchain-ai.github.io/langgraph/tutorials/introduction/): LLM should read this page when needing to build a LangGraph chatbot or when learning about chat agents with memory, human-in-the-loop functionality, and state management. This page provides a comprehensive LangGraph quickstart tutorial covering building a support chatbot with web search capability, conversation memory, human review routing, custom state management, and time travel functionality to explore alternative conversation paths.
|
||||
|
||||
[Local Deploy](https://langchain-ai.github.io/langgraph/tutorials/langgraph-platform/local-server/): LLM should read this page when setting up a LangGraph app locally using `langgraph dev` and troubleshooting LangGraph server deployment. This page contains a quickstart guide for launching a LangGraph server locally, including installation steps, app creation from templates, environment setup, API testing with Python/JS SDKs, and links to deployment options and further documentation.
|
||||
|
||||
[Workflows and Agents](https://langchain-ai.github.io/langgraph/tutorials/workflows/): LLM should read this page when implementing agent systems, designing workflow architectures, or troubleshooting LLM orchestration strategies. The page covers patterns for LLM system design, comparing workflows (predefined paths) vs agents (dynamic control), with implementations of prompt chaining, parallelization, routing, orchestrator-worker, evaluator-optimizer, and agent patterns using both graph and functional APIs in LangGraph.
|
||||
|
||||
## Concepts
|
||||
|
||||
[Concepts](https://langchain-ai.github.io/langgraph/concepts/): LLM should read this page when needing to understand LangGraph's key concepts or when planning to deploy LangGraph applications. Comprehensive guide covering LangGraph fundamentals (graph primitives, agents, multi-agent systems, breakpoints, persistence), features (time travel, memory, streaming), and LangGraph Platform deployment options (self-hosted, cloud, enterprise).
|
||||
|
||||
[Agent architectures](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/): LLM should read this page when designing agent architectures, implementing control flows for LLM applications, or customizing agent behavior patterns. This page covers different LLM agent architectures including routers, tool calling agents (ReAct), structured outputs, memory systems, planning capabilities, and advanced customization options like human-in-the-loop, parallelization, subgraphs, and reflection mechanisms.
|
||||
|
||||
[Application Structure](https://langchain-ai.github.io/langgraph/concepts/application_structure/): LLM should read this page when needing to understand LangGraph application structure, preparing to deploy a LangGraph application, or troubleshooting configuration issues. This page details the structure of LangGraph applications, including required components (graphs, langgraph.json config file, dependency files, optional .env), file organization patterns for Python/JavaScript projects, configuration file format with all supported fields, and how to specify dependencies, graphs, and environment variables.
|
||||
|
||||
[Assistants](https://langchain-ai.github.io/langgraph/concepts/assistants/): LLM should read this page when looking for information about LangGraph assistants, understanding assistant configuration in LangGraph Platform, or learning about versioning agent configurations. This page explains LangGraph assistants, which allow developers to modify agent configurations (prompts, models, etc.) without changing graph logic, supports versioning for tracking changes, and is available only in LangGraph Platform (not open source).
|
||||
|
||||
[Authentication & Access Control](https://langchain-ai.github.io/langgraph/concepts/auth/): LLM should read this page when implementing authentication in LangGraph Platform, designing access control for LangGraph applications, or troubleshooting security issues in LangGraph deployments. This page explains LangGraph's authentication and authorization system, covering the difference between authentication and authorization, system architecture, implementing custom auth handlers, common access patterns, and supported resources/actions for access control.
|
||||
|
||||
[Bring Your Own Cloud (BYOC)](https://langchain-ai.github.io/langgraph/concepts/bring_your_own_cloud/): LLM should read this page when learning about LangGraph Platform deployment options, understanding Bring Your Own Cloud architecture, or managing deployments in AWS. This page explains LangGraph's BYOC deployment model, detailing how it separates control plane (managed by LangChain) from data plane (in customer's AWS account), outlines AWS requirements, infrastructure setup via Terraform, required permissions, and explains the deployment workflow.
|
||||
|
||||
[Deployment Options](https://langchain-ai.github.io/langgraph/concepts/deployment_options/): LLM should read this page when needing information about LangGraph deployment options, comparing different deployment methods, or understanding LangGraph Platform plans. This page outlines four deployment options for LangGraph Platform: Self-Hosted Lite (available for all plans), Self-Hosted Enterprise (Enterprise plan only), Cloud SaaS (Plus and Enterprise plans), and Bring Your Own Cloud (Enterprise plan only, AWS-only).
|
||||
|
||||
[Double Texting](https://langchain-ai.github.io/langgraph/concepts/double_texting/): LLM should read this page when handling concurrent user interactions in LangGraph Platform, implementing double-texting safeguards, or designing stateful conversation systems. This page explains four approaches to handling "double texting" in LangGraph (when users send a second message before the first completes): Reject, Enqueue, Interrupt, and Rollback, noting these features are currently only available in LangGraph Platform.
|
||||
|
||||
[Durable Execution](https://langchain-ai.github.io/langgraph/concepts/durable_execution/): LLM should read this page when needing to understand durable execution in LangGraph, implementing workflow persistence, or troubleshooting workflow resumption. This page explains durable execution in LangGraph: how workflows save progress to resume later, requirements (checkpointers and thread IDs), determinism guidelines for consistent replay, using tasks to encapsulate non-deterministic operations, and approaches for pausing/resuming workflows.
|
||||
|
||||
[FAQ](https://langchain-ai.github.io/langgraph/concepts/faq/): LLM should read this page when needing to understand differences between LangGraph and LangChain, exploring deployment options for LangGraph Platform, or determining compatibility with various LLMs. FAQ covering LangGraph basics, comparisons with other frameworks, deployment options (free self-hosted, Cloud SaaS, BYOC, Enterprise), compatibility with different LLMs including OSS models, and feature differences between open-source LangGraph and proprietary LangGraph Platform.
|
||||
|
||||
[Functional API](https://langchain-ai.github.io/langgraph/concepts/functional_api/): LLM should read this page when implementing workflows with persistent state, adding human-in-the-loop features, or converting existing code to use LangGraph. The page documents LangGraph's Functional API, which allows adding persistence, memory, and human-in-the-loop capabilities with minimal code changes using @entrypoint and @task decorators, handling serialization requirements, state management, and common patterns for parallel execution and error handling.
|
||||
|
||||
[Why LangGraph?](https://langchain-ai.github.io/langgraph/concepts/high_level/): LLM should read this page when understanding LangGraph's core capabilities, exploring LLM application infrastructure, or evaluating agent/workflow persistence options. LangGraph provides infrastructure for LLM applications with three key benefits: persistence for memory and human-in-the-loop capabilities, streaming of workflow events and LLM outputs, and tools for debugging and deployment via LangGraph Platform.
|
||||
|
||||
[Human-in-the-loop](https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/): LLM should read this page when implementing human-in-the-loop workflows in LangGraph, designing approval systems with LLMs, or creating interactive multi-turn conversation agents. This page explains human-in-the-loop patterns in LangGraph using the interrupt function, showing how to pause graph execution for human review/input and resume with Command. Includes design patterns for approval workflows, state editing, tool call reviews, and multi-turn conversations, with code examples and warnings about execution flow and common pitfalls.
|
||||
|
||||
[LangGraph CLI](https://langchain-ai.github.io/langgraph/concepts/langgraph_cli/): LLM should read this page when looking for information about LangGraph CLI installation or when needing to deploy a LangGraph API server locally. The page covers LangGraph CLI installation methods (Homebrew, pip), key commands (build, dev, up, dockerfile), and features like hot reloading, debugger support, and database management for running LangGraph servers.
|
||||
|
||||
[Cloud SaaS](https://langchain-ai.github.io/langgraph/concepts/langgraph_cloud/): LLM should read this page when learning about LangGraph's Cloud SaaS offering, understanding deployment options for LangGraph Servers, or planning autoscaling infrastructure for LangGraph applications. This page describes LangGraph Cloud SaaS, a managed deployment service for LangGraph Servers with details on deployment types (Development/Production), revisions, persistence, autoscaling capabilities (up to 10 containers), LangSmith integration, IP whitelisting, and automatic deletion policies after 28 days of non-use.
|
||||
|
||||
[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/): LLM should read this page when seeking information about LangGraph Platform's components or evaluating production deployment options for agentic applications. The page details the LangGraph Platform, a commercial solution for deploying agentic applications, including its components (Server, Studio, CLI, SDK, Remote Graph) and key benefits like streaming support, background runs, long run handling, burstiness management, and human-in-the-loop capabilities.
|
||||
|
||||
[LangGraph Server](https://langchain-ai.github.io/langgraph/concepts/langgraph_server/): LLM should read this page when developing applications with LangGraph Server, deploying agent-based applications, or integrating persistent state management in agent workflows. LangGraph Server provides an API for creating and managing agent applications with key features like streaming endpoints, background runs, task queues, persistence, webhooks, cron jobs, and monitoring capabilities through a structured system of assistants, threads, runs, and stores.
|
||||
|
||||
[LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/): LLM should read this page when looking for information about LangGraph Studio features, needing to troubleshoot LangGraph Studio issues, or learning how to connect a LangGraph application to the Studio. LangGraph Studio is a specialized agent IDE for visualizing, interacting with, and debugging LLM applications, offering features such as graph visualization, state editing, assistant management, and integration with LangSmith, with instructions for connecting via deployed applications or local development servers, plus troubleshooting FAQs.
|
||||
|
||||
[LangGraph Glossary](https://langchain-ai.github.io/langgraph/concepts/low_level/): LLM should read this page when needing to understand LangGraph terminology, implementing agent workflows as graphs, or developing modular multi-step AI systems. The page covers core LangGraph concepts including StateGraph, nodes, edges, state management, messaging, persistence, configuration, human-in-the-loop features, subgraphs, and visualization capabilities.
|
||||
|
||||
[Memory](https://langchain-ai.github.io/langgraph/concepts/memory/): LLM should read this page when implementing memory systems for AI agents, managing conversation context across sessions, or designing systems that require both short-term and long-term information retention. This page explains memory systems in LangGraph, covering short-term (thread-scoped) memory for managing conversation history and long-term memory across threads, with techniques for handling long conversations, summarizing past interactions, and organizing persistent memories in namespaces.
|
||||
|
||||
[Multi-agent Systems](https://langchain-ai.github.io/langgraph/concepts/multi_agent/): LLM should read this page when implementing multi-agent systems, troubleshooting complex agent architectures, or designing agent communication patterns. Multi-agent systems organize LLMs into modular architectures (network, supervisor, hierarchical, custom) with different communication patterns, using Command objects for handoffs between agents, and supporting various state management approaches.
|
||||
|
||||
[Persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/): LLM should read this page when needing to understand LangGraph persistence mechanisms, implementing stateful workflows, or managing conversation history across interactions. This page covers LangGraph's persistence features including checkpointers, threads, state snapshots, replay functionality, forking state, cross-thread memory via InMemoryStore, and semantic search capabilities for stored memories.
|
||||
|
||||
[LangGraph Platform Plans](https://langchain-ai.github.io/langgraph/concepts/plans/): LLM should read this page when determining LangGraph Platform pricing tiers, comparing deployment options, or researching features available across different plans. This page outlines LangGraph Platform plans (Developer, Plus, Enterprise), detailing deployment options, usage limitations, feature availability, and pricing structure for agentic application deployment.
|
||||
|
||||
[LangGraph Platform Architecture](https://langchain-ai.github.io/langgraph/concepts/platform_architecture/): LLM should read this page when needing to understand LangGraph Platform's technical architecture or troubleshooting deployment issues. The page details how LangGraph Platform uses Postgres for persistent storage of user/run data and Redis for worker communication (run cancellation, output streaming) and ephemeral metadata storage (retry attempts).
|
||||
|
||||
[LangGraph's Runtime (Pregel)](https://langchain-ai.github.io/langgraph/concepts/pregel/): LLM should read this page when learning about LangGraph's runtime, implementing applications with Pregel directly, or understanding how LangGraph executes graph applications. Explains LangGraph's Pregel runtime which manages graph application execution through a three-phase process (Plan, Execution, Update), describes different channel types (LastValue, Topic, Context, BinaryOperatorAggregate), provides direct implementation examples, and contrasts the StateGraph API with the Functional API.
|
||||
|
||||
[LangGraph Platform: Scalability & Resilience](https://langchain-ai.github.io/langgraph/concepts/scalability_and_resilience/): LLM should read this page when needing to understand LangGraph Platform's scaling capabilities, designing high-availability LangGraph deployments, or troubleshooting resilience issues. This page details LangGraph Platform's horizontal scaling features including stateless server instances, queue worker scaling, resilience mechanisms for handling crashes, and database failover strategies in Postgres and Redis.
|
||||
|
||||
[LangGraph SDK](https://langchain-ai.github.io/langgraph/concepts/sdk/): LLM should read this page when looking for installation instructions for LangGraph SDK, needing to choose between sync and async Python clients, or requiring SDK API references. The page covers LangGraph SDK installation for Python and JS, provides API reference links, explains the difference between synchronous and asynchronous Python clients, and includes code examples for both client types.
|
||||
|
||||
[Self-Hosted](https://langchain-ai.github.io/langgraph/concepts/self_hosted/): LLM should read this page when looking for LangGraph deployment options, understanding self-hosted versions, or seeking requirements for self-hosting LangGraph. This page details two self-hosted deployment options for LangGraph Platform: Self-Hosted Lite (limited to 1M nodes/year) and Self-Hosted Enterprise (full version requiring license). Includes requirements, deployment process using Redis/Postgres, Docker, and optional Kubernetes deployment via Helm chart.
|
||||
|
||||
[Streaming](https://langchain-ai.github.io/langgraph/concepts/streaming/): LLM should read this page when implementing streaming features in LangGraph applications, understanding different streaming modes, or building responsive LLM applications. This page explains streaming in LangGraph, covering the main types (workflow progress, LLM tokens, custom updates) and streaming modes (values, updates, custom, messages, debug, events), with details on how to use multiple modes simultaneously and differences between LangGraph library and Platform implementations.
|
||||
|
||||
[Template Applications](https://langchain-ai.github.io/langgraph/concepts/template_applications/): LLM should read this page when looking for LangGraph template applications, setting up a new LangGraph project, or finding reference implementations for agentic workflows. This page presents LangGraph template applications with installation requirements, available templates (including ReAct Agent, Memory Agent, Retrieval Agent, etc.), instructions for creating new apps using the CLI, deployment options, and links to further learning resources.
|
||||
|
||||
[Time Travel ⏱️](https://langchain-ai.github.io/langgraph/concepts/time-travel/): LLM should read this page when debugging LLM-based agent behavior, analyzing decision-making paths, or exploring alternative execution branches in LangGraph. This page explains LangGraph's Time Travel debugging features: Replaying (reproducing past actions up to specific checkpoints) and Forking (creating alternative execution paths from specific points), with code examples for retrieving checkpoints, configuring replay, and creating forked states.
|
||||
|
||||
## How Tos
|
||||
|
||||
[How-to Guides](https://langchain-ai.github.io/langgraph/how-tos/): LLM should read this page when looking for specific implementation techniques in LangGraph or when trying to deploy LangGraph applications to production environments. This page contains an extensive collection of how-to guides for LangGraph, covering graph fundamentals, persistence, memory management, human-in-the-loop features, tool calling, multi-agent systems, streaming, and deployment options through LangGraph Platform.
|
||||
|
||||
[How to implement handoffs between agents](https://langchain-ai.github.io/langgraph/how-tos/agent-handoffs/): LLM should read this page when implementing multi-agent systems that require agent coordination, when building systems with specialized agents that need to work together, or when needing to implement handoffs between agents. This page explains how to implement handoffs between agents in LangGraph using Command objects, both directly from agent nodes and through specialized handoff tools, with code examples for creating multi-agent systems.
|
||||
|
||||
[How to run a graph asynchronously](https://langchain-ai.github.io/langgraph/how-tos/async/): LLM should read this page when needing to implement asynchronous graph execution in LangGraph or when optimizing IO-bound LLM applications. This page explains how to convert synchronous graphs to asynchronous in LangGraph, including updating node definitions with async/await, using StateGraph with TypedDict, implementing conditional edges, and streaming results.
|
||||
|
||||
[How to integrate LangGraph with AutoGen, CrewAI, and other frameworks](https://langchain-ai.github.io/langgraph/how-tos/autogen-integration/): LLM should read this page when integrating LangGraph with other agent frameworks, building multi-agent systems, or adding persistence features to agents. The page demonstrates how to combine LangGraph with AutoGen by calling AutoGen agents inside LangGraph nodes, showing code examples for setting up the integration with memory and conversation persistence.
|
||||
|
||||
[How to integrate LangGraph (functional API) with AutoGen, CrewAI, and other frameworks](https://langchain-ai.github.io/langgraph/how-tos/autogen-integration-functional/): LLM should read this page when integrating LangGraph with other agent frameworks, building multi-agent systems with different frameworks, or adding LangGraph features to existing agent systems. This page demonstrates how to integrate LangGraph's functional API with AutoGen, including code examples for creating a workflow that calls AutoGen agents, leveraging LangGraph's memory and persistence features.
|
||||
|
||||
[How to create branches for parallel node execution](https://langchain-ai.github.io/langgraph/how-tos/branching/): LLM should read this page when needing to implement parallel node execution in LangGraph, optimizing graph performance, or handling conditional branching in workflows. This page explains how to create branches for parallel execution in LangGraph using fan-out/fan-in mechanisms, reducer functions for state accumulation, handling exceptions during parallel execution, and implementing conditional branching logic between nodes.
|
||||
|
||||
[How to combine control flow and state updates with Command](https://langchain-ai.github.io/langgraph/how-tos/command): LLM should read this page when learning how to combine control flow with state updates in LangGraph, understanding Command objects, or navigating between parent graphs and subgraphs. This page explains how to use Command objects to simultaneously update state and control flow between nodes, demonstrates using Command.PARENT to navigate from subgraphs to parent graphs, and includes examples of implementing reducers for state updates across graph hierarchies.
|
||||
|
||||
[How to add runtime configuration to your graph](https://langchain-ai.github.io/langgraph/how-tos/configuration/): LLM should read this page when implementing runtime configuration for LangGraph, adding model selection options to agents, or enabling dynamic system messages. This page demonstrates how to configure LangGraph at runtime, including selecting different LLMs dynamically and adding custom configuration options like system messages through the configurable dictionary.
|
||||
|
||||
[How to use the pre-built ReAct agent](https://langchain-ai.github.io/langgraph/how-tos/create-react-agent/): LLM should read this page when implementing a ReAct agent, needing pre-built agent solutions, or learning how to integrate tools with LLM agents. This page covers how to use the pre-built ReAct agent in LangGraph, including setup instructions, creating a weather checking tool, implementing the agent architecture, and examples of running the agent with and without tool calls.
|
||||
|
||||
[How to add human-in-the-loop processes to the prebuilt ReAct agent](https://langchain-ai.github.io/langgraph/how-tos/create-react-agent-hitl/): LLM should read this page when implementing human-in-the-loop processes for ReAct agents, debugging tool calls, or learning about interrupts in LangGraph. This guide demonstrates how to add human-in-the-loop functionality to prebuilt ReAct agents using interrupt_before=["tools"], working with MemorySaver checkpoints, and showing how to approve or edit tool calls before they execute.
|
||||
|
||||
[How to add thread-level memory to a ReAct Agent](https://langchain-ai.github.io/langgraph/how-tos/create-react-agent-memory/): LLM should read this page when adding memory to ReAct agents, implementing thread-level persistence in LangGraph, or building stateful conversational agents. This guide demonstrates how to add memory to a ReAct agent using LangGraph's checkpointer interface, with code examples showing MemorySaver implementation, thread_id configuration, and persistent chat context across multiple interactions.
|
||||
|
||||
[How to return structured output from the prebuilt ReAct agent](https://langchain-ai.github.io/langgraph/how-tos/create-react-agent-structured-output/): LLM should read this page when implementing structured output with ReAct agents, customizing agent response formats, or working with LangGraph agents. This page explains how to return structured output from prebuilt ReAct agents by providing a response_format parameter with a Pydantic schema, including examples with weather data and options for customizing the prompt.
|
||||
|
||||
[How to add a custom system prompt to the prebuilt ReAct agent](https://langchain-ai.github.io/langgraph/how-tos/create-react-agent-system-prompt/): LLM should read this page when learning to customize ReAct agents, needing to add system prompts to agents, or working with LangGraph's prebuilt agents. This tutorial demonstrates how to add a custom system prompt to a prebuilt ReAct agent, with code examples showing model setup, tool creation, and using the prompt parameter in the create_react_agent function.
|
||||
|
||||
[How to add cross-thread persistence to your graph](https://langchain-ai.github.io/langgraph/how-tos/cross-thread-persistence): LLM should read this page when needing to implement persistence across multiple threads in LangGraph, when storing user data between conversations, or when implementing shared memory in graph-based LLM applications. This page demonstrates how to use LangGraph's Store API to persist data across threads, including creating an InMemoryStore with embedding search capabilities, passing stores to graph nodes, and accessing user-specific memories in different conversation threads.
|
||||
|
||||
[How to add cross-thread persistence (functional API)](https://langchain-ai.github.io/langgraph/how-tos/cross-thread-persistence-functional): LLM should read this page when needing to implement cross-thread persistence in LangGraph functional API, storing user data across different conversation threads, or creating shared memory between workflows. This page explains how to add cross-thread persistence to LangGraph using the Store interface, including defining a store, configuring the entrypoint decorator, and implementing a workflow that can store and retrieve user information across different conversation threads.
|
||||
|
||||
[How to do a Self-hosted deployment of LangGraph](https://langchain-ai.github.io/langgraph/how-tos/deploy-self-hosted/): LLM should read this page when implementing a self-hosted deployment of LangGraph, configuring required environment variables, or building Docker images for LangGraph applications. This page explains how to deploy LangGraph applications using Docker, covering environment requirements (Redis, Postgres), how to build Docker images with the LangGraph CLI, configuration using environment variables, and deployment options using Docker or Docker Compose.
|
||||
|
||||
[How to disable streaming for models that don't support it](https://langchain-ai.github.io/langgraph/how-tos/disable-streaming/): LLM should read this page when handling models that don't support streaming, implementing LangGraph with non-streaming models, or troubleshooting streaming errors with OpenAI's O1 models. This page explains how to use the disable_streaming=True parameter with ChatOpenAI to make non-streaming models work with LangGraph's astream_events API, with code examples showing the error case and proper implementation.
|
||||
|
||||
[How to edit graph state](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/edit-graph-state/): LLM should read this page when needing to implement human intervention in LangGraph workflows, wanting to edit graph state during execution, or implementing breakpoints in agent systems. This page explains how to edit graph state in LangGraph using breakpoints, including implementing human-in-the-loop interactions, setting up interruptions before specific nodes, and updating state during agent execution.
|
||||
|
||||
[How to Review Tool Calls](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/review-tool-calls/): LLM should read this page when implementing human review of tool calls, creating interactive agent workflows, or building approval systems for AI actions. This page explains how to implement human-in-the-loop review for tool calls in LangGraph, including approving tool calls, modifying tool calls manually, and providing natural language feedback to agents with complete code examples and explanations.
|
||||
|
||||
[How to view and update past graph state](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/time-travel/): LLM should read this page when needing to access or modify past states in LangGraph, when debugging agent execution, or when implementing user interventions in agent workflows. This page demonstrates how to view and update past graph states in LangGraph using get_state and update_state methods, with examples of replaying execution from checkpoints and branching workflows.
|
||||
|
||||
[How to wait for user input using interrupt](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/wait-user-input/): LLM should read this page when implementing wait-for-user functions in LangGraph, implementing human-in-the-loop interactions, or learning how to use the interrupt() function. This page explains how to pause graph execution to collect user input using LangGraph's interrupt() function, with examples of simple feedback collection and more complex agent interactions that ask clarifying questions.
|
||||
|
||||
[How to define input/output schema for your graph](https://langchain-ai.github.io/langgraph/how-tos/input_output_schema/): LLM should read this page when needing to define separate input/output schemas for LangGraph, implementing schema-based data filtering, or understanding schema definitions in StateGraph. This page explains how to define distinct input and output schemas for a StateGraph, showing how input schema validates the provided data structure while output schema filters internal data to return only relevant information, with code examples demonstrating implementation.
|
||||
|
||||
[How to handle large numbers of tools](https://langchain-ai.github.io/langgraph/how-tos/many-tools/): LLM should read this page when handling large tool collections, implementing dynamic tool selection, or creating retrieval-based tool management in LangGraph. This page demonstrates how to manage large numbers of tools by using vector search to dynamically select relevant tools based on user queries, implementing tool selection nodes in LangGraph, and handling tool selection errors with retry mechanisms.
|
||||
|
||||
[How to create map-reduce branches for parallel execution](https://langchain-ai.github.io/langgraph/how-tos/map-reduce/): LLM should read this page when learning to implement parallel execution in LangGraph, creating map-reduce operations, or handling dynamic task decomposition. This guide explains how to use LangGraph's Send API to create map-reduce workflows, breaking tasks into parallel sub-tasks and recombining results, with examples showing joke generation across multiple subjects.
|
||||
|
||||
[How to add summary of the conversation history](https://langchain-ai.github.io/langgraph/how-tos/memory/add-summary-conversation-history/): LLM should read this page when implementing conversation summarization, managing context windows, or building chatbots with memory management. This page demonstrates how to add summary functionality to conversation history using LangGraph, including checking conversation length, creating summaries, and removing old messages while maintaining context.
|
||||
|
||||
[How to delete messages](https://langchain-ai.github.io/langgraph/how-tos/memory/delete-messages): LLM should read this page when attempting to manage message history in LangGraph, needing to delete specific messages from conversational state, or implementing memory management in LLM applications. This page explains how to delete messages from a LangGraph application using RemoveMessage modifiers, covering both manual deletion with message IDs and programmatic deletion within graph logic to maintain conversation history limits.
|
||||
|
||||
[How to manage conversation history](https://langchain-ai.github.io/langgraph/how-tos/memory/manage-conversation-history/): LLM should read this page when managing conversation history in LangGraph, preventing context window issues, or implementing custom message filtering. This page explains how to manage conversation history in LangGraph to prevent context window overflow by implementing message filtering functions that control which messages are sent to the LLM.
|
||||
|
||||
[How to add semantic search to your agent's memory](https://langchain-ai.github.io/langgraph/how-tos/memory/semantic-search/): LLM should read this page when implementing semantic search in agent memory, enabling memory-aware AI assistants, or configuring advanced memory retrieval systems. This page demonstrates how to add semantic search to LangGraph agent memory stores, covering basic setup with embeddings, storing memories, searching by semantic similarity, integrating memory in agents and ReAct agents, and advanced usage like multi-vector indexing and selective memory indexing.
|
||||
|
||||
[How to add multi-turn conversation in a multi-agent application](https://langchain-ai.github.io/langgraph/how-tos/multi-agent-multi-turn-convo/): LLM should read this page when implementing multi-turn conversations between agents, creating interactive agent systems with human input, or learning about langgraph interrupts and agent handoffs. This page demonstrates how to build a multi-agent system with multi-turn conversations, including human-in-the-loop interactions, agent handoffs, and state management using LangGraph, Command objects, and interrupts.
|
||||
|
||||
[How to add multi-turn conversation in a multi-agent application (functional API)](https://langchain-ai.github.io/langgraph/how-tos/multi-agent-multi-turn-convo-functional/): LLM should read this page when building multi-turn conversational agents, implementing agent-to-agent handoffs, or using interrupts to collect user input in LangGraph. This guide demonstrates how to create a multi-agent system with multi-turn conversations using LangGraph's functional API, featuring agent handoffs, interrupt mechanics for user input, and a complete example of travel and hotel advisor agents that can transfer control between each other.
|
||||
|
||||
[How to build a multi-agent network](https://langchain-ai.github.io/langgraph/how-tos/multi-agent-network/): LLM should read this page when implementing multi-agent networks, setting up agent communication via handoffs, or building travel assistance agents. This page explains how to create a fully-connected multi-agent network with LangGraph where agents can communicate with each other via handoffs, including custom agent implementation and using prebuilt ReAct agents with tools.
|
||||
|
||||
[How to build a multi-agent network (functional API)](https://langchain-ai.github.io/langgraph/how-tos/multi-agent-network-functional/): LLM should read this page when building multi-agent systems, implementing agent handoffs between specialists, or creating fully-connected agent networks. This guide demonstrates how to create a multi-agent network using LangGraph's functional API, with tasks for individual agents and entrypoint functions to manage agent handoffs based on tool calls.
|
||||
|
||||
[How to add node retry policies](https://langchain-ai.github.io/langgraph/how-tos/node-retries/): LLM should read this page when implementing error handling in LangGraph nodes, configuring API retry mechanisms, or troubleshooting node failures in graph workflows. Shows how to add custom retry policies to LangGraph nodes, including specifying which exceptions to retry on, setting max attempts, intervals, backoff factors, and implementing different retry behaviors for different node types.
|
||||
|
||||
[How to pass config to tools](https://langchain-ai.github.io/langgraph/how-tos/pass-config-to-tools/): LLM should read this page when implementing secure tool configuration in LangChain, passing user-specific parameters to tools, or configuring tools with runtime values. This page explains how to pass configuration to LangChain tools using RunnableConfig, allowing application-controlled values (like user IDs) to be securely passed to tools without LLM control, with examples of implementing tools that access user-specific data.
|
||||
|
||||
[How to pass private state between nodes](https://langchain-ai.github.io/langgraph/how-tos/pass_private_state/): LLM should read this page when implementing data sharing between specific nodes in LangGraph, handling private state in graph workflows, or designing multi-node sequential processes with selective data visibility. This page demonstrates how to pass private data between specific nodes in a LangGraph without making it part of the main schema, using typed dictionaries to define both public and private states, and showing a three-node example where private data flows only between the first two nodes.
|
||||
|
||||
[How to add thread-level persistence to your graph](https://langchain-ai.github.io/langgraph/how-tos/persistence/): LLM should read this page when implementing persistence in LangGraph, needing to preserve context across user interactions, or learning about thread-level state management. This page explains how to add thread-level persistence to LangGraph applications using MemorySaver, including code examples for creating stateful conversations where context is maintained across multiple interactions.
|
||||
|
||||
[How to add thread-level persistence (functional API)](https://langchain-ai.github.io/langgraph/how-tos/persistence-functional/): LLM should read this page when implementing thread-level persistence in LangGraph, creating conversational agents with memory, or using functional API with state management. This page explains how to add thread-level persistence to LangGraph functional API workflows using checkpointers, including code examples for creating a simple chatbot with memory across conversation turns.
|
||||
|
||||
[How to use MongoDB checkpointer for persistence](https://langchain-ai.github.io/langgraph/how-tos/persistence_mongodb/): LLM should read this page when implementing persistence in LangGraph agents, setting up MongoDB for state checkpointing, or working with MongoDB connections in LangGraph applications. This page explains how to use the MongoDB checkpointer for LangGraph persistence, covering connection methods (direct, client-based, async), basic setup requirements, and practical examples of saving and retrieving agent state between interactions.
|
||||
|
||||
[How to use Postgres checkpointer for persistence](https://langchain-ai.github.io/langgraph/how-tos/persistence_postgres/): LLM should read this page when setting up persistence for LangGraph agents, implementing PostgreSQL as a checkpoint storage backend, or working with either synchronous or asynchronous database connections. This page details how to use PostgreSQL for persisting LangGraph agent state, covering setup and configuration of PostgresSaver and AsyncPostgresSaver with different connection methods (pool, direct connection, connection string).
|
||||
|
||||
[How to create a custom checkpointer using Redis](https://langchain-ai.github.io/langgraph/how-tos/persistence_redis/): LLM should read this page when implementing persistence in LangGraph applications, creating custom checkpoint mechanisms for agents, or working with Redis as a storage backend. This page demonstrates how to create custom checkpointers for LangGraph agents using Redis, including implementations for both synchronous and asynchronous interfaces that save and retrieve agent state.
|
||||
|
||||
[How to create a ReAct agent from scratch](https://langchain-ai.github.io/langgraph/how-tos/react-agent-from-scratch/): LLM should read this page when needing to create a custom ReAct agent, wanting more control than prebuilt agents, or implementing ReAct from scratch with LangGraph. This guide shows how to build a custom ReAct agent using LangGraph, covering state definition, model/tool setup, node/edge configuration, graph creation, and testing the implementation with a weather query example.
|
||||
|
||||
[How to create a ReAct agent from scratch (Functional API)](https://langchain-ai.github.io/langgraph/how-tos/react-agent-from-scratch-functional): LLM should read this page when creating a ReAct agent using LangGraph's Functional API, implementing tool-calling workflows, or building conversational agents with thread persistence. This page explains how to build a ReAct agent from scratch using LangGraph's Functional API, including model and tool setup, defining tasks for model/tool calling, creating an entrypoint for orchestration, and adding thread-level persistence for conversational experiences.
|
||||
|
||||
[How to force tool-calling agent to structure output](https://langchain-ai.github.io/langgraph/how-tos/react-agent-structured-output): LLM should read this page when needing to force tool-calling agents to produce structured output, implementing consistent output formats for downstream software, or choosing between single-LLM vs two-LLM structured output approaches. The page explains two methods for implementing structured output with tool-calling agents: binding output as a tool (single LLM approach) and using two LLMs with structured output conversion, with code examples for both approaches using LangGraph.
|
||||
|
||||
[How to create and control loops](https://langchain-ai.github.io/langgraph/how-tos/recursion-limit/): LLM should read this page when building loops in computational graphs, needing to implement termination conditions, or handling recursion limits in LangGraph. The page explains how to create graphs with loops using conditional edges for termination, set recursion limits, handle GraphRecursionError, and implement complex loops with branches.
|
||||
|
||||
[How to review tool calls (Functional API)](https://langchain-ai.github.io/langgraph/how-tos/review-tool-calls-functional/): LLM should read this page when implementing human review of tool calls, creating ReAct agents with Functional API, or adding human-in-the-loop workflows. This page demonstrates how to review tool calls before execution in a ReAct agent using LangGraph's Functional API, including accepting, revising, or generating custom tool messages with the interrupt function.
|
||||
|
||||
[How to pass custom run ID or set tags and metadata for graph runs in LangSmith](https://langchain-ai.github.io/langgraph/how-tos/run-id-langsmith/): LLM should read this page when needing to customize trace information in LangSmith for LangGraph runs or when debugging graph runs with custom identifiers. The page explains how to pass custom run_id, set tags, add metadata, and customize run names for LangGraph traces in LangSmith using RunnableConfig, with examples showing implementation with a ReAct agent.
|
||||
|
||||
[How to create a sequence of steps](https://langchain-ai.github.io/langgraph/how-tos/sequence/): LLM should read this page when implementing sequential workflows in LangGraph, creating multi-step processes in applications, or learning about state management in graph-based systems. This page explains how to create sequences in LangGraph, covering methods for building sequential graphs using .add_node/.add_edge or the shorthand .add_sequence, defining state with TypedDict, creating nodes as functions that update state, and compiling/invoking graphs with examples.
|
||||
|
||||
[How to use Pydantic model as graph state](https://langchain-ai.github.io/langgraph/how-tos/state-model): LLM should read this page when implementing Pydantic models for state validation in LangGraph, handling complex state schema definitions, or troubleshooting validation errors in graph nodes. This guide explains how to use Pydantic BaseModel as a state schema in LangGraph for runtime validation, covering basic implementation, limitations, validation behavior across multiple nodes, serialization patterns, type coercion, and working with message models.
|
||||
|
||||
[How to update graph state from nodes](https://langchain-ai.github.io/langgraph/how-tos/state-reducers/): LLM should read this page when needing to update state in LangGraph, designing graphs with nodes that modify state, or implementing reducers for state management. This page explains how to define state schemas in LangGraph using TypedDict, how nodes can update state, and how to use reducers to control state updates, with specific examples using message handling.
|
||||
|
||||
[How to stream](https://langchain-ai.github.io/langgraph/how-tos/streaming/): LLM should read this page when needing to implement streaming in LangGraph applications, understanding different streaming modes, or troubleshooting LLM response delivery. This page explains how to stream LLM outputs using LangGraph, covering different streaming modes (values, updates, custom, messages, debug), with code examples for each mode and how to combine multiple streaming modes.
|
||||
|
||||
[How to stream data from within a tool](https://langchain-ai.github.io/langgraph/how-tos/streaming-events-from-within-tools/): LLM should read this page when implementing streaming functionality in tools, integrating LLM outputs with custom data streams, or developing LangGraph applications with real-time feedback. This page explains how to stream data from within tools using LangGraph, covering custom data streaming with stream_mode="custom", LLM token streaming with stream_mode="messages", and implementation approaches both with and without LangChain.
|
||||
|
||||
[How to stream LLM tokens from specific nodes](https://langchain-ai.github.io/langgraph/how-tos/streaming-specific-nodes/): LLM should read this page when needing to filter token streaming from specific nodes in LangGraph, implementing selective streaming in multi-node workflows, or controlling which node outputs are displayed. Guide explains how to stream LLM tokens from specific nodes using stream_mode="messages" and filtering by the langgraph_node metadata field, with complete code examples for implementing this in StateGraph applications.
|
||||
|
||||
[How to stream from subgraphs](https://langchain-ai.github.io/langgraph/how-tos/streaming-subgraphs/): LLM should read this page when needing to stream outputs from subgraphs in LangGraph, implementing nested graph streaming, or debugging hierarchical graph execution. This page explains how to stream outputs from subgraphs in LangGraph by using the subgraphs=True parameter in the parent graph's stream() method, with a complete code example showing the difference between regular streaming and subgraph streaming.
|
||||
|
||||
[How to stream LLM tokens from your graph](https://langchain-ai.github.io/langgraph/how-tos/streaming-tokens): LLM should read this page when needing to stream LLM tokens from a LangGraph application, implementing custom token streaming, or filtering streamed outputs. This page explains how to stream individual LLM tokens from LangGraph nodes using graph.stream() with different stream_mode options, including examples with and without LangChain, async implementations, and how to filter streamed tokens using metadata.
|
||||
|
||||
[How to use subgraphs](https://langchain-ai.github.io/langgraph/how-tos/subgraph/): LLM should read this page when building complex systems with subgraphs, implementing multi-agent systems, or needing to share state between parent graphs and subgraphs. The page explains two methods for using subgraphs: adding compiled subgraphs when schemas share keys, and invoking subgraphs via node functions when schemas differ, with code examples for both approaches.
|
||||
|
||||
[How to add thread-level persistence to a subgraph](https://langchain-ai.github.io/langgraph/how-tos/subgraph-persistence/): LLM should read this page when implementing persistence in nested LangGraph architectures, adding thread-level storage to subgraphs, or debugging state propagation in LangGraph applications. This guide demonstrates how to add thread-level persistence to subgraphs by passing a checkpointer only to the parent graph during compilation, accessing persisted states from both parent and child graphs, and retrieving subgraph state using the proper configuration parameters.
|
||||
|
||||
[How to transform inputs and outputs of a subgraph](https://langchain-ai.github.io/langgraph/how-tos/subgraph-transform-state/): LLM should read this page when needing to work with nested subgraphs, transforming state between parent and child graphs, or integrating independent state components in LangGraph. This page demonstrates how to transform inputs and outputs between parent graphs and subgraphs with different state structures, showing implementation of three nested graphs (parent, child, grandchild) with separate state dictionaries and transformation functions.
|
||||
|
||||
[How to view and update state in subgraphs](https://langchain-ai.github.io/langgraph/how-tos/subgraphs-manage-state/): LLM should read this page when working with state management in nested subgraphs, implementing human-in-the-loop patterns, or debugging complex graph flows. This guide covers viewing and updating state in LangGraph subgraphs, including how to resume execution from breakpoints, modify subgraph state, act as specific nodes, and work with multi-level nested subgraphs.
|
||||
|
||||
[How to call tools using ToolNode](https://langchain-ai.github.io/langgraph/how-tos/tool-calling/): LLM should read this page when learning how to implement tool calling with LangGraph, when working with the ToolNode component, or when building ReAct agents. This page covers using LangGraph's ToolNode for tool calling, including setup, manual invocation, working with chat models, building a ReAct agent, handling single and parallel tool calls, and error handling.
|
||||
|
||||
[How to handle tool calling errors](https://langchain-ai.github.io/langgraph/how-tos/tool-calling-errors/): LLM should read this page when handling tool call errors, implementing error handling for LLM-tool interactions, or creating fallback strategies for failed tool calls. This page covers strategies for handling tool calling errors in LangGraph, including using the prebuilt ToolNode with built-in error handling, implementing custom error handling patterns, and fallback mechanisms with model upgrades when tools fail.
|
||||
|
||||
[How to update graph state from tools](https://langchain-ai.github.io/langgraph/how-tos/update-state-from-tools/): LLM should read this page when needing to update graph state from tools in LangGraph, implementing personalized responses based on tool updates, or using Command objects to modify state. This page details how to update graph state from tools using Command objects, creating personalized agents with state tracking, and implementing dynamic prompt construction based on updated state values.
|
||||
|
||||
[How to interact with the deployment using RemoteGraph](https://langchain-ai.github.io/langgraph/how-tos/use-remote-graph/): LLM should read this page when needing to interact with LangGraph Platform deployments remotely, when implementing RemoteGraph interfaces, or when using deployed graphs as subgraphs. This page explains how to use RemoteGraph to interact with LangGraph Platform deployments, covering initialization methods (URL-based or client-based), synchronous/asynchronous invocation, thread-level persistence, and using RemoteGraph as a subgraph in larger applications.
|
||||
|
||||
[How to visualize your graph](https://langchain-ai.github.io/langgraph/how-tos/visualization): LLM should read this page when needing to visualize LangGraph graphs, looking for graph visualization methods, or working with graph visualization in Python. Comprehensive guide for visualizing graphs in LangGraph with multiple methods: Mermaid syntax, Mermaid.ink API for PNG rendering, Pyppeteer-based visualization, and Graphviz, with customization options for colors, styles, and layout.
|
||||
|
||||
[How to wait for user input (Functional API)](https://langchain-ai.github.io/langgraph/how-tos/wait-user-input-functional/): LLM should read this page when implementing human-in-the-loop workflows, integrating user input into agent systems, or adding interruption capabilities to LangGraph applications. The page explains how to use the `interrupt()` function in LangGraph's Functional API to pause execution for human input, with examples for both simple workflows and ReAct agents, including code implementations with checkpointing.
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
"\n",
|
||||
"### Code solution\n",
|
||||
"\n",
|
||||
"First, we will try OpenAI and [Claude3](https://docs.anthropic.com/en/docs/about-claude/models) with function calling.\n",
|
||||
"First, we will try OpenAI and [Claude3](https://python.langchain.com/docs/integrations/providers/anthropic/) with function calling.\n",
|
||||
"\n",
|
||||
"We will create a `code_gen_chain` w/ either OpenAI or Claude and test them here."
|
||||
]
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
search:
|
||||
boost: 2
|
||||
---
|
||||
|
||||
# Deployment
|
||||
|
||||
Get started deploying your LangGraph applications locally or on the cloud with
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
"\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"\n",
|
||||
"from pydantic import BaseModel, Field\n",
|
||||
"from pydantic import BaseModel, Field, field_validator\n",
|
||||
"\n",
|
||||
"direct_gen_outline_prompt = ChatPromptTemplate.from_messages(\n",
|
||||
" [\n",
|
||||
@@ -336,6 +336,10 @@
|
||||
" description=\"Description of the editor's focus, concerns, and motives.\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" @field_validator(\"name\", mode=\"before\")\n",
|
||||
" def sanitize_name(cls, value: str) -> str:\n",
|
||||
" return value.replace(\" \", \"\").replace(\".\", \"\")\n",
|
||||
"\n",
|
||||
" @property\n",
|
||||
" def persona(self) -> str:\n",
|
||||
" return f\"Name: {self.name}\\nRole: {self.role}\\nAffiliation: {self.affiliation}\\nDescription: {self.description}\\n\"\n",
|
||||
@@ -362,9 +366,9 @@
|
||||
" ]\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"gen_perspectives_chain = gen_perspectives_prompt | ChatOpenAI(\n",
|
||||
" model=\"gpt-3.5-turbo\"\n",
|
||||
").with_structured_output(Perspectives)"
|
||||
"gen_perspectives_chain = gen_perspectives_prompt | fast_llm.with_structured_output(\n",
|
||||
" Perspectives, method=\"function_calling\"\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -451,7 +455,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"perspectives.dict()"
|
||||
"perspectives.model_dump()"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -559,7 +563,7 @@
|
||||
" converted = []\n",
|
||||
" for message in state[\"messages\"]:\n",
|
||||
" if isinstance(message, AIMessage) and message.name != name:\n",
|
||||
" message = HumanMessage(**message.dict(exclude={\"type\"}))\n",
|
||||
" message = HumanMessage(**message.model_dump(exclude={\"type\"}))\n",
|
||||
" converted.append(message)\n",
|
||||
" return {\"messages\": converted}\n",
|
||||
"\n",
|
||||
@@ -637,9 +641,9 @@
|
||||
" MessagesPlaceholder(variable_name=\"messages\", optional=True),\n",
|
||||
" ]\n",
|
||||
")\n",
|
||||
"gen_queries_chain = gen_queries_prompt | ChatOpenAI(\n",
|
||||
" model=\"gpt-3.5-turbo\"\n",
|
||||
").with_structured_output(Queries, include_raw=True)"
|
||||
"gen_queries_chain = gen_queries_prompt | fast_llm.with_structured_output(\n",
|
||||
" Queries, include_raw=True, method=\"function_calling\"\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1695,6 +1699,13 @@
|
||||
"# We will down-header the sections to create less confusion in this notebook\n",
|
||||
"Markdown(article.replace(\"\\n#\", \"\\n##\"))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Workflows and Agents
|
||||
|
||||
This guide reviews common patterns for agentic systems. In describing these systems, it can be useful to make a distinction between "workflows" and "agents". One way to think about this difference is nicely explained [here](https://www.anthropic.com/research/building-effective-agents) by Anthropic:
|
||||
This guide reviews common patterns for agentic systems. In describing these systems, it can be useful to make a distinction between "workflows" and "agents". One way to think about this difference is nicely explained in [Anthropic's](https://python.langchain.com/docs/integrations/providers/anthropic/) `Building Effective Agents` blog post:
|
||||
|
||||
> Workflows are systems where LLMs and tools are orchestrated through predefined code paths.
|
||||
> Agents, on the other hand, are systems where LLMs dynamically direct their own processes and tool usage, maintaining control over how they accomplish tasks.
|
||||
@@ -9,7 +9,7 @@ Here is a simple way to visualize these differences:
|
||||
|
||||

|
||||
|
||||
When building agents and workflows, LangGraph [offers a number of benefits](https://langchain-ai.github.io/langgraph/concepts/high_level/) including persistence, streaming, and support for debugging as well as deployment.
|
||||
When building agents and workflows, LangGraph offers a number of benefits including persistence, streaming, and support for debugging as well as deployment.
|
||||
|
||||
## Set up
|
||||
|
||||
@@ -41,7 +41,7 @@ llm = ChatAnthropic(model="claude-3-5-sonnet-latest")
|
||||
|
||||
## Building Blocks: The Augmented LLM
|
||||
|
||||
LLM have [augmentations](https://www.anthropic.com/research/building-effective-agents) that support building workflows and agents. These include [structured outputs](https://python.langchain.com/docs/concepts/structured_outputs/) and [tool calling](https://python.langchain.com/docs/concepts/tool_calling/), as shown in this image from the Anthropic [blog](https://www.anthropic.com/research/building-effective-agents):
|
||||
LLM have augmentations that support building workflows and agents. These include [structured outputs](https://python.langchain.com/docs/concepts/structured_outputs/) and [tool calling](https://python.langchain.com/docs/concepts/tool_calling/), as shown in this image from the Anthropic blog on `Building Effective Agents`:
|
||||
|
||||

|
||||
|
||||
@@ -81,7 +81,7 @@ msg.tool_calls
|
||||
|
||||
In prompt chaining, each LLM call processes the output of the previous one.
|
||||
|
||||
As noted in the [Anthropic blog](https://www.anthropic.com/research/building-effective-agents):
|
||||
As noted in the Anthropic blog on `Building Effective Agents`:
|
||||
|
||||
> Prompt chaining decomposes a task into a sequence of steps, where each LLM call processes the output of the previous one. You can add programmatic checks (see "gate” in the diagram below) on any intermediate steps to ensure that the process is still on track.
|
||||
|
||||
@@ -392,7 +392,7 @@ With parallelization, LLMs work simultaneously on a task:
|
||||
|
||||
## Routing
|
||||
|
||||
Routing classifies an input and directs it to a followup task. As noted in the [Anthropic blog](https://www.anthropic.com/research/building-effective-agents):
|
||||
Routing classifies an input and directs it to a followup task. As noted in the Anthropic blog on `Building Effective Agents`:
|
||||
|
||||
> Routing classifies an input and directs it to a specialized followup task. This workflow allows for separation of concerns, and building more specialized prompts. Without this workflow, optimizing for one kind of input can hurt performance on other inputs.
|
||||
|
||||
@@ -603,7 +603,7 @@ Routing classifies an input and directs it to a followup task. As noted in the [
|
||||
|
||||
## Orchestrator-Worker
|
||||
|
||||
With orchestrator-worker, an orchestrator breaks down a task and delegates each sub-task to workers. As noted in the [Anthropic blog](https://www.anthropic.com/research/building-effective-agents):
|
||||
With orchestrator-worker, an orchestrator breaks down a task and delegates each sub-task to workers. As noted in the Anthropic blog on `Building Effective Agents`:
|
||||
|
||||
> In the orchestrator-workers workflow, a central LLM dynamically breaks down tasks, delegates them to worker LLMs, and synthesizes their results.
|
||||
|
||||
@@ -948,7 +948,7 @@ In the evaluator-optimizer workflow, one LLM call generates a response while ano
|
||||
|
||||
**Examples**
|
||||
|
||||
[Here](https://github.com/langchain-ai/research-rabbit) is an assistant that uses evaluator-optimizer to improve a report. See our video [here](https://www.youtube.com/watch?v=XGuTzHoqlj8).
|
||||
[Here](https://github.com/langchain-ai/local-deep-researcher) is an assistant that uses evaluator-optimizer to improve a report. See our video [here](https://www.youtube.com/watch?v=XGuTzHoqlj8).
|
||||
|
||||
[Here](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_adaptive_rag_local/) is a RAG workflow that grades answers for hallucinations or errors. See our video [here](https://www.youtube.com/watch?v=bq1Plo2RhYI).
|
||||
|
||||
@@ -1012,7 +1012,7 @@ In the evaluator-optimizer workflow, one LLM call generates a response while ano
|
||||
|
||||
## Agent
|
||||
|
||||
Agents are typically implemented as an LLM performing actions (via tool-calling) based on environmental feedback in a loop. As noted in the [Anthropic blog](https://www.anthropic.com/research/building-effective-agents):
|
||||
Agents are typically implemented as an LLM performing actions (via tool-calling) based on environmental feedback in a loop. As noted in the Anthropic blog on `Building Effective Agents`:
|
||||
|
||||
> Agents can handle sophisticated tasks, but their implementation is often straightforward. They are typically just LLMs using tools based on environmental feedback in a loop. It is therefore crucial to design toolsets and their documentation clearly and thoughtfully.
|
||||
|
||||
|
||||
+4
-2
@@ -54,7 +54,7 @@ theme:
|
||||
code: "Roboto Mono"
|
||||
plugins:
|
||||
- search:
|
||||
separator: '[\s\u200b\-_,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])'
|
||||
separator: '[\s\u200b\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;'
|
||||
- autorefs
|
||||
- mkdocstrings:
|
||||
handlers:
|
||||
@@ -85,7 +85,7 @@ plugins:
|
||||
|
||||
nav:
|
||||
- Home:
|
||||
- Introduction: index.md
|
||||
- index.md
|
||||
- Get started:
|
||||
- Learn the basics: tutorials/introduction.ipynb
|
||||
- Deployment:
|
||||
@@ -231,6 +231,7 @@ nav:
|
||||
- cloud/how-tos/stream_debug.md
|
||||
- cloud/how-tos/stream_multiple.md
|
||||
- cloud/how-tos/use_stream_react.md
|
||||
- cloud/how-tos/generative_ui_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
|
||||
@@ -360,6 +361,7 @@ nav:
|
||||
# NOTE: prebuilt.md is auto-generated by `make build-prebuilt`
|
||||
- Prebuilt Agents: prebuilt.md
|
||||
- Companies using LangGraph: adopters.md
|
||||
- LLMS-txt: llms-txt-overview.md
|
||||
- FAQ: concepts/faq.md
|
||||
- Troubleshooting:
|
||||
- Troubleshooting: troubleshooting/errors/index.md
|
||||
|
||||
Generated
+1512
-1955
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ readme = "README.md"
|
||||
python = "^3.10"
|
||||
aiohappyeyeballs = "2.4.3"
|
||||
hub = "^3.0.1"
|
||||
xxhash = "^3.5.0"
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
langgraph = { path = "../libs/langgraph/", develop = true }
|
||||
|
||||
@@ -25,7 +25,7 @@ with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
|
||||
# call .setup() the first time you're using the checkpointer
|
||||
checkpointer.setup()
|
||||
checkpoint = {
|
||||
"v": 1,
|
||||
"v": 2,
|
||||
"ts": "2024-07-31T20:14:19.804150+00:00",
|
||||
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
|
||||
"channel_values": {
|
||||
@@ -67,7 +67,7 @@ from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
||||
|
||||
async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
|
||||
checkpoint = {
|
||||
"v": 1,
|
||||
"v": 2,
|
||||
"ts": "2024-07-31T20:14:19.804150+00:00",
|
||||
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
|
||||
"channel_values": {
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import logging
|
||||
from collections.abc import AsyncIterator, Iterable, Sequence
|
||||
from contextlib import asynccontextmanager
|
||||
from types import TracebackType
|
||||
from typing import Any, Callable, Optional, Union, cast
|
||||
|
||||
import orjson
|
||||
@@ -20,11 +21,12 @@ from langgraph.store.base import (
|
||||
)
|
||||
from langgraph.store.base.batch import AsyncBatchedBaseStore
|
||||
from langgraph.store.postgres.base import (
|
||||
_PLACEHOLDER,
|
||||
PLACEHOLDER,
|
||||
BasePostgresStore,
|
||||
PoolConfig,
|
||||
PostgresIndexConfig,
|
||||
Row,
|
||||
TTLConfig,
|
||||
_decode_ns_bytes,
|
||||
_ensure_index_config,
|
||||
_group_ops,
|
||||
@@ -76,7 +78,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
await store.aput(("docs",), "doc3", {"text": "Other guide"}, index=False) # don't index
|
||||
|
||||
# Search by similarity
|
||||
results = await store.asearch(("docs",), "programming guides", limit=2)
|
||||
results = await store.asearch(("docs",), query="programming guides", limit=2)
|
||||
```
|
||||
|
||||
Using connection pooling for better performance:
|
||||
@@ -106,6 +108,11 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
Semantic search is disabled by default. You can enable it by providing an `index` configuration
|
||||
when creating the store. Without this configuration, all `index` arguments passed to
|
||||
`put` or `aput` will have no effect.
|
||||
|
||||
Note:
|
||||
If you provide a TTL configuration, you must explicitly call `start_ttl_sweeper()` to begin
|
||||
the background task that removes expired items. Call `stop_ttl_sweeper()` to properly
|
||||
clean up resources when you're done with the store.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
@@ -115,7 +122,11 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
"supports_pipeline",
|
||||
"index_config",
|
||||
"embeddings",
|
||||
"ttl_config",
|
||||
"_ttl_sweeper_task",
|
||||
"_ttl_stop_event",
|
||||
)
|
||||
supports_ttl: bool = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -126,6 +137,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]
|
||||
] = None,
|
||||
index: Optional[PostgresIndexConfig] = None,
|
||||
ttl: Optional[TTLConfig] = None,
|
||||
) -> None:
|
||||
if isinstance(conn, AsyncConnectionPool) and pipe is not None:
|
||||
raise ValueError(
|
||||
@@ -141,10 +153,13 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
self.index_config = index
|
||||
if self.index_config:
|
||||
self.embeddings, self.index_config = _ensure_index_config(self.index_config)
|
||||
|
||||
else:
|
||||
self.embeddings = None
|
||||
|
||||
self.ttl_config = ttl
|
||||
self._ttl_sweeper_task: Optional[asyncio.Task[None]] = None
|
||||
self._ttl_stop_event = asyncio.Event()
|
||||
|
||||
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
|
||||
grouped_ops, num_ops = _group_ops(ops)
|
||||
results: list[Result] = [None] * num_ops
|
||||
@@ -167,6 +182,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
pipeline: bool = False,
|
||||
pool_config: Optional[PoolConfig] = None,
|
||||
index: Optional[PostgresIndexConfig] = None,
|
||||
ttl: Optional[TTLConfig] = None,
|
||||
) -> AsyncIterator["AsyncPostgresStore"]:
|
||||
"""Create a new AsyncPostgresStore instance from a connection string.
|
||||
|
||||
@@ -198,16 +214,16 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
**cast(dict, pc),
|
||||
),
|
||||
) as pool:
|
||||
yield cls(conn=pool, index=index)
|
||||
yield cls(conn=pool, index=index, ttl=ttl)
|
||||
else:
|
||||
async with await AsyncConnection.connect(
|
||||
conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
|
||||
) as conn:
|
||||
if pipeline:
|
||||
async with conn.pipeline() as pipe:
|
||||
yield cls(conn=conn, pipe=pipe, index=index)
|
||||
yield cls(conn=conn, pipe=pipe, index=index, ttl=ttl)
|
||||
else:
|
||||
yield cls(conn=conn, index=index)
|
||||
yield cls(conn=conn, index=index, ttl=ttl)
|
||||
|
||||
async def setup(self) -> None:
|
||||
"""Set up the store database asynchronously.
|
||||
@@ -256,6 +272,119 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
"INSERT INTO vector_migrations (v) VALUES (%s)", (v,)
|
||||
)
|
||||
|
||||
async def sweep_ttl(self) -> int:
|
||||
"""Delete expired store items based on TTL.
|
||||
|
||||
Returns:
|
||||
int: The number of deleted items.
|
||||
"""
|
||||
async with self._cursor() as cur:
|
||||
await cur.execute(
|
||||
"""
|
||||
DELETE FROM store
|
||||
WHERE expires_at IS NOT NULL AND expires_at < NOW()
|
||||
"""
|
||||
)
|
||||
deleted_count = cur.rowcount
|
||||
return deleted_count
|
||||
|
||||
async def start_ttl_sweeper(
|
||||
self, sweep_interval_minutes: Optional[int] = None
|
||||
) -> asyncio.Task[None]:
|
||||
"""Periodically delete expired store items based on TTL.
|
||||
|
||||
Returns:
|
||||
Task that can be awaited or cancelled.
|
||||
"""
|
||||
if not self.ttl_config:
|
||||
return asyncio.create_task(asyncio.sleep(0))
|
||||
|
||||
if self._ttl_sweeper_task is not None and not self._ttl_sweeper_task.done():
|
||||
return self._ttl_sweeper_task
|
||||
|
||||
self._ttl_stop_event.clear()
|
||||
|
||||
interval = float(
|
||||
sweep_interval_minutes or self.ttl_config.get("sweep_interval_minutes") or 5
|
||||
)
|
||||
logger.info(f"Starting store TTL sweeper with interval {interval} minutes")
|
||||
|
||||
async def _sweep_loop() -> None:
|
||||
while not self._ttl_stop_event.is_set():
|
||||
try:
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
self._ttl_stop_event.wait(),
|
||||
timeout=interval * 60,
|
||||
)
|
||||
break
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
expired_items = await self.sweep_ttl()
|
||||
if expired_items > 0:
|
||||
logger.info(f"Store swept {expired_items} expired items")
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as exc:
|
||||
logger.exception("Store TTL sweep iteration failed", exc_info=exc)
|
||||
|
||||
task = asyncio.create_task(_sweep_loop())
|
||||
task.set_name("ttl_sweeper")
|
||||
self._ttl_sweeper_task = task
|
||||
return task
|
||||
|
||||
async def stop_ttl_sweeper(self, timeout: Optional[float] = None) -> bool:
|
||||
"""Stop the TTL sweeper task if it's running.
|
||||
|
||||
Args:
|
||||
timeout: Maximum time to wait for the task to stop, in seconds.
|
||||
If None, wait indefinitely.
|
||||
|
||||
Returns:
|
||||
bool: True if the task was successfully stopped or wasn't running,
|
||||
False if the timeout was reached before the task stopped.
|
||||
"""
|
||||
if self._ttl_sweeper_task is None or self._ttl_sweeper_task.done():
|
||||
return True
|
||||
|
||||
logger.info("Stopping TTL sweeper task")
|
||||
self._ttl_stop_event.set()
|
||||
|
||||
if timeout is not None:
|
||||
try:
|
||||
await asyncio.wait_for(self._ttl_sweeper_task, timeout=timeout)
|
||||
success = True
|
||||
except asyncio.TimeoutError:
|
||||
success = False
|
||||
else:
|
||||
await self._ttl_sweeper_task
|
||||
success = True
|
||||
|
||||
if success:
|
||||
self._ttl_sweeper_task = None
|
||||
logger.info("TTL sweeper task stopped")
|
||||
else:
|
||||
logger.warning("Timed out waiting for TTL sweeper task to stop")
|
||||
|
||||
return success
|
||||
|
||||
async def __aenter__(self) -> "AsyncPostgresStore":
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: Optional[type[BaseException]],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional["TracebackType"],
|
||||
) -> None:
|
||||
# Ensure the TTL sweeper task is stopped when exiting the context
|
||||
if hasattr(self, "_ttl_sweeper_task") and self._ttl_sweeper_task is not None:
|
||||
# Set the event to signal the task to stop
|
||||
self._ttl_stop_event.set()
|
||||
# We don't wait for the task to complete here to avoid blocking
|
||||
# The task will clean up itself gracefully
|
||||
|
||||
async def _execute_batch(
|
||||
self,
|
||||
grouped_ops: dict,
|
||||
@@ -360,7 +489,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
for (idx, _), vector in zip(embedding_requests, vectors):
|
||||
_paramslist = queries[idx][1]
|
||||
for i in range(len(_paramslist)):
|
||||
if _paramslist[i] is _PLACEHOLDER:
|
||||
if _paramslist[i] is PLACEHOLDER:
|
||||
_paramslist[i] = vector
|
||||
|
||||
for (idx, _), (query, params) in zip(search_ops, queries):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
@@ -39,6 +40,7 @@ from langgraph.store.base import (
|
||||
Result,
|
||||
SearchItem,
|
||||
SearchOp,
|
||||
TTLConfig,
|
||||
ensure_embeddings,
|
||||
get_text_at_path,
|
||||
tokenize_path,
|
||||
@@ -73,6 +75,17 @@ CREATE TABLE IF NOT EXISTS store (
|
||||
"""
|
||||
-- For faster lookups by prefix
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS store_prefix_idx ON store USING btree (prefix text_pattern_ops);
|
||||
""",
|
||||
"""
|
||||
-- Add expires_at column to store table
|
||||
ALTER TABLE store
|
||||
ADD COLUMN IF NOT EXISTS expires_at TIMESTAMP WITH TIME ZONE,
|
||||
ADD COLUMN IF NOT EXISTS ttl_minutes INT;
|
||||
""",
|
||||
"""
|
||||
-- Add indexes for efficient TTL sweeping
|
||||
CREATE INDEX IF NOT EXISTS idx_store_expires_at ON store (expires_at)
|
||||
WHERE expires_at IS NOT NULL;
|
||||
""",
|
||||
]
|
||||
|
||||
@@ -224,20 +237,55 @@ class BasePostgresStore(Generic[C]):
|
||||
self,
|
||||
get_ops: Sequence[tuple[int, GetOp]],
|
||||
) -> list[tuple[str, tuple, tuple[str, ...], list]]:
|
||||
"""
|
||||
Build queries to fetch (and optionally refresh the TTL of) multiple keys per namespace.
|
||||
|
||||
Each returned element is a tuple of:
|
||||
(sql_query_string, sql_params, namespace, items_for_this_namespace)
|
||||
|
||||
where items_for_this_namespace is the original list of (idx, key, refresh_ttl).
|
||||
"""
|
||||
|
||||
namespace_groups = defaultdict(list)
|
||||
refresh_ttls = defaultdict(list)
|
||||
for idx, op in get_ops:
|
||||
namespace_groups[op.namespace].append((idx, op.key))
|
||||
refresh_ttls[op.namespace].append(op.refresh_ttl)
|
||||
|
||||
results = []
|
||||
for namespace, items in namespace_groups.items():
|
||||
_, keys = zip(*items)
|
||||
keys_to_query = ",".join(["%s"] * len(keys))
|
||||
query = f"""
|
||||
SELECT key, value, created_at, updated_at
|
||||
FROM store
|
||||
WHERE prefix = %s AND key IN ({keys_to_query})
|
||||
this_refresh_ttls = refresh_ttls[namespace]
|
||||
|
||||
query = """
|
||||
WITH passed_in AS (
|
||||
SELECT unnest(%s::text[]) AS key,
|
||||
unnest(%s::bool[]) AS do_refresh
|
||||
),
|
||||
updated AS (
|
||||
UPDATE store s
|
||||
SET expires_at = NOW() + (s.ttl_minutes || ' minutes')::interval
|
||||
FROM passed_in p
|
||||
WHERE s.prefix = %s
|
||||
AND s.key = p.key
|
||||
AND p.do_refresh = TRUE
|
||||
AND s.ttl_minutes IS NOT NULL
|
||||
RETURNING s.key
|
||||
)
|
||||
SELECT s.key, s.value, s.created_at, s.updated_at
|
||||
FROM store s
|
||||
JOIN passed_in p ON s.key = p.key
|
||||
WHERE s.prefix = %s
|
||||
"""
|
||||
params = (_namespace_to_text(namespace), *keys)
|
||||
ns_text = _namespace_to_text(namespace)
|
||||
params = (
|
||||
list(keys), # -> unnest(%s::text[])
|
||||
list(this_refresh_ttls), # -> unnest(%s::bool[])
|
||||
ns_text, # -> prefix = %s (for UPDATE)
|
||||
ns_text, # -> prefix = %s (for final SELECT)
|
||||
)
|
||||
results.append((query, params, namespace, items))
|
||||
|
||||
return results
|
||||
|
||||
def _prepare_batch_PUT_queries(
|
||||
@@ -247,7 +295,6 @@ class BasePostgresStore(Generic[C]):
|
||||
list[tuple[str, Sequence]],
|
||||
Optional[tuple[str, Sequence[tuple[str, str, str, str]]]],
|
||||
]:
|
||||
# Last-write wins
|
||||
dedupped_ops: dict[tuple[tuple[str, ...], str], PutOp] = {}
|
||||
for _, op in put_ops:
|
||||
dedupped_ops[(op.namespace, op.key)] = op
|
||||
@@ -281,15 +328,26 @@ class BasePostgresStore(Generic[C]):
|
||||
insertion_params = []
|
||||
vector_values = []
|
||||
embedding_request_params = []
|
||||
# Handle TTL expiration
|
||||
|
||||
# First handle main store insertions
|
||||
for op in inserts:
|
||||
values.append("(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)")
|
||||
if op.ttl is not None:
|
||||
expires_at_str = f"NOW() + INTERVAL '{op.ttl*60} seconds'"
|
||||
ttl_minutes = op.ttl
|
||||
else:
|
||||
expires_at_str = "NULL"
|
||||
ttl_minutes = None
|
||||
|
||||
values.append(
|
||||
f"(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, {expires_at_str}, %s)"
|
||||
)
|
||||
insertion_params.extend(
|
||||
[
|
||||
_namespace_to_text(op.namespace),
|
||||
op.key,
|
||||
Jsonb(cast(dict, op.value)),
|
||||
ttl_minutes,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -303,7 +361,7 @@ class BasePostgresStore(Generic[C]):
|
||||
k = op.key
|
||||
|
||||
if op.index is None:
|
||||
paths = self.index_config["__tokenized_fields"]
|
||||
paths = cast(dict, self.index_config)["__tokenized_fields"]
|
||||
else:
|
||||
paths = [(ix, tokenize_path(ix)) for ix in op.index]
|
||||
|
||||
@@ -318,11 +376,13 @@ class BasePostgresStore(Generic[C]):
|
||||
|
||||
values_str = ",".join(values)
|
||||
query = f"""
|
||||
INSERT INTO store (prefix, key, value, created_at, updated_at)
|
||||
INSERT INTO store (prefix, key, value, created_at, updated_at, expires_at, ttl_minutes)
|
||||
VALUES {values_str}
|
||||
ON CONFLICT (prefix, key) DO UPDATE
|
||||
SET value = EXCLUDED.value,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
updated_at = CURRENT_TIMESTAMP,
|
||||
expires_at = EXCLUDED.expires_at,
|
||||
ttl_minutes = EXCLUDED.ttl_minutes
|
||||
"""
|
||||
queries.append((query, insertion_params))
|
||||
|
||||
@@ -346,117 +406,151 @@ class BasePostgresStore(Generic[C]):
|
||||
list[tuple[str, list[Union[None, str, list[float]]]]], # queries, params
|
||||
list[tuple[int, str]], # idx, query_text pairs to embed
|
||||
]:
|
||||
"""
|
||||
Build per-SearchOp SQL queries (with optional TTL refresh) plus embedding requests.
|
||||
Returns:
|
||||
- queries: list of (SQL, param_list)
|
||||
- embedding_requests: list of (original_index_in_search_ops, text_query)
|
||||
"""
|
||||
|
||||
queries = []
|
||||
embedding_requests = []
|
||||
|
||||
for idx, (_, op) in enumerate(search_ops):
|
||||
# Build filter conditions first
|
||||
filter_params = []
|
||||
filter_conditions = []
|
||||
filter_clauses = []
|
||||
if op.filter:
|
||||
for key, value in op.filter.items():
|
||||
if isinstance(value, dict):
|
||||
for op_name, val in value.items():
|
||||
condition, filter_params_ = self._get_filter_condition(
|
||||
condition, params_ = self._get_filter_condition(
|
||||
key, op_name, val
|
||||
)
|
||||
filter_conditions.append(condition)
|
||||
filter_params.extend(filter_params_)
|
||||
filter_clauses.append(condition)
|
||||
filter_params.extend(params_)
|
||||
else:
|
||||
filter_conditions.append("value->%s = %s::jsonb")
|
||||
filter_params.extend([key, json.dumps(value)])
|
||||
filter_clauses.append("value->%s = %s::jsonb")
|
||||
filter_params.extend([key, orjson.dumps(value).decode("utf-8")])
|
||||
|
||||
ns_condition = "TRUE"
|
||||
ns_param: Optional[Sequence[Union[str]]] = None
|
||||
if op.namespace_prefix:
|
||||
ns_condition = "store.prefix LIKE %s"
|
||||
ns_param = (f"{_namespace_to_text(op.namespace_prefix)}%",)
|
||||
else:
|
||||
ns_param = ()
|
||||
|
||||
extra_filters = (
|
||||
" AND " + " AND ".join(filter_clauses) if filter_clauses else ""
|
||||
)
|
||||
|
||||
# Vector search branch
|
||||
if op.query and self.index_config:
|
||||
# We'll embed the text later, so record the request.
|
||||
embedding_requests.append((idx, op.query))
|
||||
|
||||
score_operator, post_operator = _get_distance_operator(self)
|
||||
score_operator, post_operator = get_distance_operator(self)
|
||||
post_operator = post_operator.replace("scored", "uniq")
|
||||
vector_type = (
|
||||
cast(PostgresIndexConfig, self.index_config)
|
||||
.get("ann_index_config", {})
|
||||
.get("vector_type", "vector")
|
||||
)
|
||||
|
||||
# For hamming bit vectors, or “regular” vectors
|
||||
if (
|
||||
vector_type == "bit"
|
||||
and self.index_config.get("distance_type") == "hamming"
|
||||
and cast(dict, self.index_config).get("distance_type") == "hamming"
|
||||
):
|
||||
score_operator = score_operator % (
|
||||
"%s",
|
||||
self.index_config["dims"],
|
||||
cast(dict, self.index_config)["dims"],
|
||||
)
|
||||
else:
|
||||
score_operator = score_operator % (
|
||||
"%s",
|
||||
vector_type,
|
||||
)
|
||||
score_operator = score_operator % ("%s", vector_type)
|
||||
|
||||
vectors_per_doc_estimate = self.index_config["__estimated_num_vectors"]
|
||||
vectors_per_doc_estimate = cast(dict, self.index_config)[
|
||||
"__estimated_num_vectors"
|
||||
]
|
||||
expanded_limit = (op.limit * vectors_per_doc_estimate * 2) + 1
|
||||
|
||||
# Vector search with CTE for proper score handling
|
||||
filter_str = (
|
||||
""
|
||||
if not filter_conditions
|
||||
else " AND " + " AND ".join(filter_conditions)
|
||||
)
|
||||
if op.namespace_prefix:
|
||||
prefix_filter_str = f"WHERE s.prefix LIKE %s {filter_str} "
|
||||
ns_args: Sequence = (f"{_namespace_to_text(op.namespace_prefix)}%",)
|
||||
else:
|
||||
ns_args = ()
|
||||
if filter_str:
|
||||
prefix_filter_str = f"WHERE {filter_str} "
|
||||
else:
|
||||
prefix_filter_str = ""
|
||||
|
||||
base_query = f"""
|
||||
WITH scored AS (
|
||||
SELECT s.prefix, s.key, s.value, s.created_at, s.updated_at, {score_operator} AS neg_score
|
||||
FROM store s
|
||||
JOIN store_vectors sv ON s.prefix = sv.prefix AND s.key = sv.key
|
||||
{prefix_filter_str}
|
||||
ORDER BY {score_operator} ASC
|
||||
# “sub_scored” does the main vector search
|
||||
# Then we do DISTINCT ON to drop duplicates if your store can have them
|
||||
# Finally we limit & offset
|
||||
vector_search_cte = f"""
|
||||
SELECT store.prefix, store.key, store.value, store.created_at, store.updated_at,
|
||||
{score_operator} AS neg_score
|
||||
FROM store
|
||||
JOIN store_vectors sv ON store.prefix = sv.prefix AND store.key = sv.key
|
||||
WHERE {ns_condition} {extra_filters}
|
||||
ORDER BY {score_operator} ASC
|
||||
LIMIT %s
|
||||
)
|
||||
SELECT * FROM (
|
||||
SELECT DISTINCT ON (prefix, key)
|
||||
prefix, key, value, created_at, updated_at, {post_operator} as score
|
||||
FROM scored
|
||||
ORDER BY prefix, key, score DESC
|
||||
) AS unique_docs
|
||||
ORDER BY score DESC
|
||||
LIMIT %s
|
||||
OFFSET %s
|
||||
"""
|
||||
params = [
|
||||
_PLACEHOLDER, # Vector placeholder
|
||||
*ns_args,
|
||||
"""
|
||||
|
||||
search_results_sql = f"""
|
||||
WITH scored AS (
|
||||
{vector_search_cte}
|
||||
)
|
||||
SELECT uniq.prefix, uniq.key, uniq.value, uniq.created_at, uniq.updated_at,
|
||||
{post_operator} AS score
|
||||
FROM (
|
||||
SELECT DISTINCT ON (scored.prefix, scored.key)
|
||||
scored.prefix, scored.key, scored.value, scored.created_at, scored.updated_at, scored.neg_score
|
||||
FROM scored
|
||||
ORDER BY scored.prefix, scored.key, scored.neg_score ASC
|
||||
) uniq
|
||||
ORDER BY score DESC
|
||||
LIMIT %s
|
||||
OFFSET %s
|
||||
"""
|
||||
|
||||
search_results_params = [
|
||||
PLACEHOLDER,
|
||||
*ns_param,
|
||||
*filter_params,
|
||||
_PLACEHOLDER,
|
||||
PLACEHOLDER,
|
||||
expanded_limit,
|
||||
op.limit,
|
||||
op.offset,
|
||||
]
|
||||
|
||||
# Regular search branch
|
||||
else:
|
||||
base_query = """
|
||||
SELECT prefix, key, value, created_at, updated_at
|
||||
FROM store
|
||||
WHERE prefix LIKE %s
|
||||
"""
|
||||
params = [f"{_namespace_to_text(op.namespace_prefix)}%"]
|
||||
base_query = f"""
|
||||
SELECT store.prefix, store.key, store.value, store.created_at, store.updated_at, NULL AS score
|
||||
FROM store
|
||||
WHERE {ns_condition} {extra_filters}
|
||||
ORDER BY store.updated_at DESC
|
||||
LIMIT %s
|
||||
OFFSET %s
|
||||
"""
|
||||
search_results_sql = base_query
|
||||
search_results_params = [
|
||||
*ns_param,
|
||||
*filter_params,
|
||||
op.limit,
|
||||
op.offset,
|
||||
]
|
||||
|
||||
if filter_conditions:
|
||||
params.extend(filter_params)
|
||||
base_query += " AND " + " AND ".join(filter_conditions)
|
||||
|
||||
base_query += " ORDER BY updated_at DESC"
|
||||
base_query += " LIMIT %s OFFSET %s"
|
||||
params.extend([op.limit, op.offset])
|
||||
|
||||
queries.append((base_query, params))
|
||||
if op.refresh_ttl:
|
||||
# Wrap entire primary query in a CTE, then perform "update_at"
|
||||
final_sql = f"""
|
||||
WITH search_results AS (
|
||||
{search_results_sql}
|
||||
),
|
||||
updated AS (
|
||||
UPDATE store s
|
||||
SET expires_at = NOW() + (s.ttl_minutes || ' minutes')::interval
|
||||
FROM search_results sr
|
||||
WHERE s.prefix = sr.prefix
|
||||
AND s.key = sr.key
|
||||
AND s.ttl_minutes IS NOT NULL
|
||||
)
|
||||
SELECT sr.prefix, sr.key, sr.value, sr.created_at, sr.updated_at, sr.score
|
||||
FROM search_results sr
|
||||
"""
|
||||
final_params = search_results_params[:] # copy
|
||||
else:
|
||||
final_sql = search_results_sql
|
||||
final_params = search_results_params
|
||||
queries.append((final_sql, final_params))
|
||||
|
||||
return queries, embedding_requests
|
||||
|
||||
@@ -602,6 +696,11 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
||||
Make sure to call `setup()` before first use to create necessary tables and indexes.
|
||||
The pgvector extension must be available to use vector search.
|
||||
|
||||
Note:
|
||||
If you provide a TTL configuration, you must explicitly call `start_ttl_sweeper()` to begin
|
||||
the background thread that removes expired items. Call `stop_ttl_sweeper()` to properly
|
||||
clean up resources when you're done with the store.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
@@ -611,7 +710,10 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
||||
"supports_pipeline",
|
||||
"index_config",
|
||||
"embeddings",
|
||||
"_ttl_sweeper_thread",
|
||||
"_ttl_stop_event",
|
||||
)
|
||||
supports_ttl: bool = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -622,6 +724,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
||||
Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]
|
||||
] = None,
|
||||
index: Optional[PostgresIndexConfig] = None,
|
||||
ttl: Optional[TTLConfig] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._deserializer = deserializer
|
||||
@@ -634,6 +737,9 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
||||
self.embeddings, self.index_config = _ensure_index_config(self.index_config)
|
||||
else:
|
||||
self.embeddings = None
|
||||
self.ttl_config = ttl
|
||||
self._ttl_sweeper_thread: Optional[threading.Thread] = None
|
||||
self._ttl_stop_event = threading.Event()
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
@@ -644,6 +750,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
||||
pipeline: bool = False,
|
||||
pool_config: Optional[PoolConfig] = None,
|
||||
index: Optional[PostgresIndexConfig] = None,
|
||||
ttl: Optional[TTLConfig] = None,
|
||||
) -> Iterator["PostgresStore"]:
|
||||
"""Create a new PostgresStore instance from a connection string.
|
||||
|
||||
@@ -675,16 +782,123 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
||||
**cast(dict, pc),
|
||||
),
|
||||
) as pool:
|
||||
yield cls(conn=pool, index=index)
|
||||
yield cls(conn=pool, index=index, ttl=ttl)
|
||||
else:
|
||||
with Connection.connect(
|
||||
conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
|
||||
) as conn:
|
||||
if pipeline:
|
||||
with conn.pipeline() as pipe:
|
||||
yield cls(conn, pipe=pipe, index=index)
|
||||
yield cls(conn, pipe=pipe, index=index, ttl=ttl)
|
||||
else:
|
||||
yield cls(conn, index=index)
|
||||
yield cls(conn, index=index, ttl=ttl)
|
||||
|
||||
def sweep_ttl(self) -> int:
|
||||
"""Delete expired store items based on TTL.
|
||||
|
||||
Returns:
|
||||
int: The number of deleted items.
|
||||
"""
|
||||
with self._cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
DELETE FROM store
|
||||
WHERE expires_at IS NOT NULL AND expires_at < NOW()
|
||||
"""
|
||||
)
|
||||
deleted_count = cur.rowcount
|
||||
return deleted_count
|
||||
|
||||
def start_ttl_sweeper(
|
||||
self, sweep_interval_minutes: Optional[int] = None
|
||||
) -> concurrent.futures.Future[None]:
|
||||
"""Periodically delete expired store items based on TTL.
|
||||
|
||||
Returns:
|
||||
Future that can be waited on or cancelled.
|
||||
"""
|
||||
if not self.ttl_config:
|
||||
future: concurrent.futures.Future[None] = concurrent.futures.Future()
|
||||
future.set_result(None)
|
||||
return future
|
||||
|
||||
if self._ttl_sweeper_thread and self._ttl_sweeper_thread.is_alive():
|
||||
logger.info("TTL sweeper thread is already running")
|
||||
# Return a future that can be used to cancel the existing thread
|
||||
future = concurrent.futures.Future()
|
||||
future.add_done_callback(
|
||||
lambda f: self._ttl_stop_event.set() if f.cancelled() else None
|
||||
)
|
||||
return future
|
||||
|
||||
self._ttl_stop_event.clear()
|
||||
|
||||
interval = float(
|
||||
sweep_interval_minutes or self.ttl_config.get("sweep_interval_minutes") or 5
|
||||
)
|
||||
logger.info(f"Starting store TTL sweeper with interval {interval} minutes")
|
||||
|
||||
future = concurrent.futures.Future()
|
||||
|
||||
def _sweep_loop() -> None:
|
||||
try:
|
||||
while not self._ttl_stop_event.is_set():
|
||||
if self._ttl_stop_event.wait(interval * 60):
|
||||
break
|
||||
|
||||
try:
|
||||
expired_items = self.sweep_ttl()
|
||||
if expired_items > 0:
|
||||
logger.info(f"Store swept {expired_items} expired items")
|
||||
except Exception as exc:
|
||||
logger.exception(
|
||||
"Store TTL sweep iteration failed", exc_info=exc
|
||||
)
|
||||
future.set_result(None)
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
|
||||
thread = threading.Thread(target=_sweep_loop, daemon=True, name="ttl-sweeper")
|
||||
self._ttl_sweeper_thread = thread
|
||||
thread.start()
|
||||
|
||||
future.add_done_callback(
|
||||
lambda f: self._ttl_stop_event.set() if f.cancelled() else None
|
||||
)
|
||||
return future
|
||||
|
||||
def stop_ttl_sweeper(self, timeout: Optional[float] = None) -> bool:
|
||||
"""Stop the TTL sweeper thread if it's running.
|
||||
|
||||
Args:
|
||||
timeout: Maximum time to wait for the thread to stop, in seconds.
|
||||
If None, wait indefinitely.
|
||||
|
||||
Returns:
|
||||
bool: True if the thread was successfully stopped or wasn't running,
|
||||
False if the timeout was reached before the thread stopped.
|
||||
"""
|
||||
if not self._ttl_sweeper_thread or not self._ttl_sweeper_thread.is_alive():
|
||||
return True
|
||||
|
||||
logger.info("Stopping TTL sweeper thread")
|
||||
self._ttl_stop_event.set()
|
||||
|
||||
self._ttl_sweeper_thread.join(timeout)
|
||||
success = not self._ttl_sweeper_thread.is_alive()
|
||||
|
||||
if success:
|
||||
self._ttl_sweeper_thread = None
|
||||
logger.info("TTL sweeper thread stopped")
|
||||
else:
|
||||
logger.warning("Timed out waiting for TTL sweeper thread to stop")
|
||||
|
||||
return success
|
||||
|
||||
def __del__(self) -> None:
|
||||
"""Ensure the TTL sweeper thread is stopped when the object is garbage collected."""
|
||||
if hasattr(self, "_ttl_stop_event") and hasattr(self, "_ttl_sweeper_thread"):
|
||||
self.stop_ttl_sweeper(timeout=0.1)
|
||||
|
||||
@contextmanager
|
||||
def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor[DictRow]]:
|
||||
@@ -828,7 +1042,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
||||
for (idx, _), embedding in zip(embedding_requests, embeddings):
|
||||
_paramslist = queries[idx][1]
|
||||
for i in range(len(_paramslist)):
|
||||
if _paramslist[i] is _PLACEHOLDER:
|
||||
if _paramslist[i] is PLACEHOLDER:
|
||||
_paramslist[i] = embedding
|
||||
|
||||
for (idx, _), (query, params) in zip(search_ops, queries):
|
||||
@@ -883,8 +1097,14 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
||||
with self._cursor() as cur:
|
||||
version = _get_version(cur, table="store_migrations")
|
||||
for v, sql in enumerate(self.MIGRATIONS[version + 1 :], start=version + 1):
|
||||
cur.execute(sql)
|
||||
cur.execute("INSERT INTO store_migrations (v) VALUES (%s)", (v,))
|
||||
try:
|
||||
cur.execute(sql)
|
||||
cur.execute("INSERT INTO store_migrations (v) VALUES (%s)", (v,))
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to apply migration {v}.\nSql={sql}\nError={e}"
|
||||
)
|
||||
raise
|
||||
|
||||
if self.index_config:
|
||||
version = _get_version(cur, table="vector_migrations")
|
||||
@@ -1055,7 +1275,7 @@ def _decode_ns_bytes(namespace: Union[str, bytes, list]) -> tuple[str, ...]:
|
||||
return tuple(namespace.split("."))
|
||||
|
||||
|
||||
def _get_distance_operator(store: Any) -> tuple[str, str]:
|
||||
def get_distance_operator(store: Any) -> tuple[str, str]:
|
||||
"""Get the distance operator and score expression based on config."""
|
||||
# Note: Today, we are not using ANN indices due to restrictions
|
||||
# on PGVector's support for mixing vector and non-vector filters
|
||||
@@ -1121,4 +1341,4 @@ def _ensure_index_config(
|
||||
return embeddings, index_config
|
||||
|
||||
|
||||
_PLACEHOLDER = object()
|
||||
PLACEHOLDER = object()
|
||||
|
||||
Generated
+670
-506
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "2.0.15"
|
||||
version = "2.0.19"
|
||||
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.15"
|
||||
langgraph-checkpoint = "^2.0.21"
|
||||
orjson = ">=3.10.1"
|
||||
psycopg = "^3.2.0"
|
||||
psycopg-pool = "^3.2.0"
|
||||
|
||||
@@ -26,6 +26,9 @@ from tests.conftest import (
|
||||
CharacterEmbeddings,
|
||||
)
|
||||
|
||||
TTL_SECONDS = 6
|
||||
TTL_MINUTES = TTL_SECONDS / 60
|
||||
|
||||
|
||||
@pytest.fixture(scope="function", params=["default", "pipe", "pool"])
|
||||
async def store(request) -> AsyncIterator[AsyncPostgresStore]:
|
||||
@@ -42,28 +45,54 @@ async def store(request) -> AsyncIterator[AsyncPostgresStore]:
|
||||
|
||||
conn_string = f"{uri_base}/{database}{query_params}"
|
||||
admin_conn_string = DEFAULT_URI
|
||||
|
||||
ttl_config = {
|
||||
"default_ttl": TTL_MINUTES,
|
||||
"refresh_on_read": True,
|
||||
"sweep_interval_minutes": TTL_MINUTES / 2,
|
||||
}
|
||||
async with await AsyncConnection.connect(
|
||||
admin_conn_string, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
async with AsyncPostgresStore.from_conn_string(conn_string) as store:
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
conn_string, ttl=ttl_config
|
||||
) as store:
|
||||
store.MIGRATIONS = [
|
||||
(
|
||||
mig.replace("ttl_minutes INT;", "ttl_minutes FLOAT;")
|
||||
if isinstance(mig, str)
|
||||
else mig
|
||||
)
|
||||
for mig in store.MIGRATIONS
|
||||
]
|
||||
await store.setup()
|
||||
async with store._cursor() as cur:
|
||||
# drop the migration index
|
||||
await cur.execute("DROP TABLE IF EXISTS store_migrations")
|
||||
await store.setup() # Will fail if migrations aren't idempotent
|
||||
|
||||
if request.param == "pipe":
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
conn_string, pipeline=True
|
||||
conn_string, pipeline=True, ttl=ttl_config
|
||||
) as store:
|
||||
await store.start_ttl_sweeper()
|
||||
yield store
|
||||
await store.stop_ttl_sweeper()
|
||||
elif request.param == "pool":
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
conn_string, pool_config={"min_size": 1, "max_size": 10}
|
||||
conn_string, pool_config={"min_size": 1, "max_size": 10}, ttl=ttl_config
|
||||
) as store:
|
||||
await store.start_ttl_sweeper()
|
||||
yield store
|
||||
await store.stop_ttl_sweeper()
|
||||
else: # default
|
||||
async with AsyncPostgresStore.from_conn_string(conn_string) as store:
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
conn_string, ttl=ttl_config
|
||||
) as store:
|
||||
await store.start_ttl_sweeper()
|
||||
yield store
|
||||
await store.stop_ttl_sweeper()
|
||||
finally:
|
||||
async with await AsyncConnection.connect(
|
||||
admin_conn_string, autocommit=True
|
||||
@@ -635,3 +664,28 @@ async def test_search_sorting(
|
||||
assert len(set(r.key for r in results)) == 10
|
||||
assert results[0].key == "M"
|
||||
assert results[0].score > results[1].score
|
||||
|
||||
|
||||
async def test_store_ttl(store):
|
||||
# Assumes a TTL of 1 minute = 60 seconds
|
||||
ns = ("foo",)
|
||||
await store.start_ttl_sweeper()
|
||||
await store.aput(
|
||||
ns,
|
||||
key="item1",
|
||||
value={"foo": "bar"},
|
||||
ttl=TTL_MINUTES, # type: ignore
|
||||
)
|
||||
await asyncio.sleep(TTL_SECONDS - 2)
|
||||
res = await store.aget(ns, key="item1", refresh_ttl=True)
|
||||
assert res is not None
|
||||
await asyncio.sleep(TTL_SECONDS - 2)
|
||||
results = await store.asearch(ns, query="foo", refresh_ttl=True)
|
||||
assert len(results) == 1
|
||||
await asyncio.sleep(TTL_SECONDS - 2)
|
||||
res = await store.aget(ns, key="item1", refresh_ttl=False)
|
||||
assert res is not None
|
||||
await asyncio.sleep(TTL_SECONDS - 1)
|
||||
# Now has been (TTL_SECONDS-2)*2 > TTL_SECONDS + TTL_SECONDS/2
|
||||
results = await store.asearch(ns, query="bar", refresh_ttl=False)
|
||||
assert len(results) == 0
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# type: ignore
|
||||
|
||||
import re
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Optional
|
||||
from uuid import uuid4
|
||||
@@ -24,6 +25,9 @@ from tests.conftest import (
|
||||
CharacterEmbeddings,
|
||||
)
|
||||
|
||||
TTL_SECONDS = 6
|
||||
TTL_MINUTES = TTL_SECONDS / 60
|
||||
|
||||
|
||||
@pytest.fixture(scope="function", params=["default", "pipe", "pool"])
|
||||
def store(request) -> PostgresStore:
|
||||
@@ -32,29 +36,56 @@ def store(request) -> PostgresStore:
|
||||
uri_base = "/".join(uri_parts[:-1])
|
||||
query_params = ""
|
||||
if "?" in uri_parts[-1]:
|
||||
db_name, query_params = uri_parts[-1].split("?", 1)
|
||||
_, query_params = uri_parts[-1].split("?", 1)
|
||||
query_params = "?" + query_params
|
||||
|
||||
conn_string = f"{uri_base}/{database}{query_params}"
|
||||
admin_conn_string = DEFAULT_URI
|
||||
|
||||
ttl_config = {
|
||||
"default_ttl": TTL_MINUTES,
|
||||
"refresh_on_read": True,
|
||||
"sweep_interval_minutes": TTL_MINUTES / 2,
|
||||
}
|
||||
with Connection.connect(admin_conn_string, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
with PostgresStore.from_conn_string(conn_string) as store:
|
||||
with PostgresStore.from_conn_string(conn_string, ttl=ttl_config) as store:
|
||||
store.MIGRATIONS = [
|
||||
(
|
||||
mig.replace("ttl_minutes INT;", "ttl_minutes FLOAT;")
|
||||
if isinstance(mig, str)
|
||||
else mig
|
||||
)
|
||||
for mig in store.MIGRATIONS
|
||||
]
|
||||
store.setup()
|
||||
|
||||
if request.param == "pipe":
|
||||
with PostgresStore.from_conn_string(conn_string, pipeline=True) as store:
|
||||
with PostgresStore.from_conn_string(
|
||||
conn_string,
|
||||
pipeline=True,
|
||||
ttl=ttl_config,
|
||||
) as store:
|
||||
store.start_ttl_sweeper()
|
||||
yield store
|
||||
|
||||
store.stop_ttl_sweeper()
|
||||
elif request.param == "pool":
|
||||
with PostgresStore.from_conn_string(
|
||||
conn_string, pool_config={"min_size": 1, "max_size": 10}
|
||||
conn_string,
|
||||
pool_config={"min_size": 1, "max_size": 10},
|
||||
ttl=ttl_config,
|
||||
) as store:
|
||||
store.start_ttl_sweeper()
|
||||
yield store
|
||||
|
||||
store.stop_ttl_sweeper()
|
||||
else: # default
|
||||
with PostgresStore.from_conn_string(conn_string) as store:
|
||||
with PostgresStore.from_conn_string(conn_string, ttl=ttl_config) as store:
|
||||
store.start_ttl_sweeper()
|
||||
yield store
|
||||
|
||||
store.stop_ttl_sweeper()
|
||||
finally:
|
||||
with Connection.connect(admin_conn_string, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
@@ -220,134 +251,127 @@ def test_batch_list_namespaces_ops(store: PostgresStore) -> None:
|
||||
assert all(ns[-1] == "public" for ns in results[2])
|
||||
|
||||
|
||||
class TestPostgresStore:
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self) -> None:
|
||||
with PostgresStore.from_conn_string(DEFAULT_URI) as store:
|
||||
store.setup()
|
||||
def test_basic_store_ops(store) -> None:
|
||||
namespace = ("test", "documents")
|
||||
item_id = "doc1"
|
||||
item_value = {"title": "Test Document", "content": "Hello, World!"}
|
||||
|
||||
def test_basic_store_ops(self) -> None:
|
||||
with PostgresStore.from_conn_string(DEFAULT_URI) as store:
|
||||
namespace = ("test", "documents")
|
||||
item_id = "doc1"
|
||||
item_value = {"title": "Test Document", "content": "Hello, World!"}
|
||||
store.put(namespace, item_id, item_value)
|
||||
item = store.get(namespace, item_id)
|
||||
|
||||
store.put(namespace, item_id, item_value)
|
||||
item = store.get(namespace, item_id)
|
||||
assert item
|
||||
assert item.namespace == namespace
|
||||
assert item.key == item_id
|
||||
assert item.value == item_value
|
||||
|
||||
assert item
|
||||
assert item.namespace == namespace
|
||||
assert item.key == item_id
|
||||
assert item.value == item_value
|
||||
# Test update
|
||||
updated_value = {"title": "Updated Document", "content": "Hello, Updated!"}
|
||||
store.put(namespace, item_id, updated_value)
|
||||
updated_item = store.get(namespace, item_id)
|
||||
|
||||
# Test update
|
||||
updated_value = {"title": "Updated Document", "content": "Hello, Updated!"}
|
||||
store.put(namespace, item_id, updated_value)
|
||||
updated_item = store.get(namespace, item_id)
|
||||
assert updated_item.value == updated_value
|
||||
assert updated_item.updated_at > item.updated_at
|
||||
|
||||
assert updated_item.value == updated_value
|
||||
assert updated_item.updated_at > item.updated_at
|
||||
# Test get from non-existent namespace
|
||||
different_namespace = ("test", "other_documents")
|
||||
item_in_different_namespace = store.get(different_namespace, item_id)
|
||||
assert item_in_different_namespace is None
|
||||
|
||||
# Test get from non-existent namespace
|
||||
different_namespace = ("test", "other_documents")
|
||||
item_in_different_namespace = store.get(different_namespace, item_id)
|
||||
assert item_in_different_namespace is None
|
||||
# Test delete
|
||||
store.delete(namespace, item_id)
|
||||
deleted_item = store.get(namespace, item_id)
|
||||
assert deleted_item is None
|
||||
|
||||
# Test delete
|
||||
store.delete(namespace, item_id)
|
||||
deleted_item = store.get(namespace, item_id)
|
||||
assert deleted_item is None
|
||||
|
||||
def test_list_namespaces(self) -> None:
|
||||
with PostgresStore.from_conn_string(DEFAULT_URI) as store:
|
||||
# Create test data with various namespaces
|
||||
test_namespaces = [
|
||||
("test", "documents", "public"),
|
||||
("test", "documents", "private"),
|
||||
("test", "images", "public"),
|
||||
("test", "images", "private"),
|
||||
("prod", "documents", "public"),
|
||||
("prod", "documents", "private"),
|
||||
]
|
||||
def test_list_namespaces(store) -> None:
|
||||
# Create test data with various namespaces
|
||||
test_namespaces = [
|
||||
("test", "documents", "public"),
|
||||
("test", "documents", "private"),
|
||||
("test", "images", "public"),
|
||||
("test", "images", "private"),
|
||||
("prod", "documents", "public"),
|
||||
("prod", "documents", "private"),
|
||||
]
|
||||
|
||||
# Insert test data
|
||||
for namespace in test_namespaces:
|
||||
store.put(namespace, "dummy", {"content": "dummy"})
|
||||
# Insert test data
|
||||
for namespace in test_namespaces:
|
||||
store.put(namespace, "dummy", {"content": "dummy"})
|
||||
|
||||
# Test listing with various filters
|
||||
all_namespaces = store.list_namespaces()
|
||||
assert len(all_namespaces) == len(test_namespaces)
|
||||
# Test listing with various filters
|
||||
all_namespaces = store.list_namespaces()
|
||||
assert len(all_namespaces) == len(test_namespaces)
|
||||
|
||||
# Test prefix filtering
|
||||
test_prefix_namespaces = store.list_namespaces(prefix=["test"])
|
||||
assert len(test_prefix_namespaces) == 4
|
||||
assert all(ns[0] == "test" for ns in test_prefix_namespaces)
|
||||
# Test prefix filtering
|
||||
test_prefix_namespaces = store.list_namespaces(prefix=["test"])
|
||||
assert len(test_prefix_namespaces) == 4
|
||||
assert all(ns[0] == "test" for ns in test_prefix_namespaces)
|
||||
|
||||
# Test suffix filtering
|
||||
public_namespaces = store.list_namespaces(suffix=["public"])
|
||||
assert len(public_namespaces) == 3
|
||||
assert all(ns[-1] == "public" for ns in public_namespaces)
|
||||
# Test suffix filtering
|
||||
public_namespaces = store.list_namespaces(suffix=["public"])
|
||||
assert len(public_namespaces) == 3
|
||||
assert all(ns[-1] == "public" for ns in public_namespaces)
|
||||
|
||||
# Test max depth
|
||||
depth_2_namespaces = store.list_namespaces(max_depth=2)
|
||||
assert all(len(ns) <= 2 for ns in depth_2_namespaces)
|
||||
# Test max depth
|
||||
depth_2_namespaces = store.list_namespaces(max_depth=2)
|
||||
assert all(len(ns) <= 2 for ns in depth_2_namespaces)
|
||||
|
||||
# Test pagination
|
||||
paginated_namespaces = store.list_namespaces(limit=3)
|
||||
assert len(paginated_namespaces) == 3
|
||||
# Test pagination
|
||||
paginated_namespaces = store.list_namespaces(limit=3)
|
||||
assert len(paginated_namespaces) == 3
|
||||
|
||||
# Cleanup
|
||||
for namespace in test_namespaces:
|
||||
store.delete(namespace, "dummy")
|
||||
# Cleanup
|
||||
for namespace in test_namespaces:
|
||||
store.delete(namespace, "dummy")
|
||||
|
||||
def test_search(self) -> None:
|
||||
with PostgresStore.from_conn_string(DEFAULT_URI) as store:
|
||||
# Create test data
|
||||
test_data = [
|
||||
(
|
||||
("test", "docs"),
|
||||
"doc1",
|
||||
{"title": "First Doc", "author": "Alice", "tags": ["important"]},
|
||||
),
|
||||
(
|
||||
("test", "docs"),
|
||||
"doc2",
|
||||
{"title": "Second Doc", "author": "Bob", "tags": ["draft"]},
|
||||
),
|
||||
(
|
||||
("test", "images"),
|
||||
"img1",
|
||||
{"title": "Image 1", "author": "Alice", "tags": ["final"]},
|
||||
),
|
||||
]
|
||||
|
||||
for namespace, key, value in test_data:
|
||||
store.put(namespace, key, value)
|
||||
def test_search(store) -> None:
|
||||
# Create test data
|
||||
test_data = [
|
||||
(
|
||||
("test", "docs"),
|
||||
"doc1",
|
||||
{"title": "First Doc", "author": "Alice", "tags": ["important"]},
|
||||
),
|
||||
(
|
||||
("test", "docs"),
|
||||
"doc2",
|
||||
{"title": "Second Doc", "author": "Bob", "tags": ["draft"]},
|
||||
),
|
||||
(
|
||||
("test", "images"),
|
||||
"img1",
|
||||
{"title": "Image 1", "author": "Alice", "tags": ["final"]},
|
||||
),
|
||||
]
|
||||
|
||||
# Test basic search
|
||||
all_items = store.search(["test"])
|
||||
assert len(all_items) == 3
|
||||
for namespace, key, value in test_data:
|
||||
store.put(namespace, key, value)
|
||||
|
||||
# Test namespace filtering
|
||||
docs_items = store.search(["test", "docs"])
|
||||
assert len(docs_items) == 2
|
||||
assert all(item.namespace == ("test", "docs") for item in docs_items)
|
||||
# Test basic search
|
||||
all_items = store.search(["test"])
|
||||
assert len(all_items) == 3
|
||||
|
||||
# Test value filtering
|
||||
alice_items = store.search(["test"], filter={"author": "Alice"})
|
||||
assert len(alice_items) == 2
|
||||
assert all(item.value["author"] == "Alice" for item in alice_items)
|
||||
# Test namespace filtering
|
||||
docs_items = store.search(["test", "docs"])
|
||||
assert len(docs_items) == 2
|
||||
assert all(item.namespace == ("test", "docs") for item in docs_items)
|
||||
|
||||
# Test pagination
|
||||
paginated_items = store.search(["test"], limit=2)
|
||||
assert len(paginated_items) == 2
|
||||
# Test value filtering
|
||||
alice_items = store.search(["test"], filter={"author": "Alice"})
|
||||
assert len(alice_items) == 2
|
||||
assert all(item.value["author"] == "Alice" for item in alice_items)
|
||||
|
||||
offset_items = store.search(["test"], offset=2)
|
||||
assert len(offset_items) == 1
|
||||
# Test pagination
|
||||
paginated_items = store.search(["test"], limit=2)
|
||||
assert len(paginated_items) == 2
|
||||
|
||||
# Cleanup
|
||||
for namespace, key, _ in test_data:
|
||||
store.delete(namespace, key)
|
||||
offset_items = store.search(["test"], offset=2)
|
||||
assert len(offset_items) == 1
|
||||
|
||||
# Cleanup
|
||||
for namespace, key, _ in test_data:
|
||||
store.delete(namespace, key)
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -356,6 +380,7 @@ def _create_vector_store(
|
||||
distance_type: str,
|
||||
fake_embeddings: Embeddings,
|
||||
text_fields: Optional[list[str]] = None,
|
||||
enable_ttl: bool = True,
|
||||
) -> PostgresStore:
|
||||
"""Create a store with vector search enabled."""
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
@@ -385,23 +410,32 @@ def _create_vector_store(
|
||||
with PostgresStore.from_conn_string(
|
||||
conn_string,
|
||||
index=index_config,
|
||||
ttl={"default_ttl": 2, "refresh_on_read": True} if enable_ttl else None,
|
||||
) as store:
|
||||
store.setup()
|
||||
with store._cursor() as cur:
|
||||
# drop the migration index
|
||||
cur.execute("DROP TABLE IF EXISTS store_migrations")
|
||||
store.setup() # Will fail if migrations aren't idempotent
|
||||
yield store
|
||||
finally:
|
||||
with Connection.connect(admin_conn_string, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
_vector_params = [
|
||||
(vector_type, distance_type, True)
|
||||
for vector_type in VECTOR_TYPES
|
||||
for distance_type in (
|
||||
["hamming"] if vector_type == "bit" else ["l2", "inner_product", "cosine"]
|
||||
)
|
||||
]
|
||||
_vector_params += [(*_vector_params[-1][:2], False)]
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
scope="function",
|
||||
params=[
|
||||
(vector_type, distance_type)
|
||||
for vector_type in VECTOR_TYPES
|
||||
for distance_type in (
|
||||
["hamming"] if vector_type == "bit" else ["l2", "inner_product", "cosine"]
|
||||
)
|
||||
],
|
||||
params=_vector_params,
|
||||
ids=lambda p: f"{p[0]}_{p[1]}",
|
||||
)
|
||||
def vector_store(
|
||||
@@ -409,8 +443,10 @@ def vector_store(
|
||||
fake_embeddings: Embeddings,
|
||||
) -> PostgresStore:
|
||||
"""Create a store with vector search enabled."""
|
||||
vector_type, distance_type = request.param
|
||||
with _create_vector_store(vector_type, distance_type, fake_embeddings) as store:
|
||||
vector_type, distance_type, enable_ttl = request.param
|
||||
with _create_vector_store(
|
||||
vector_type, distance_type, fake_embeddings, enable_ttl=enable_ttl
|
||||
) as store:
|
||||
yield store
|
||||
|
||||
|
||||
@@ -474,7 +510,10 @@ def test_vector_update_with_embedding(vector_store: PostgresStore) -> None:
|
||||
assert not any(r.key == "doc4" for r in results_new)
|
||||
|
||||
|
||||
def test_vector_search_with_filters(vector_store: PostgresStore) -> None:
|
||||
@pytest.mark.parametrize("refresh_ttl", [True, False])
|
||||
def test_vector_search_with_filters(
|
||||
vector_store: PostgresStore, refresh_ttl: bool
|
||||
) -> None:
|
||||
"""Test combining vector search with filters."""
|
||||
# Insert test documents
|
||||
docs = [
|
||||
@@ -487,16 +526,23 @@ def test_vector_search_with_filters(vector_store: PostgresStore) -> None:
|
||||
for key, value in docs:
|
||||
vector_store.put(("test",), key, value)
|
||||
|
||||
results = vector_store.search(("test",), query="apple", filter={"color": "red"})
|
||||
results = vector_store.search(
|
||||
("test",), query="apple", filter={"color": "red"}, refresh_ttl=refresh_ttl
|
||||
)
|
||||
assert len(results) == 2
|
||||
assert results[0].key == "doc1"
|
||||
|
||||
results = vector_store.search(("test",), query="car", filter={"color": "red"})
|
||||
results = vector_store.search(
|
||||
("test",), query="car", filter={"color": "red"}, refresh_ttl=refresh_ttl
|
||||
)
|
||||
assert len(results) == 2
|
||||
assert results[0].key == "doc2"
|
||||
|
||||
results = vector_store.search(
|
||||
("test",), query="bbbbluuu", filter={"score": {"$gt": 3.2}}
|
||||
("test",),
|
||||
query="bbbbluuu",
|
||||
filter={"score": {"$gt": 3.2}},
|
||||
refresh_ttl=refresh_ttl,
|
||||
)
|
||||
assert len(results) == 3
|
||||
assert results[0].key == "doc4"
|
||||
@@ -688,7 +734,7 @@ def test_embed_with_path_operation_config(
|
||||
store.put(("test",), "doc5", doc5, index=False)
|
||||
results = store.search(("test",))
|
||||
assert len(results) == 3
|
||||
assert all(r.score is None for r in results)
|
||||
assert all(r.score is None for r in results), f"{results}"
|
||||
assert any(r.key == "doc5" for r in results)
|
||||
|
||||
results = store.search(("test",), query="hhh")
|
||||
@@ -790,3 +836,27 @@ def test_nonnull_migrations() -> None:
|
||||
for migration in PostgresStore.MIGRATIONS:
|
||||
statement = _leading_comment_remover.sub("", migration).split()[0]
|
||||
assert statement.strip()
|
||||
|
||||
|
||||
def test_store_ttl(store):
|
||||
# Assumes a TTL of 1 minute = 60 seconds
|
||||
ns = ("foo",)
|
||||
store.put(
|
||||
ns,
|
||||
key="item1",
|
||||
value={"foo": "bar"},
|
||||
ttl=TTL_MINUTES, # type: ignore
|
||||
)
|
||||
time.sleep(TTL_SECONDS - 2)
|
||||
res = store.get(ns, key="item1", refresh_ttl=True)
|
||||
assert res is not None
|
||||
time.sleep(TTL_SECONDS - 2)
|
||||
results = store.search(ns, query="foo", refresh_ttl=True)
|
||||
assert len(results) == 1
|
||||
time.sleep(TTL_SECONDS - 2)
|
||||
res = store.get(ns, key="item1", refresh_ttl=False)
|
||||
assert res is not None
|
||||
time.sleep(TTL_SECONDS - 1)
|
||||
# Now has been (TTL_SECONDS-2)*2 > TTL_SECONDS + TTL_SECONDS/2
|
||||
res = store.search(ns, query="bar", refresh_ttl=False)
|
||||
assert len(res) == 0
|
||||
|
||||
@@ -12,7 +12,7 @@ read_config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
with SqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
checkpoint = {
|
||||
"v": 1,
|
||||
"v": 2,
|
||||
"ts": "2024-07-31T20:14:19.804150+00:00",
|
||||
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
|
||||
"channel_values": {
|
||||
@@ -54,7 +54,7 @@ from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
||||
|
||||
async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
checkpoint = {
|
||||
"v": 1,
|
||||
"v": 2,
|
||||
"ts": "2024-07-31T20:14:19.804150+00:00",
|
||||
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
|
||||
"channel_values": {
|
||||
|
||||
@@ -56,7 +56,10 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
||||
>>> builder.add_node("add_one", lambda x: x + 1)
|
||||
>>> builder.set_entry_point("add_one")
|
||||
>>> builder.set_finish_point("add_one")
|
||||
>>> conn = sqlite3.connect("checkpoints.sqlite")
|
||||
>>> # Create a new SqliteSaver instance
|
||||
>>> # Note: check_same_thread=False is OK as the implementation uses a lock
|
||||
>>> # to ensure thread safety.
|
||||
>>> conn = sqlite3.connect("checkpoints.sqlite", check_same_thread=False)
|
||||
>>> memory = SqliteSaver(conn)
|
||||
>>> graph = builder.compile(checkpointer=memory)
|
||||
>>> config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
@@ -70,15 +70,18 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
||||
>>> from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
||||
>>> from langgraph.graph import StateGraph
|
||||
>>>
|
||||
>>> builder = StateGraph(int)
|
||||
>>> builder.add_node("add_one", lambda x: x + 1)
|
||||
>>> builder.set_entry_point("add_one")
|
||||
>>> builder.set_finish_point("add_one")
|
||||
>>> async with AsyncSqliteSaver.from_conn_string("checkpoints.db") as memory:
|
||||
>>> graph = builder.compile(checkpointer=memory)
|
||||
>>> coro = graph.ainvoke(1, {"configurable": {"thread_id": "thread-1"}})
|
||||
>>> print(asyncio.run(coro))
|
||||
Output: 2
|
||||
>>> async def main():
|
||||
>>> builder = StateGraph(int)
|
||||
>>> builder.add_node("add_one", lambda x: x + 1)
|
||||
>>> builder.set_entry_point("add_one")
|
||||
>>> builder.set_finish_point("add_one")
|
||||
>>> async with AsyncSqliteSaver.from_conn_string("checkpoints.db") as memory:
|
||||
>>> graph = builder.compile(checkpointer=memory)
|
||||
>>> coro = graph.ainvoke(1, {"configurable": {"thread_id": "thread-1"}})
|
||||
>>> print(await asyncio.gather(coro))
|
||||
>>>
|
||||
>>> asyncio.run(main())
|
||||
Output: [2]
|
||||
```
|
||||
Raw usage:
|
||||
|
||||
@@ -90,12 +93,12 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
||||
>>> async def main():
|
||||
>>> async with aiosqlite.connect("checkpoints.db") as conn:
|
||||
... saver = AsyncSqliteSaver(conn)
|
||||
... config = {"configurable": {"thread_id": "1"}}
|
||||
... checkpoint = {"ts": "2023-05-03T10:00:00Z", "data": {"key": "value"}}
|
||||
... config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
|
||||
... checkpoint = {"ts": "2023-05-03T10:00:00Z", "data": {"key": "value"}, "id": "0c62ca34-ac19-445d-bbb0-5b4984975b2a"}
|
||||
... saved_config = await saver.aput(config, checkpoint, {}, {})
|
||||
... print(saved_config)
|
||||
>>> asyncio.run(main())
|
||||
{"configurable": {"thread_id": "1", "checkpoint_id": "0c62ca34-ac19-445d-bbb0-5b4984975b2a"}}
|
||||
{'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '0c62ca34-ac19-445d-bbb0-5b4984975b2a'}}
|
||||
```
|
||||
"""
|
||||
|
||||
@@ -530,6 +533,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
||||
for idx, (channel, value) in enumerate(writes)
|
||||
],
|
||||
)
|
||||
await self.conn.commit()
|
||||
|
||||
def get_next_version(self, current: Optional[str], channel: ChannelProtocol) -> str:
|
||||
"""Generate the next version ID for a channel.
|
||||
|
||||
Generated
+39
-77
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiosqlite"
|
||||
@@ -350,7 +350,7 @@ typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.15"
|
||||
version = "2.0.21"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -360,7 +360,7 @@ develop = true
|
||||
|
||||
[package.dependencies]
|
||||
langchain-core = ">=0.2.38,<0.4"
|
||||
msgpack = "^1.1.0"
|
||||
ormsgpack = "^1.8.0"
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
@@ -387,80 +387,6 @@ pydantic = [
|
||||
]
|
||||
requests = ">=2,<3"
|
||||
|
||||
[[package]]
|
||||
name = "msgpack"
|
||||
version = "1.1.0"
|
||||
description = "MessagePack serializer"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"},
|
||||
{file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec"},
|
||||
{file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96"},
|
||||
{file = "msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870"},
|
||||
{file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7"},
|
||||
{file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb"},
|
||||
{file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f"},
|
||||
{file = "msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b"},
|
||||
{file = "msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325"},
|
||||
{file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.11.2"
|
||||
@@ -584,6 +510,42 @@ files = [
|
||||
{file = "orjson-3.10.6.tar.gz", hash = "sha256:e54b63d0a7c6c54a5f5f726bc93a2078111ef060fec4ecbf34c5db800ca3b3a7"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ormsgpack"
|
||||
version = "1.9.0"
|
||||
description = "Fast, correct Python msgpack library supporting dataclasses, datetimes, and numpy"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "ormsgpack-1.9.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9c7cc221489aaf8bf394225a275edf068f3531529def415a8e6e32d6228ee138"},
|
||||
{file = "ormsgpack-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42a5c5028417e710e5169c77d90b08891299f77ffd87abbb2855ffc62314740a"},
|
||||
{file = "ormsgpack-1.9.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:350fdfac11215234b14d7fb484cf8f3f524eb0e7c6a3614bf878f4d034c1cef2"},
|
||||
{file = "ormsgpack-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebb49ca6d3f8dca7b667397016cb2cab7e6581b1d85b30f2697824479150e31e"},
|
||||
{file = "ormsgpack-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ec9ad897bf00c4933bea519d505b82e20f9e0972bdd458dd1e06d6d5e0b8eec6"},
|
||||
{file = "ormsgpack-1.9.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5b473282dacddf20f03b99971e3fc3691bbeafc6142c8e51e80f137e35147ec9"},
|
||||
{file = "ormsgpack-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84bbd03ebca6efb38cb697e2e24f9ae22feb58ef1e6e664239ae68f4ccb3db76"},
|
||||
{file = "ormsgpack-1.9.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49e7e3612f1930267ddf85e914ba417bf5fa801e4a045acb466fa8a8bf7f8bf8"},
|
||||
{file = "ormsgpack-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da0aa79373e70c8ad32c0a23f410a7d611a13ea4f1e427f501307a487caf0557"},
|
||||
{file = "ormsgpack-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:6dfecbe00e504ccf946fc168ad56d038682fd17592da1be44368ab996fbeae3e"},
|
||||
{file = "ormsgpack-1.9.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6f13a538674ee42764278b418f9e97743401cd3895c7c473d45abd03f650169b"},
|
||||
{file = "ormsgpack-1.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:332d85cbf3775b96b6eacdd0c03758517b530365dfa6e55981190062d840be47"},
|
||||
{file = "ormsgpack-1.9.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4b9de72dc94f73d63047ad40cfdd6e9dd2b28c51e9ccbc72117d5146b4f5fc18"},
|
||||
{file = "ormsgpack-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4577cf304fa4c079092280e9ed4858cd9bd8b1475a803c206a449a3830b499ef"},
|
||||
{file = "ormsgpack-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:32302872cf10e4eccc8437cdaf46ac8e5e56cbb7519734a0b8f8a1ed2cbdfd44"},
|
||||
{file = "ormsgpack-1.9.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6ccbdf412af6c46b3549929d90a960ebe1b45f9b3e6c530774cd29de0846ce4d"},
|
||||
{file = "ormsgpack-1.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a6e113502c002f12f6bcf100eb8c2ccb85d1e75931ede669765ffaf5cc0e69d"},
|
||||
{file = "ormsgpack-1.9.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:afd8bc92bb903fc37ce16921bb522d205ba02b90871dc4edc6fac13ac9226481"},
|
||||
{file = "ormsgpack-1.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:800d918e6bca16d01c382015a4c05b00cabafa7c2070126b7feaefe2cf1437f0"},
|
||||
{file = "ormsgpack-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:305ec6de5fd687b7de0861673e967b4f6474a634b159a3a82e481707308203c9"},
|
||||
{file = "ormsgpack-1.9.0-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ecd28f5e0a07578972c9681034f1a6413ac0d0f016ff09db47dd9a7e8191d57a"},
|
||||
{file = "ormsgpack-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0767bde96b932c70f3e1dd0e82a5c3dd969e2223edd7e8b3303cba1fa38473d1"},
|
||||
{file = "ormsgpack-1.9.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58b7c35bb813bb461b2bf848e99e129d536f6ed47f1d1c49e3de02748fe8554f"},
|
||||
{file = "ormsgpack-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aa6bc3904fbc4e6538e1bb3f2748f5cbc34906597724a3f0b8f578972a21fae"},
|
||||
{file = "ormsgpack-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:09f7b11abc0b493735870f3dea5daf36a147916b0609f394d45373f5ae4b6850"},
|
||||
{file = "ormsgpack-1.9.0.tar.gz", hash = "sha256:015e8e6e74e5a1c2bcb9c25fdd8205cad0e8e2d1d32c6a259615aa189b61b8b4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "2.0.5"
|
||||
version = "2.0.6"
|
||||
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -51,7 +51,7 @@ read_config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
checkpoint = {
|
||||
"v": 1,
|
||||
"v": 2,
|
||||
"ts": "2024-07-31T20:14:19.804150+00:00",
|
||||
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
|
||||
"channel_values": {
|
||||
|
||||
@@ -30,6 +30,7 @@ from langgraph.checkpoint.serde.types import (
|
||||
|
||||
V = TypeVar("V", int, float, str)
|
||||
PendingWrite = Tuple[str, str, Any]
|
||||
LATEST_VERSION = 2
|
||||
|
||||
|
||||
# Marked as total=False to allow for future expansion.
|
||||
@@ -101,7 +102,7 @@ class Checkpoint(TypedDict):
|
||||
|
||||
def empty_checkpoint() -> Checkpoint:
|
||||
return Checkpoint(
|
||||
v=1,
|
||||
v=LATEST_VERSION,
|
||||
id=str(uuid6(clock_seq=-2)),
|
||||
ts=datetime.now(timezone.utc).isoformat(),
|
||||
channel_values={},
|
||||
@@ -144,7 +145,7 @@ def create_checkpoint(
|
||||
except EmptyChannelError:
|
||||
pass
|
||||
return Checkpoint(
|
||||
v=1,
|
||||
v=LATEST_VERSION,
|
||||
ts=ts,
|
||||
id=id or str(uuid6(clock_seq=step)),
|
||||
channel_values=values,
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections import defaultdict
|
||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||
from contextlib import AbstractAsyncContextManager, AbstractContextManager, ExitStack
|
||||
from types import TracebackType
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
@@ -70,6 +70,12 @@ class InMemorySaver(
|
||||
tuple[str, str, str],
|
||||
dict[tuple[str, int], tuple[str, str, tuple[str, bytes], str]],
|
||||
]
|
||||
blobs: dict[
|
||||
tuple[
|
||||
str, str, str, Union[str, int, float]
|
||||
], # thread id, checkpoint ns, channel, version
|
||||
tuple[str, bytes],
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -80,6 +86,7 @@ class InMemorySaver(
|
||||
super().__init__(serde=serde)
|
||||
self.storage = factory(lambda: defaultdict(dict))
|
||||
self.writes = factory(dict)
|
||||
self.blobs = factory()
|
||||
self.stack = ExitStack()
|
||||
if factory is not defaultdict:
|
||||
self.stack.enter_context(self.storage) # type: ignore[arg-type]
|
||||
@@ -107,6 +114,18 @@ class InMemorySaver(
|
||||
) -> Optional[bool]:
|
||||
return self.stack.__exit__(__exc_type, __exc_value, __traceback)
|
||||
|
||||
def _load_blobs(
|
||||
self, thread_id: str, checkpoint_ns: str, versions: ChannelVersions
|
||||
) -> dict[str, Any]:
|
||||
channel_values: dict[str, Any] = {}
|
||||
for k, v in versions.items():
|
||||
kk = (thread_id, checkpoint_ns, k, v)
|
||||
if kk in self.blobs:
|
||||
vv = self.blobs[kk]
|
||||
if vv[0] != "empty":
|
||||
channel_values[k] = self.serde.loads_typed(vv)
|
||||
return channel_values
|
||||
|
||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||
"""Get a checkpoint tuple from the in-memory storage.
|
||||
|
||||
@@ -121,8 +140,8 @@ class InMemorySaver(
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
"""
|
||||
thread_id = config["configurable"]["thread_id"]
|
||||
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
||||
thread_id: str = config["configurable"]["thread_id"]
|
||||
checkpoint_ns: str = config["configurable"].get("checkpoint_ns", "")
|
||||
if checkpoint_id := get_checkpoint_id(config):
|
||||
if saved := self.storage[thread_id][checkpoint_ns].get(checkpoint_id):
|
||||
checkpoint, metadata, parent_checkpoint_id = saved
|
||||
@@ -140,10 +159,14 @@ class InMemorySaver(
|
||||
)
|
||||
else:
|
||||
sends = []
|
||||
checkpoint_: Checkpoint = self.serde.loads_typed(checkpoint)
|
||||
return CheckpointTuple(
|
||||
config=config,
|
||||
checkpoint={
|
||||
**self.serde.loads_typed(checkpoint),
|
||||
**checkpoint_,
|
||||
"channel_values": self._load_blobs(
|
||||
thread_id, checkpoint_ns, checkpoint_["channel_versions"]
|
||||
),
|
||||
"pending_sends": [self.serde.loads_typed(s[2]) for s in sends],
|
||||
},
|
||||
metadata=self.serde.loads_typed(metadata),
|
||||
@@ -180,6 +203,9 @@ class InMemorySaver(
|
||||
)
|
||||
else:
|
||||
sends = []
|
||||
|
||||
checkpoint_ = self.serde.loads_typed(checkpoint)
|
||||
|
||||
return CheckpointTuple(
|
||||
config={
|
||||
"configurable": {
|
||||
@@ -189,7 +215,10 @@ class InMemorySaver(
|
||||
}
|
||||
},
|
||||
checkpoint={
|
||||
**self.serde.loads_typed(checkpoint),
|
||||
**checkpoint_,
|
||||
"channel_values": self._load_blobs(
|
||||
thread_id, checkpoint_ns, checkpoint_["channel_versions"]
|
||||
),
|
||||
"pending_sends": [self.serde.loads_typed(s[2]) for s in sends],
|
||||
},
|
||||
metadata=self.serde.loads_typed(metadata),
|
||||
@@ -297,6 +326,8 @@ class InMemorySaver(
|
||||
else:
|
||||
sends = []
|
||||
|
||||
checkpoint_: Checkpoint = self.serde.loads_typed(checkpoint)
|
||||
|
||||
yield CheckpointTuple(
|
||||
config={
|
||||
"configurable": {
|
||||
@@ -306,7 +337,12 @@ class InMemorySaver(
|
||||
}
|
||||
},
|
||||
checkpoint={
|
||||
**self.serde.loads_typed(checkpoint),
|
||||
**checkpoint_,
|
||||
"channel_values": self._load_blobs(
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
checkpoint_["channel_versions"],
|
||||
),
|
||||
"pending_sends": [
|
||||
self.serde.loads_typed(s[2]) for s in sends
|
||||
],
|
||||
@@ -353,6 +389,11 @@ class InMemorySaver(
|
||||
c.pop("pending_sends") # type: ignore[misc]
|
||||
thread_id = config["configurable"]["thread_id"]
|
||||
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
||||
values: dict[str, Any] = c.pop("channel_values") # type: ignore[misc]
|
||||
for k, v in new_versions.items():
|
||||
self.blobs[(thread_id, checkpoint_ns, k, v)] = (
|
||||
self.serde.dumps_typed(values[k]) if k in values else ("empty", b"")
|
||||
)
|
||||
self.storage[thread_id][checkpoint_ns].update(
|
||||
{
|
||||
checkpoint["id"]: (
|
||||
|
||||
@@ -45,3 +45,18 @@ def maybe_add_typed_methods(serde: SerializerProtocol) -> SerializerProtocol:
|
||||
return SerializerCompat(serde)
|
||||
|
||||
return serde
|
||||
|
||||
|
||||
class CipherProtocol(Protocol):
|
||||
"""Protocol for encryption and decryption of data.
|
||||
- `encrypt`: Encrypt plaintext.
|
||||
- `decrypt`: Decrypt ciphertext.
|
||||
"""
|
||||
|
||||
def encrypt(self, plaintext: bytes) -> tuple[str, bytes]:
|
||||
"""Encrypt plaintext. Returns a tuple (cipher name, ciphertext)."""
|
||||
...
|
||||
|
||||
def decrypt(self, ciphername: str, ciphertext: bytes) -> bytes:
|
||||
"""Decrypt ciphertext. Returns the plaintext."""
|
||||
...
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from langgraph.checkpoint.serde.base import CipherProtocol, SerializerProtocol
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
|
||||
|
||||
class EncryptedSerializer(SerializerProtocol):
|
||||
"""Serializer that encrypts and decrypts data using an encryption protocol."""
|
||||
|
||||
def __init__(
|
||||
self, cipher: CipherProtocol, serde: SerializerProtocol = JsonPlusSerializer()
|
||||
) -> None:
|
||||
self.cipher = cipher
|
||||
self.serde = serde
|
||||
|
||||
def dumps(self, obj: Any) -> bytes:
|
||||
return self.serde.dumps(obj)
|
||||
|
||||
def loads(self, data: bytes) -> Any:
|
||||
return self.serde.loads(data)
|
||||
|
||||
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
|
||||
"""Serialize an object to a tuple (type, bytes) and encrypt the bytes."""
|
||||
# serialize data
|
||||
typ, data = self.serde.dumps_typed(obj)
|
||||
# encrypt data
|
||||
ciphername, ciphertext = self.cipher.encrypt(data)
|
||||
# add cipher name to type
|
||||
return f"{typ}+{ciphername}", ciphertext
|
||||
|
||||
def loads_typed(self, data: tuple[str, bytes]) -> Any:
|
||||
enc_cipher, ciphertext = data
|
||||
# unencrypted data
|
||||
if "+" not in enc_cipher:
|
||||
return self.serde.loads_typed(data)
|
||||
# extract cipher name
|
||||
typ, ciphername = enc_cipher.split("+", 1)
|
||||
# decrypt data
|
||||
decrypted_data = self.cipher.decrypt(ciphername, ciphertext)
|
||||
# deserialize data
|
||||
return self.serde.loads_typed((typ, decrypted_data))
|
||||
|
||||
@classmethod
|
||||
def from_pycryptodome_aes(
|
||||
cls, serde: SerializerProtocol = JsonPlusSerializer(), **kwargs: Any
|
||||
) -> "EncryptedSerializer":
|
||||
"""Create an EncryptedSerializer using AES encryption."""
|
||||
try:
|
||||
from Crypto.Cipher import AES # type: ignore
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Pycryptodome is not installed. Please install it with `pip install pycryptodome`."
|
||||
) from None
|
||||
|
||||
# check if AES key is provided
|
||||
if "key" in kwargs:
|
||||
key: bytes = kwargs.pop("key")
|
||||
else:
|
||||
key_str = os.getenv("LANGGRAPH_AES_KEY")
|
||||
if key_str is None:
|
||||
raise ValueError("LANGGRAPH_AES_KEY environment variable is not set.")
|
||||
key = key_str.encode()
|
||||
if len(key) not in (16, 24, 32):
|
||||
raise ValueError("LANGGRAPH_AES_KEY must be 16, 24, or 32 bytes long.")
|
||||
|
||||
# set default mode to EAX if not provided
|
||||
if kwargs.get("mode") is None:
|
||||
kwargs["mode"] = AES.MODE_EAX
|
||||
|
||||
class PycryptodomeAesCipher(CipherProtocol):
|
||||
def encrypt(self, plaintext: bytes) -> tuple[str, bytes]:
|
||||
cipher = AES.new(key, **kwargs)
|
||||
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
|
||||
return "aes", cipher.nonce + tag + ciphertext
|
||||
|
||||
def decrypt(self, ciphername: str, ciphertext: bytes) -> bytes:
|
||||
assert ciphername == "aes", f"Unsupported cipher: {ciphername}"
|
||||
nonce = ciphertext[:16]
|
||||
tag = ciphertext[16:32]
|
||||
actual_ciphertext = ciphertext[32:]
|
||||
|
||||
cipher = AES.new(key, **kwargs, nonce=nonce)
|
||||
return cipher.decrypt_and_verify(actual_ciphertext, tag)
|
||||
|
||||
return cls(PycryptodomeAesCipher(), serde)
|
||||
@@ -20,7 +20,7 @@ from ipaddress import (
|
||||
from typing import Any, Callable, Optional, Union, cast
|
||||
from uuid import UUID
|
||||
|
||||
import msgpack # type: ignore[import-untyped]
|
||||
import ormsgpack
|
||||
from langchain_core.load.load import Reviver
|
||||
from langchain_core.load.serializable import Serializable
|
||||
from zoneinfo import ZoneInfo
|
||||
@@ -33,6 +33,15 @@ LC_REVIVER = Reviver()
|
||||
|
||||
|
||||
class JsonPlusSerializer(SerializerProtocol):
|
||||
def __init__(
|
||||
self, *, __unpack_ext_hook__: Optional[Callable[[int, bytes], Any]] = None
|
||||
) -> None:
|
||||
self._unpack_ext_hook = (
|
||||
__unpack_ext_hook__
|
||||
if __unpack_ext_hook__ is not None
|
||||
else _msgpack_ext_hook
|
||||
)
|
||||
|
||||
def _encode_constructor_args(
|
||||
self,
|
||||
constructor: Union[Callable, type[Any]],
|
||||
@@ -192,8 +201,10 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
else:
|
||||
try:
|
||||
return "msgpack", _msgpack_enc(obj)
|
||||
except UnicodeEncodeError:
|
||||
return "json", self.dumps(obj)
|
||||
except ormsgpack.MsgpackEncodeError as exc:
|
||||
if "valid UTF-8" in str(exc):
|
||||
return "json", self.dumps(obj)
|
||||
raise exc
|
||||
|
||||
def loads(self, data: bytes) -> Any:
|
||||
return json.loads(data, object_hook=self._reviver)
|
||||
@@ -207,8 +218,8 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
elif type_ == "json":
|
||||
return self.loads(data_)
|
||||
elif type_ == "msgpack":
|
||||
return msgpack.unpackb(
|
||||
data_, ext_hook=_msgpack_ext_hook, strict_map_key=False
|
||||
return ormsgpack.unpackb(
|
||||
data_, ext_hook=self._unpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown serialization type: {type_}")
|
||||
@@ -224,9 +235,9 @@ EXT_PYDANTIC_V1 = 4
|
||||
EXT_PYDANTIC_V2 = 5
|
||||
|
||||
|
||||
def _msgpack_default(obj: Any) -> Union[str, msgpack.ExtType]:
|
||||
def _msgpack_default(obj: Any) -> Union[str, ormsgpack.Ext]:
|
||||
if hasattr(obj, "model_dump") and callable(obj.model_dump): # pydantic v2
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_PYDANTIC_V2,
|
||||
_msgpack_enc(
|
||||
(
|
||||
@@ -238,7 +249,7 @@ def _msgpack_default(obj: Any) -> Union[str, msgpack.ExtType]:
|
||||
),
|
||||
)
|
||||
elif hasattr(obj, "get_secret_value") and callable(obj.get_secret_value):
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_SINGLE_ARG,
|
||||
_msgpack_enc(
|
||||
(
|
||||
@@ -249,7 +260,7 @@ def _msgpack_default(obj: Any) -> Union[str, msgpack.ExtType]:
|
||||
),
|
||||
)
|
||||
elif hasattr(obj, "dict") and callable(obj.dict): # pydantic v1
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_PYDANTIC_V1,
|
||||
_msgpack_enc(
|
||||
(
|
||||
@@ -260,7 +271,7 @@ def _msgpack_default(obj: Any) -> Union[str, msgpack.ExtType]:
|
||||
),
|
||||
)
|
||||
elif hasattr(obj, "_asdict") and callable(obj._asdict): # namedtuple
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_KW_ARGS,
|
||||
_msgpack_enc(
|
||||
(
|
||||
@@ -271,56 +282,63 @@ def _msgpack_default(obj: Any) -> Union[str, msgpack.ExtType]:
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, pathlib.Path):
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_POS_ARGS,
|
||||
_msgpack_enc(
|
||||
(obj.__class__.__module__, obj.__class__.__name__, obj.parts),
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, re.Pattern):
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_POS_ARGS,
|
||||
_msgpack_enc(
|
||||
("re", "compile", (obj.pattern, obj.flags)),
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, UUID):
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_SINGLE_ARG,
|
||||
_msgpack_enc(
|
||||
(obj.__class__.__module__, obj.__class__.__name__, obj.hex),
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, bytearray):
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_SINGLE_ARG,
|
||||
_msgpack_enc(
|
||||
(obj.__class__.__module__, obj.__class__.__name__, bytes(obj)),
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, decimal.Decimal):
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_SINGLE_ARG,
|
||||
_msgpack_enc(
|
||||
(obj.__class__.__module__, obj.__class__.__name__, str(obj)),
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, (set, frozenset, deque)):
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_SINGLE_ARG,
|
||||
_msgpack_enc(
|
||||
(obj.__class__.__module__, obj.__class__.__name__, tuple(obj)),
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, (IPv4Address, IPv4Interface, IPv4Network)):
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_SINGLE_ARG,
|
||||
_msgpack_enc(
|
||||
(obj.__class__.__module__, obj.__class__.__name__, str(obj)),
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, (IPv6Address, IPv6Interface, IPv6Network)):
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_SINGLE_ARG,
|
||||
_msgpack_enc(
|
||||
(obj.__class__.__module__, obj.__class__.__name__, str(obj)),
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, datetime):
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_METHOD_SINGLE_ARG,
|
||||
_msgpack_enc(
|
||||
(
|
||||
@@ -332,7 +350,7 @@ def _msgpack_default(obj: Any) -> Union[str, msgpack.ExtType]:
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, timedelta):
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_POS_ARGS,
|
||||
_msgpack_enc(
|
||||
(
|
||||
@@ -343,7 +361,7 @@ def _msgpack_default(obj: Any) -> Union[str, msgpack.ExtType]:
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, date):
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_POS_ARGS,
|
||||
_msgpack_enc(
|
||||
(
|
||||
@@ -354,7 +372,7 @@ def _msgpack_default(obj: Any) -> Union[str, msgpack.ExtType]:
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, time):
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_KW_ARGS,
|
||||
_msgpack_enc(
|
||||
(
|
||||
@@ -372,7 +390,7 @@ def _msgpack_default(obj: Any) -> Union[str, msgpack.ExtType]:
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, timezone):
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_POS_ARGS,
|
||||
_msgpack_enc(
|
||||
(
|
||||
@@ -383,21 +401,21 @@ def _msgpack_default(obj: Any) -> Union[str, msgpack.ExtType]:
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, ZoneInfo):
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_SINGLE_ARG,
|
||||
_msgpack_enc(
|
||||
(obj.__class__.__module__, obj.__class__.__name__, obj.key),
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, Enum):
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_SINGLE_ARG,
|
||||
_msgpack_enc(
|
||||
(obj.__class__.__module__, obj.__class__.__name__, obj.value),
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, SendProtocol):
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_POS_ARGS,
|
||||
_msgpack_enc(
|
||||
(obj.__class__.__module__, obj.__class__.__name__, (obj.node, obj.arg)),
|
||||
@@ -405,7 +423,7 @@ def _msgpack_default(obj: Any) -> Union[str, msgpack.ExtType]:
|
||||
)
|
||||
elif dataclasses.is_dataclass(obj):
|
||||
# doesn't use dataclasses.asdict to avoid deepcopy and recursion
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_KW_ARGS,
|
||||
_msgpack_enc(
|
||||
(
|
||||
@@ -419,7 +437,7 @@ def _msgpack_default(obj: Any) -> Union[str, msgpack.ExtType]:
|
||||
),
|
||||
)
|
||||
elif isinstance(obj, Item):
|
||||
return msgpack.ExtType(
|
||||
return ormsgpack.Ext(
|
||||
EXT_CONSTRUCTOR_KW_ARGS,
|
||||
_msgpack_enc(
|
||||
(
|
||||
@@ -429,7 +447,6 @@ def _msgpack_default(obj: Any) -> Union[str, msgpack.ExtType]:
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
elif isinstance(obj, BaseException):
|
||||
return repr(obj)
|
||||
else:
|
||||
@@ -439,8 +456,8 @@ def _msgpack_default(obj: Any) -> Union[str, msgpack.ExtType]:
|
||||
def _msgpack_ext_hook(code: int, data: bytes) -> Any:
|
||||
if code == EXT_CONSTRUCTOR_SINGLE_ARG:
|
||||
try:
|
||||
tup = msgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, strict_map_key=False
|
||||
tup = ormsgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
# module, name, arg
|
||||
return getattr(importlib.import_module(tup[0]), tup[1])(tup[2])
|
||||
@@ -448,8 +465,8 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
|
||||
return
|
||||
elif code == EXT_CONSTRUCTOR_POS_ARGS:
|
||||
try:
|
||||
tup = msgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, strict_map_key=False
|
||||
tup = ormsgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
# module, name, args
|
||||
return getattr(importlib.import_module(tup[0]), tup[1])(*tup[2])
|
||||
@@ -457,8 +474,8 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
|
||||
return
|
||||
elif code == EXT_CONSTRUCTOR_KW_ARGS:
|
||||
try:
|
||||
tup = msgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, strict_map_key=False
|
||||
tup = ormsgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
# module, name, args
|
||||
return getattr(importlib.import_module(tup[0]), tup[1])(**tup[2])
|
||||
@@ -466,8 +483,8 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
|
||||
return
|
||||
elif code == EXT_METHOD_SINGLE_ARG:
|
||||
try:
|
||||
tup = msgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, strict_map_key=False
|
||||
tup = ormsgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
# module, name, arg, method
|
||||
return getattr(getattr(importlib.import_module(tup[0]), tup[1]), tup[3])(
|
||||
@@ -477,8 +494,8 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
|
||||
return
|
||||
elif code == EXT_PYDANTIC_V1:
|
||||
try:
|
||||
tup = msgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, strict_map_key=False
|
||||
tup = ormsgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
# module, name, kwargs
|
||||
cls = getattr(importlib.import_module(tup[0]), tup[1])
|
||||
@@ -487,11 +504,16 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
|
||||
except Exception:
|
||||
return cls.construct(**tup[2])
|
||||
except Exception:
|
||||
return
|
||||
# for pydantic objects we can't find/reconstruct
|
||||
# let's return the kwargs dict instead
|
||||
try:
|
||||
return tup[2]
|
||||
except NameError:
|
||||
return
|
||||
elif code == EXT_PYDANTIC_V2:
|
||||
try:
|
||||
tup = msgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, strict_map_key=False
|
||||
tup = ormsgpack.unpackb(
|
||||
data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
|
||||
)
|
||||
# module, name, kwargs, method
|
||||
cls = getattr(importlib.import_module(tup[0]), tup[1])
|
||||
@@ -499,9 +521,99 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
|
||||
return cls(**tup[2])
|
||||
except Exception:
|
||||
return cls.model_construct(**tup[2])
|
||||
except Exception:
|
||||
# for pydantic objects we can't find/reconstruct
|
||||
# let's return the kwargs dict instead
|
||||
try:
|
||||
return tup[2]
|
||||
except NameError:
|
||||
return
|
||||
|
||||
|
||||
def _msgpack_ext_hook_to_json(code: int, data: bytes) -> Any:
|
||||
if code == EXT_CONSTRUCTOR_SINGLE_ARG:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data,
|
||||
ext_hook=_msgpack_ext_hook_to_json,
|
||||
option=ormsgpack.OPT_NON_STR_KEYS,
|
||||
)
|
||||
if tup[0] == "uuid" and tup[1] == "UUID":
|
||||
hex_ = tup[2]
|
||||
return (
|
||||
f"{hex_[:8]}-{hex_[8:12]}-{hex_[12:16]}-{hex_[16:20]}-{hex_[20:]}"
|
||||
)
|
||||
# module, name, arg
|
||||
return tup[2]
|
||||
except Exception:
|
||||
return
|
||||
elif code == EXT_CONSTRUCTOR_POS_ARGS:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data,
|
||||
ext_hook=_msgpack_ext_hook_to_json,
|
||||
option=ormsgpack.OPT_NON_STR_KEYS,
|
||||
)
|
||||
# module, name, args
|
||||
return tup[2]
|
||||
except Exception:
|
||||
return
|
||||
elif code == EXT_CONSTRUCTOR_KW_ARGS:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data,
|
||||
ext_hook=_msgpack_ext_hook_to_json,
|
||||
option=ormsgpack.OPT_NON_STR_KEYS,
|
||||
)
|
||||
# module, name, args
|
||||
return tup[2]
|
||||
except Exception:
|
||||
return
|
||||
elif code == EXT_METHOD_SINGLE_ARG:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data,
|
||||
ext_hook=_msgpack_ext_hook_to_json,
|
||||
option=ormsgpack.OPT_NON_STR_KEYS,
|
||||
)
|
||||
# module, name, arg, method
|
||||
return tup[2]
|
||||
except Exception:
|
||||
return
|
||||
elif code == EXT_PYDANTIC_V1:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data,
|
||||
ext_hook=_msgpack_ext_hook_to_json,
|
||||
option=ormsgpack.OPT_NON_STR_KEYS,
|
||||
)
|
||||
# module, name, kwargs
|
||||
return tup[2]
|
||||
except Exception:
|
||||
# for pydantic objects we can't find/reconstruct
|
||||
# let's return the kwargs dict instead
|
||||
return
|
||||
elif code == EXT_PYDANTIC_V2:
|
||||
try:
|
||||
tup = ormsgpack.unpackb(
|
||||
data,
|
||||
ext_hook=_msgpack_ext_hook_to_json,
|
||||
option=ormsgpack.OPT_NON_STR_KEYS,
|
||||
)
|
||||
# module, name, kwargs, method
|
||||
return tup[2]
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
_option = (
|
||||
ormsgpack.OPT_NON_STR_KEYS
|
||||
| ormsgpack.OPT_PASSTHROUGH_DATACLASS
|
||||
| ormsgpack.OPT_PASSTHROUGH_DATETIME
|
||||
| ormsgpack.OPT_PASSTHROUGH_ENUM
|
||||
| ormsgpack.OPT_PASSTHROUGH_UUID
|
||||
)
|
||||
|
||||
|
||||
def _msgpack_enc(data: Any) -> bytes:
|
||||
return msgpack.packb(data, default=_msgpack_default)
|
||||
return ormsgpack.packb(data, default=_msgpack_default, option=_option)
|
||||
|
||||
@@ -11,9 +11,19 @@ Core types:
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from typing import Any, Iterable, Literal, NamedTuple, Optional, TypedDict, Union, cast
|
||||
from typing import (
|
||||
Any,
|
||||
Iterable,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
TypedDict,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
from langchain_core.embeddings import Embeddings
|
||||
from typing_extensions import override
|
||||
|
||||
from langgraph.store.base.embed import (
|
||||
AEmbeddingsFunc,
|
||||
@@ -24,6 +34,20 @@ from langgraph.store.base.embed import (
|
||||
)
|
||||
|
||||
|
||||
class NotProvided:
|
||||
"""Sentinel singleton."""
|
||||
|
||||
def __bool__(self) -> Literal[False]:
|
||||
return False
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return "NOT_GIVEN"
|
||||
|
||||
|
||||
NOT_PROVIDED = NotProvided()
|
||||
|
||||
|
||||
class Item:
|
||||
"""Represents a stored item with metadata.
|
||||
|
||||
@@ -59,7 +83,7 @@ class Item:
|
||||
else created_at
|
||||
)
|
||||
self.updated_at = (
|
||||
datetime.fromisoformat(cast(str, created_at))
|
||||
datetime.fromisoformat(cast(str, updated_at))
|
||||
if isinstance(updated_at, str)
|
||||
else updated_at
|
||||
)
|
||||
@@ -166,6 +190,13 @@ class GetOp(NamedTuple):
|
||||
"doc456" # For a document
|
||||
```
|
||||
"""
|
||||
refresh_ttl: bool = True
|
||||
"""Whether to refresh TTLs for the returned item.
|
||||
|
||||
If no TTL was specified for the original item(s),
|
||||
or if TTL support is not enabled for your adapter,
|
||||
this argument is ignored.
|
||||
"""
|
||||
|
||||
|
||||
class SearchOp(NamedTuple):
|
||||
@@ -260,6 +291,13 @@ class SearchOp(NamedTuple):
|
||||
- "technical documentation about REST APIs"
|
||||
- "machine learning papers from 2023"
|
||||
"""
|
||||
refresh_ttl: bool = True
|
||||
"""Whether to refresh TTLs for the returned item.
|
||||
|
||||
If no TTL was specified for the original item(s),
|
||||
or if TTL support is not enabled for your adapter,
|
||||
this argument is ignored.
|
||||
"""
|
||||
|
||||
|
||||
# Type representing a namespace path that can include wildcards
|
||||
@@ -463,6 +501,15 @@ class PutOp(NamedTuple):
|
||||
]
|
||||
```
|
||||
"""
|
||||
ttl: Optional[float] = None
|
||||
"""Controls the TTL (time-to-live) for the item in minutes.
|
||||
|
||||
If provided, and if the store you are using supports this feature, the item
|
||||
will expire this many minutes after it was last accessed. The expiration timer
|
||||
refreshes on both read operations (get/search) and write operations (put/update).
|
||||
When the TTL expires, the item will be scheduled for deletion on a best-effort basis.
|
||||
Defaults to None (no expiration).
|
||||
"""
|
||||
|
||||
|
||||
Op = Union[GetOp, SearchOp, PutOp, ListNamespacesOp]
|
||||
@@ -473,6 +520,31 @@ class InvalidNamespaceError(ValueError):
|
||||
"""Provided namespace is invalid."""
|
||||
|
||||
|
||||
class TTLConfig(TypedDict, total=False):
|
||||
"""Configuration for TTL (time-to-live) behavior in the store."""
|
||||
|
||||
refresh_on_read: bool
|
||||
"""Default behavior for refreshing TTLs on read operations (GET and SEARCH).
|
||||
|
||||
If True, TTLs will be refreshed on read operations (get/search) by default.
|
||||
This can be overridden per-operation by explicitly setting refresh_ttl.
|
||||
Defaults to True if not configured.
|
||||
"""
|
||||
default_ttl: Optional[float]
|
||||
"""Default TTL (time-to-live) in minutes for new items.
|
||||
|
||||
If provided, new items will expire after this many minutes after their last access.
|
||||
The expiration timer refreshes on both read and write operations.
|
||||
Defaults to None (no expiration).
|
||||
"""
|
||||
sweep_interval_minutes: Optional[int]
|
||||
"""Interval in minutes between TTL sweep operations.
|
||||
|
||||
If provided, the store will periodically delete expired items based on TTL.
|
||||
Defaults to None (no sweeping).
|
||||
"""
|
||||
|
||||
|
||||
class IndexConfig(TypedDict, total=False):
|
||||
"""Configuration for indexing documents for semantic search in the store.
|
||||
|
||||
@@ -612,8 +684,14 @@ class BaseStore(ABC):
|
||||
by providing an `index` configuration at creation time. Without this
|
||||
configuration, semantic search is disabled and any `index` arguments
|
||||
to storage operations will have no effect.
|
||||
|
||||
Similarly, TTL (time-to-live) support is disabled by default.
|
||||
Subclasses must explicitly set `supports_ttl = True` to enable this feature.
|
||||
"""
|
||||
|
||||
supports_ttl: bool = False
|
||||
ttl_config: Optional[TTLConfig] = None
|
||||
|
||||
__slots__ = ("__weakref__",)
|
||||
|
||||
@abstractmethod
|
||||
@@ -640,17 +718,28 @@ class BaseStore(ABC):
|
||||
The order of results matches the order of input operations.
|
||||
"""
|
||||
|
||||
def get(self, namespace: tuple[str, ...], key: str) -> Optional[Item]:
|
||||
def get(
|
||||
self,
|
||||
namespace: tuple[str, ...],
|
||||
key: str,
|
||||
*,
|
||||
refresh_ttl: Optional[bool] = None,
|
||||
) -> Optional[Item]:
|
||||
"""Retrieve a single item.
|
||||
|
||||
Args:
|
||||
namespace: Hierarchical path for the item.
|
||||
key: Unique identifier within the namespace.
|
||||
refresh_ttl: Whether to refresh TTLs for the returned item.
|
||||
If None (default), uses the store's default refresh_ttl setting.
|
||||
If no TTL is specified, this argument is ignored.
|
||||
|
||||
Returns:
|
||||
The retrieved item or None if not found.
|
||||
"""
|
||||
return self.batch([GetOp(namespace, key)])[0]
|
||||
return self.batch(
|
||||
[GetOp(namespace, str(key), _ensure_refresh(self.ttl_config, refresh_ttl))]
|
||||
)[0]
|
||||
|
||||
def search(
|
||||
self,
|
||||
@@ -661,6 +750,7 @@ class BaseStore(ABC):
|
||||
filter: Optional[dict[str, Any]] = None,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
refresh_ttl: Optional[bool] = None,
|
||||
) -> list[SearchItem]:
|
||||
"""Search for items within a namespace prefix.
|
||||
|
||||
@@ -670,6 +760,8 @@ class BaseStore(ABC):
|
||||
filter: Key-value pairs to filter results.
|
||||
limit: Maximum number of items to return.
|
||||
offset: Number of items to skip before returning results.
|
||||
refresh_ttl: Whether to refresh TTLs for the returned items.
|
||||
If no TTL is specified, this argument is ignored.
|
||||
|
||||
Returns:
|
||||
List of items matching the search criteria.
|
||||
@@ -707,7 +799,18 @@ class BaseStore(ABC):
|
||||
Note: Natural language search support depends on your store implementation
|
||||
and requires proper embedding configuration.
|
||||
"""
|
||||
return self.batch([SearchOp(namespace_prefix, filter, limit, offset, query)])[0]
|
||||
return self.batch(
|
||||
[
|
||||
SearchOp(
|
||||
namespace_prefix,
|
||||
filter,
|
||||
limit,
|
||||
offset,
|
||||
query,
|
||||
_ensure_refresh(self.ttl_config, refresh_ttl),
|
||||
)
|
||||
]
|
||||
)[0]
|
||||
|
||||
def put(
|
||||
self,
|
||||
@@ -715,6 +818,8 @@ class BaseStore(ABC):
|
||||
key: str,
|
||||
value: dict[str, Any],
|
||||
index: Optional[Union[Literal[False], list[str]]] = None,
|
||||
*,
|
||||
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
|
||||
) -> None:
|
||||
"""Store or update an item in the store.
|
||||
|
||||
@@ -735,12 +840,20 @@ class BaseStore(ABC):
|
||||
- Nested fields: "metadata.title"
|
||||
- Array access: "chapters[*].content" (each indexed separately)
|
||||
- Specific indices: "authors[0].name"
|
||||
ttl: Time to live in minutes. Support for this argument depends on your store adapter.
|
||||
If specified, the item will expire after this many minutes from when it was last accessed.
|
||||
None means no expiration. Expired runs will be deleted opportunistically.
|
||||
By default, the expiration timer refreshes on both read operations (get/search)
|
||||
and write operations (put/update), whenever the item is included in the operation.
|
||||
|
||||
Note:
|
||||
Indexing support depends on your store implementation.
|
||||
If you do not initialize the store with indexing capabilities,
|
||||
the `index` parameter will be ignored.
|
||||
|
||||
Similarly, TTL support depends on the specific store implementation.
|
||||
Some implementations may not support expiration of items.
|
||||
|
||||
???+ example "Examples"
|
||||
Store item. Indexing depends on how you configure the store.
|
||||
```python
|
||||
@@ -759,7 +872,22 @@ class BaseStore(ABC):
|
||||
```
|
||||
"""
|
||||
_validate_namespace(namespace)
|
||||
self.batch([PutOp(namespace, key, value, index=index)])
|
||||
if ttl not in (NOT_PROVIDED, None) and not self.supports_ttl:
|
||||
raise NotImplementedError(
|
||||
f"TTL is not supported by {self.__class__.__name__}. "
|
||||
f"Use a store implementation that supports TTL or set ttl=None."
|
||||
)
|
||||
self.batch(
|
||||
[
|
||||
PutOp(
|
||||
namespace,
|
||||
str(key),
|
||||
value,
|
||||
index=index,
|
||||
ttl=_ensure_ttl(self.ttl_config, ttl),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
def delete(self, namespace: tuple[str, ...], key: str) -> None:
|
||||
"""Delete an item.
|
||||
@@ -768,7 +896,7 @@ class BaseStore(ABC):
|
||||
namespace: Hierarchical path for the item.
|
||||
key: Unique identifier within the namespace.
|
||||
"""
|
||||
self.batch([PutOp(namespace, key, None)])
|
||||
self.batch([PutOp(namespace, str(key), None, ttl=None)])
|
||||
|
||||
def list_namespaces(
|
||||
self,
|
||||
@@ -823,7 +951,13 @@ class BaseStore(ABC):
|
||||
)
|
||||
return self.batch([op])[0]
|
||||
|
||||
async def aget(self, namespace: tuple[str, ...], key: str) -> Optional[Item]:
|
||||
async def aget(
|
||||
self,
|
||||
namespace: tuple[str, ...],
|
||||
key: str,
|
||||
*,
|
||||
refresh_ttl: Optional[bool] = None,
|
||||
) -> Optional[Item]:
|
||||
"""Asynchronously retrieve a single item.
|
||||
|
||||
Args:
|
||||
@@ -833,7 +967,17 @@ class BaseStore(ABC):
|
||||
Returns:
|
||||
The retrieved item or None if not found.
|
||||
"""
|
||||
return (await self.abatch([GetOp(namespace, key)]))[0]
|
||||
return (
|
||||
await self.abatch(
|
||||
[
|
||||
GetOp(
|
||||
namespace,
|
||||
str(key),
|
||||
_ensure_refresh(self.ttl_config, refresh_ttl),
|
||||
)
|
||||
]
|
||||
)
|
||||
)[0]
|
||||
|
||||
async def asearch(
|
||||
self,
|
||||
@@ -844,6 +988,7 @@ class BaseStore(ABC):
|
||||
filter: Optional[dict[str, Any]] = None,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
refresh_ttl: Optional[bool] = None,
|
||||
) -> list[SearchItem]:
|
||||
"""Asynchronously search for items within a namespace prefix.
|
||||
|
||||
@@ -853,6 +998,9 @@ class BaseStore(ABC):
|
||||
filter: Key-value pairs to filter results.
|
||||
limit: Maximum number of items to return.
|
||||
offset: Number of items to skip before returning results.
|
||||
refresh_ttl: Whether to refresh TTLs for the returned items.
|
||||
If None (default), uses the store's TTLConfig.refresh_default setting.
|
||||
If TTLConfig is not provided or no TTL is specified, this argument is ignored.
|
||||
|
||||
Returns:
|
||||
List of items matching the search criteria.
|
||||
@@ -892,7 +1040,16 @@ class BaseStore(ABC):
|
||||
"""
|
||||
return (
|
||||
await self.abatch(
|
||||
[SearchOp(namespace_prefix, filter, limit, offset, query)]
|
||||
[
|
||||
SearchOp(
|
||||
namespace_prefix,
|
||||
filter,
|
||||
limit,
|
||||
offset,
|
||||
query,
|
||||
_ensure_refresh(self.ttl_config, refresh_ttl),
|
||||
)
|
||||
]
|
||||
)
|
||||
)[0]
|
||||
|
||||
@@ -902,6 +1059,8 @@ class BaseStore(ABC):
|
||||
key: str,
|
||||
value: dict[str, Any],
|
||||
index: Optional[Union[Literal[False], list[str]]] = None,
|
||||
*,
|
||||
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
|
||||
) -> None:
|
||||
"""Asynchronously store or update an item in the store.
|
||||
|
||||
@@ -922,12 +1081,20 @@ class BaseStore(ABC):
|
||||
- Nested fields: "metadata.title"
|
||||
- Array access: "chapters[*].content" (each indexed separately)
|
||||
- Specific indices: "authors[0].name"
|
||||
ttl: Time to live in minutes. Support for this argument depends on your store adapter.
|
||||
If specified, the item will expire after this many minutes from when it was last accessed.
|
||||
None means no expiration. Expired runs will be deleted opportunistically.
|
||||
By default, the expiration timer refreshes on both read operations (get/search)
|
||||
and write operations (put/update), whenever the item is included in the operation.
|
||||
|
||||
Note:
|
||||
Indexing support depends on your store implementation.
|
||||
If you do not initialize the store with indexing capabilities,
|
||||
the `index` parameter will be ignored.
|
||||
|
||||
Similarly, TTL support depends on the specific store implementation.
|
||||
Some implementations may not support expiration of items.
|
||||
|
||||
???+ example "Examples"
|
||||
Store item. Indexing depends on how you configure the store.
|
||||
```python
|
||||
@@ -954,7 +1121,22 @@ class BaseStore(ABC):
|
||||
```
|
||||
"""
|
||||
_validate_namespace(namespace)
|
||||
await self.abatch([PutOp(namespace, key, value, index=index)])
|
||||
if ttl not in (NOT_PROVIDED, None) and not self.supports_ttl:
|
||||
raise NotImplementedError(
|
||||
f"TTL is not supported by {self.__class__.__name__}. "
|
||||
f"Use a store implementation that supports TTL or set ttl=None."
|
||||
)
|
||||
await self.abatch(
|
||||
[
|
||||
PutOp(
|
||||
namespace,
|
||||
str(key),
|
||||
value,
|
||||
index=index,
|
||||
ttl=_ensure_ttl(self.ttl_config, ttl),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
async def adelete(self, namespace: tuple[str, ...], key: str) -> None:
|
||||
"""Asynchronously delete an item.
|
||||
@@ -963,7 +1145,7 @@ class BaseStore(ABC):
|
||||
namespace: Hierarchical path for the item.
|
||||
key: Unique identifier within the namespace.
|
||||
"""
|
||||
await self.abatch([PutOp(namespace, key, None)])
|
||||
await self.abatch([PutOp(namespace, str(key), None)])
|
||||
|
||||
async def alist_namespaces(
|
||||
self,
|
||||
@@ -1043,6 +1225,27 @@ def _validate_namespace(namespace: tuple[str, ...]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _ensure_refresh(
|
||||
ttl_config: Optional[TTLConfig], refresh_ttl: Optional[bool] = None
|
||||
) -> bool:
|
||||
if refresh_ttl is not None:
|
||||
return refresh_ttl
|
||||
if ttl_config is not None:
|
||||
return ttl_config.get("refresh_on_read", True)
|
||||
return True
|
||||
|
||||
|
||||
def _ensure_ttl(
|
||||
ttl_config: Optional[TTLConfig],
|
||||
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
|
||||
) -> Optional[float]:
|
||||
if ttl is NOT_PROVIDED:
|
||||
if ttl_config:
|
||||
return ttl_config.get("default_ttl")
|
||||
return None
|
||||
return ttl
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BaseStore",
|
||||
"Item",
|
||||
|
||||
@@ -5,17 +5,21 @@ from collections.abc import Iterable
|
||||
from typing import Any, Callable, Literal, Optional, TypeVar, Union
|
||||
|
||||
from langgraph.store.base import (
|
||||
NOT_PROVIDED,
|
||||
BaseStore,
|
||||
GetOp,
|
||||
Item,
|
||||
ListNamespacesOp,
|
||||
MatchCondition,
|
||||
NamespacePath,
|
||||
NotProvided,
|
||||
Op,
|
||||
PutOp,
|
||||
Result,
|
||||
SearchItem,
|
||||
SearchOp,
|
||||
_ensure_refresh,
|
||||
_ensure_ttl,
|
||||
_validate_namespace,
|
||||
)
|
||||
|
||||
@@ -68,10 +72,21 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
self,
|
||||
namespace: tuple[str, ...],
|
||||
key: str,
|
||||
*,
|
||||
refresh_ttl: Optional[bool] = None,
|
||||
) -> Optional[Item]:
|
||||
assert not self._task.done()
|
||||
fut = self._loop.create_future()
|
||||
self._aqueue.put_nowait((fut, GetOp(namespace, key)))
|
||||
self._aqueue.put_nowait(
|
||||
(
|
||||
fut,
|
||||
GetOp(
|
||||
namespace,
|
||||
key,
|
||||
refresh_ttl=_ensure_refresh(self.ttl_config, refresh_ttl),
|
||||
),
|
||||
)
|
||||
)
|
||||
return await fut
|
||||
|
||||
async def asearch(
|
||||
@@ -83,11 +98,22 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
filter: Optional[dict[str, Any]] = None,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
refresh_ttl: Optional[bool] = None,
|
||||
) -> list[SearchItem]:
|
||||
assert not self._task.done()
|
||||
fut = self._loop.create_future()
|
||||
self._aqueue.put_nowait(
|
||||
(fut, SearchOp(namespace_prefix, filter, limit, offset, query))
|
||||
(
|
||||
fut,
|
||||
SearchOp(
|
||||
namespace_prefix,
|
||||
filter,
|
||||
limit,
|
||||
offset,
|
||||
query,
|
||||
refresh_ttl=_ensure_refresh(self.ttl_config, refresh_ttl),
|
||||
),
|
||||
)
|
||||
)
|
||||
return await fut
|
||||
|
||||
@@ -97,11 +123,20 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
key: str,
|
||||
value: dict[str, Any],
|
||||
index: Optional[Union[Literal[False], list[str]]] = None,
|
||||
*,
|
||||
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
|
||||
) -> None:
|
||||
assert not self._task.done()
|
||||
_validate_namespace(namespace)
|
||||
fut = self._loop.create_future()
|
||||
self._aqueue.put_nowait((fut, PutOp(namespace, key, value, index)))
|
||||
self._aqueue.put_nowait(
|
||||
(
|
||||
fut,
|
||||
PutOp(
|
||||
namespace, key, value, index, ttl=_ensure_ttl(self.ttl_config, ttl)
|
||||
),
|
||||
)
|
||||
)
|
||||
return await fut
|
||||
|
||||
async def adelete(
|
||||
@@ -149,9 +184,11 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
self,
|
||||
namespace: tuple[str, ...],
|
||||
key: str,
|
||||
*,
|
||||
refresh_ttl: Optional[bool] = None,
|
||||
) -> Optional[Item]:
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.aget(namespace, key=key), self._loop
|
||||
self.aget(namespace, key=key, refresh_ttl=refresh_ttl), self._loop
|
||||
).result()
|
||||
|
||||
@_check_loop
|
||||
@@ -164,10 +201,16 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
filter: Optional[dict[str, Any]] = None,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
refresh_ttl: Optional[bool] = None,
|
||||
) -> list[SearchItem]:
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.asearch(
|
||||
namespace_prefix, query=query, filter=filter, limit=limit, offset=offset
|
||||
namespace_prefix,
|
||||
query=query,
|
||||
filter=filter,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
refresh_ttl=refresh_ttl,
|
||||
),
|
||||
self._loop,
|
||||
).result()
|
||||
@@ -179,10 +222,19 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
key: str,
|
||||
value: dict[str, Any],
|
||||
index: Optional[Union[Literal[False], list[str]]] = None,
|
||||
*,
|
||||
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
|
||||
) -> None:
|
||||
_validate_namespace(namespace)
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.aput(namespace, key=key, value=value, index=index), self._loop
|
||||
self.aput(
|
||||
namespace,
|
||||
key=key,
|
||||
value=value,
|
||||
index=index,
|
||||
ttl=_ensure_ttl(self.ttl_config, ttl),
|
||||
),
|
||||
self._loop,
|
||||
).result()
|
||||
|
||||
@_check_loop
|
||||
|
||||
Generated
+76
-76
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
@@ -6,6 +6,7 @@ version = "0.7.0"
|
||||
description = "Reusable constraint types to use with typing.Annotated"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
|
||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
||||
@@ -17,6 +18,7 @@ version = "2024.7.4"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
|
||||
{file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
|
||||
@@ -28,6 +30,7 @@ version = "3.3.2"
|
||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||
optional = false
|
||||
python-versions = ">=3.7.0"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
|
||||
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
|
||||
@@ -127,6 +130,7 @@ version = "2.3.0"
|
||||
description = "Codespell"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "codespell-2.3.0-py3-none-any.whl", hash = "sha256:a9c7cef2501c9cfede2110fd6d4e5e62296920efe9abfb84648df866e47f58d1"},
|
||||
{file = "codespell-2.3.0.tar.gz", hash = "sha256:360c7d10f75e65f67bad720af7007e1060a5d395670ec11a7ed1fed9dd17471f"},
|
||||
@@ -144,6 +148,8 @@ version = "0.4.6"
|
||||
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"},
|
||||
@@ -155,6 +161,7 @@ version = "0.6.7"
|
||||
description = "Easily serialize dataclasses to and from JSON."
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a"},
|
||||
{file = "dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0"},
|
||||
@@ -170,6 +177,8 @@ version = "1.2.2"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
|
||||
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
|
||||
@@ -184,6 +193,7 @@ version = "3.7"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
||||
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
||||
@@ -195,6 +205,7 @@ version = "2.0.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
@@ -206,6 +217,7 @@ version = "1.33"
|
||||
description = "Apply JSON-Patches (RFC 6902)"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"},
|
||||
{file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"},
|
||||
@@ -220,6 +232,7 @@ version = "3.0.0"
|
||||
description = "Identify specific nodes in a JSON document (RFC 6901)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"},
|
||||
{file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"},
|
||||
@@ -231,6 +244,7 @@ version = "0.2.38"
|
||||
description = "Building applications with LLMs through composability"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.8.1"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "langchain_core-0.2.38-py3-none-any.whl", hash = "sha256:8a5729bc7e68b4af089af20eff44fe4e7ca21d0e0c87ec21cef7621981fd1a4a"},
|
||||
{file = "langchain_core-0.2.38.tar.gz", hash = "sha256:eb69dbedd344f2ee1f15bcea6c71a05884b867588fadc42d04632e727c1238f3"},
|
||||
@@ -254,6 +268,7 @@ version = "0.1.93"
|
||||
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.8.1"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "langsmith-0.1.93-py3-none-any.whl", hash = "sha256:811210b9d5f108f36431bd7b997eb9476a9ecf5a2abd7ddbb606c1cdcf0f43ce"},
|
||||
{file = "langsmith-0.1.93.tar.gz", hash = "sha256:285b6ad3a54f50fa8eb97b5f600acc57d0e37e139dd8cf2111a117d0435ba9b4"},
|
||||
@@ -273,6 +288,7 @@ version = "3.21.3"
|
||||
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "marshmallow-3.21.3-py3-none-any.whl", hash = "sha256:86ce7fb914aa865001a4b2092c4c2872d13bc347f3d42673272cabfdbad386f1"},
|
||||
{file = "marshmallow-3.21.3.tar.gz", hash = "sha256:4f57c5e050a54d66361e826f94fba213eb10b67b2fdb02c3e0343ce207ba1662"},
|
||||
@@ -286,85 +302,13 @@ dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"]
|
||||
docs = ["alabaster (==0.7.16)", "autodocsumm (==0.2.12)", "sphinx (==7.3.7)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"]
|
||||
tests = ["pytest", "pytz", "simplejson"]
|
||||
|
||||
[[package]]
|
||||
name = "msgpack"
|
||||
version = "1.1.0"
|
||||
description = "MessagePack serializer"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"},
|
||||
{file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"},
|
||||
{file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"},
|
||||
{file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"},
|
||||
{file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"},
|
||||
{file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec"},
|
||||
{file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96"},
|
||||
{file = "msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870"},
|
||||
{file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7"},
|
||||
{file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb"},
|
||||
{file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f"},
|
||||
{file = "msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b"},
|
||||
{file = "msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd"},
|
||||
{file = "msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325"},
|
||||
{file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.11.0"
|
||||
description = "Optional static typing for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "mypy-1.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3824187c99b893f90c845bab405a585d1ced4ff55421fdf5c84cb7710995229"},
|
||||
{file = "mypy-1.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96f8dbc2c85046c81bcddc246232d500ad729cb720da4e20fce3b542cab91287"},
|
||||
@@ -412,6 +356,7 @@ version = "1.0.0"
|
||||
description = "Type system extensions for programs checked with the mypy type checker."
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||
@@ -423,6 +368,7 @@ version = "3.10.6"
|
||||
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"},
|
||||
{file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"},
|
||||
@@ -479,12 +425,49 @@ files = [
|
||||
{file = "orjson-3.10.6.tar.gz", hash = "sha256:e54b63d0a7c6c54a5f5f726bc93a2078111ef060fec4ecbf34c5db800ca3b3a7"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ormsgpack"
|
||||
version = "1.9.0"
|
||||
description = "Fast, correct Python msgpack library supporting dataclasses, datetimes, and numpy"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "ormsgpack-1.9.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9c7cc221489aaf8bf394225a275edf068f3531529def415a8e6e32d6228ee138"},
|
||||
{file = "ormsgpack-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42a5c5028417e710e5169c77d90b08891299f77ffd87abbb2855ffc62314740a"},
|
||||
{file = "ormsgpack-1.9.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:350fdfac11215234b14d7fb484cf8f3f524eb0e7c6a3614bf878f4d034c1cef2"},
|
||||
{file = "ormsgpack-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebb49ca6d3f8dca7b667397016cb2cab7e6581b1d85b30f2697824479150e31e"},
|
||||
{file = "ormsgpack-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ec9ad897bf00c4933bea519d505b82e20f9e0972bdd458dd1e06d6d5e0b8eec6"},
|
||||
{file = "ormsgpack-1.9.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5b473282dacddf20f03b99971e3fc3691bbeafc6142c8e51e80f137e35147ec9"},
|
||||
{file = "ormsgpack-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84bbd03ebca6efb38cb697e2e24f9ae22feb58ef1e6e664239ae68f4ccb3db76"},
|
||||
{file = "ormsgpack-1.9.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49e7e3612f1930267ddf85e914ba417bf5fa801e4a045acb466fa8a8bf7f8bf8"},
|
||||
{file = "ormsgpack-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da0aa79373e70c8ad32c0a23f410a7d611a13ea4f1e427f501307a487caf0557"},
|
||||
{file = "ormsgpack-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:6dfecbe00e504ccf946fc168ad56d038682fd17592da1be44368ab996fbeae3e"},
|
||||
{file = "ormsgpack-1.9.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6f13a538674ee42764278b418f9e97743401cd3895c7c473d45abd03f650169b"},
|
||||
{file = "ormsgpack-1.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:332d85cbf3775b96b6eacdd0c03758517b530365dfa6e55981190062d840be47"},
|
||||
{file = "ormsgpack-1.9.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4b9de72dc94f73d63047ad40cfdd6e9dd2b28c51e9ccbc72117d5146b4f5fc18"},
|
||||
{file = "ormsgpack-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4577cf304fa4c079092280e9ed4858cd9bd8b1475a803c206a449a3830b499ef"},
|
||||
{file = "ormsgpack-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:32302872cf10e4eccc8437cdaf46ac8e5e56cbb7519734a0b8f8a1ed2cbdfd44"},
|
||||
{file = "ormsgpack-1.9.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6ccbdf412af6c46b3549929d90a960ebe1b45f9b3e6c530774cd29de0846ce4d"},
|
||||
{file = "ormsgpack-1.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a6e113502c002f12f6bcf100eb8c2ccb85d1e75931ede669765ffaf5cc0e69d"},
|
||||
{file = "ormsgpack-1.9.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:afd8bc92bb903fc37ce16921bb522d205ba02b90871dc4edc6fac13ac9226481"},
|
||||
{file = "ormsgpack-1.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:800d918e6bca16d01c382015a4c05b00cabafa7c2070126b7feaefe2cf1437f0"},
|
||||
{file = "ormsgpack-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:305ec6de5fd687b7de0861673e967b4f6474a634b159a3a82e481707308203c9"},
|
||||
{file = "ormsgpack-1.9.0-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ecd28f5e0a07578972c9681034f1a6413ac0d0f016ff09db47dd9a7e8191d57a"},
|
||||
{file = "ormsgpack-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0767bde96b932c70f3e1dd0e82a5c3dd969e2223edd7e8b3303cba1fa38473d1"},
|
||||
{file = "ormsgpack-1.9.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58b7c35bb813bb461b2bf848e99e129d536f6ed47f1d1c49e3de02748fe8554f"},
|
||||
{file = "ormsgpack-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aa6bc3904fbc4e6538e1bb3f2748f5cbc34906597724a3f0b8f578972a21fae"},
|
||||
{file = "ormsgpack-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:09f7b11abc0b493735870f3dea5daf36a147916b0609f394d45373f5ae4b6850"},
|
||||
{file = "ormsgpack-1.9.0.tar.gz", hash = "sha256:015e8e6e74e5a1c2bcb9c25fdd8205cad0e8e2d1d32c6a259615aa189b61b8b4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.1"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
||||
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
||||
@@ -496,6 +479,7 @@ version = "1.5.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
||||
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
||||
@@ -511,6 +495,7 @@ version = "2.8.2"
|
||||
description = "Data validation using Python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"},
|
||||
{file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"},
|
||||
@@ -533,6 +518,7 @@ version = "2.20.1"
|
||||
description = "Core functionality for Pydantic validation and serialization"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"},
|
||||
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"},
|
||||
@@ -634,6 +620,7 @@ version = "7.4.4"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
|
||||
{file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
|
||||
@@ -656,6 +643,7 @@ version = "0.21.2"
|
||||
description = "Pytest support for asyncio"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"},
|
||||
{file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"},
|
||||
@@ -674,6 +662,7 @@ version = "3.14.0"
|
||||
description = "Thin-wrapper around the mock package for easier use with pytest"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"},
|
||||
{file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"},
|
||||
@@ -691,6 +680,7 @@ version = "0.4.2"
|
||||
description = "Automatically rerun your tests on file modifications"
|
||||
optional = false
|
||||
python-versions = "<4.0.0,>=3.7.0"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest_watcher-0.4.2-py3-none-any.whl", hash = "sha256:a43949ba67dd8d7e1fd0de5eea44a999081f0aec9f93b4e744264b4c6a3d9bbe"},
|
||||
{file = "pytest_watcher-0.4.2.tar.gz", hash = "sha256:7b292f025ca19617cd7567c228c6187b5087f2da9e4d2cf6e144e5764a0471b0"},
|
||||
@@ -706,6 +696,7 @@ version = "6.0.1"
|
||||
description = "YAML parser and emitter for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
|
||||
@@ -766,6 +757,7 @@ version = "2.32.3"
|
||||
description = "Python HTTP for Humans."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
|
||||
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
|
||||
@@ -787,6 +779,7 @@ version = "0.6.2"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"},
|
||||
{file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"},
|
||||
@@ -814,6 +807,7 @@ version = "8.5.0"
|
||||
description = "Retry code until it succeeds"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687"},
|
||||
{file = "tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78"},
|
||||
@@ -829,6 +823,8 @@ version = "2.0.1"
|
||||
description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
@@ -840,6 +836,7 @@ version = "4.12.2"
|
||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||
@@ -851,6 +848,7 @@ version = "0.9.0"
|
||||
description = "Runtime inspection utilities for typing module."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"},
|
||||
{file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"},
|
||||
@@ -866,6 +864,7 @@ version = "2.2.2"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
|
||||
{file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
|
||||
@@ -883,6 +882,7 @@ version = "4.0.1"
|
||||
description = "Filesystem events monitoring"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"},
|
||||
{file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"},
|
||||
@@ -922,6 +922,6 @@ files = [
|
||||
watchmedo = ["PyYAML (>=3.10)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
content-hash = "8861f12053a7b4594cd8a218f31b78f861d7e5391017bc9209cc5bccbd6f769c"
|
||||
content-hash = "db4bfc26829b1abd13c9426d74dc26100f36a6777e965a32eed4cc1ae17a8c14"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.16"
|
||||
version = "2.0.22"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -11,7 +11,7 @@ packages = [{ include = "langgraph" }]
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
langchain-core = ">=0.2.38,<0.4"
|
||||
msgpack = "^1.1.0"
|
||||
ormsgpack = "^1.8.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.6.2"
|
||||
|
||||
@@ -15,7 +15,10 @@ from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from pydantic.v1 import SecretStr as SecretStrV1
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
from langgraph.checkpoint.serde.jsonplus import (
|
||||
JsonPlusSerializer,
|
||||
_msgpack_ext_hook_to_json,
|
||||
)
|
||||
from langgraph.store.base import Item
|
||||
|
||||
|
||||
@@ -104,6 +107,7 @@ def test_serde_jsonplus() -> None:
|
||||
"time": current_time,
|
||||
"uid": uid,
|
||||
"timestamp": current_timestamp,
|
||||
"my_rich_dict": {(1, 2, 3): 45},
|
||||
"my_slotted_class": MyDataclassWSlots("bar", 2, InnerDataclass("hello")),
|
||||
"my_dataclass": MyDataclass("foo", 1, InnerDataclass("hello")),
|
||||
"my_enum": MyEnum.FOO,
|
||||
@@ -130,7 +134,7 @@ def test_serde_jsonplus() -> None:
|
||||
key="my-key",
|
||||
namespace=("a", "name", " "),
|
||||
created_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
|
||||
updated_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
|
||||
updated_at=datetime(2024, 9, 24, 17, 29, 11, 128397),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -164,6 +168,113 @@ def test_serde_jsonplus() -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_serde_jsonplus_json_mode() -> None:
|
||||
uid = uuid.UUID(int=1)
|
||||
deque_instance = deque([1, 2, 3])
|
||||
tzn = ZoneInfo("America/New_York")
|
||||
ip4 = IPv4Address("192.168.0.1")
|
||||
current_date = date(2024, 4, 19)
|
||||
current_time = time(23, 4, 57, 51022, timezone.max)
|
||||
current_timestamp = datetime(2024, 4, 19, 23, 4, 57, 51022, timezone.max)
|
||||
|
||||
to_serialize = {
|
||||
"path": pathlib.Path("foo", "bar"),
|
||||
"re": re.compile(r"foo", re.DOTALL),
|
||||
"decimal": Decimal("1.10101"),
|
||||
"set": {1, 2, frozenset({1, 2})},
|
||||
"frozen_set": frozenset({1, 2, 3}),
|
||||
"ip4": ip4,
|
||||
"deque": deque_instance,
|
||||
"tzn": tzn,
|
||||
"date": current_date,
|
||||
"time": current_time,
|
||||
"uid": uid,
|
||||
"timestamp": current_timestamp,
|
||||
"my_slotted_class": MyDataclassWSlots("bar", 2, InnerDataclass("hello")),
|
||||
"my_dataclass": MyDataclass("foo", 1, InnerDataclass("hello")),
|
||||
"my_enum": MyEnum.FOO,
|
||||
"my_pydantic": MyPydantic(foo="foo", bar=1, inner=InnerPydantic(hello="hello")),
|
||||
"my_pydantic_v1": MyPydanticV1(
|
||||
foo="foo", bar=1, inner=InnerPydanticV1(hello="hello")
|
||||
),
|
||||
"my_secret_str": SecretStr("meow"),
|
||||
"my_secret_str_v1": SecretStrV1("meow"),
|
||||
"person": Person(name="foo"),
|
||||
"a_bool": True,
|
||||
"a_none": None,
|
||||
"a_str": "foo",
|
||||
"a_str_nuc": "foo\u0000",
|
||||
"a_str_uc": "foo ⛰️",
|
||||
"a_str_ucuc": "foo \u26f0\ufe0f\u0000",
|
||||
"a_str_ucucuc": "foo \\u26f0\\ufe0f",
|
||||
"an_int": 1,
|
||||
"a_float": 1.1,
|
||||
"a_bytes": b"my bytes",
|
||||
"a_bytearray": bytearray([42]),
|
||||
"my_item": Item(
|
||||
value={},
|
||||
key="my-key",
|
||||
namespace=("a", "name", " "),
|
||||
created_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
|
||||
updated_at=datetime(2024, 9, 24, 17, 29, 11, 128397),
|
||||
),
|
||||
}
|
||||
|
||||
serde = JsonPlusSerializer(__unpack_ext_hook__=_msgpack_ext_hook_to_json)
|
||||
|
||||
dumped = serde.dumps_typed(to_serialize)
|
||||
|
||||
assert dumped[0] == "msgpack"
|
||||
result = serde.loads_typed(dumped)
|
||||
assert result == {
|
||||
"path": ["foo", "bar"],
|
||||
"re": ["foo", 48],
|
||||
"decimal": "1.10101",
|
||||
"set": [1, 2, [1, 2]],
|
||||
"frozen_set": [1, 2, 3],
|
||||
"ip4": "192.168.0.1",
|
||||
"deque": [1, 2, 3],
|
||||
"tzn": "America/New_York",
|
||||
"date": [2024, 4, 19],
|
||||
"time": {
|
||||
"hour": 23,
|
||||
"minute": 4,
|
||||
"second": 57,
|
||||
"microsecond": 51022,
|
||||
"tzinfo": [[0, 86340, 0]],
|
||||
"fold": 0,
|
||||
},
|
||||
"uid": "00000000-0000-0000-0000-000000000001",
|
||||
"timestamp": "2024-04-19T23:04:57.051022+23:59",
|
||||
"my_slotted_class": {"foo": "bar", "bar": 2, "inner": {"hello": "hello"}},
|
||||
"my_dataclass": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
|
||||
"my_enum": "foo",
|
||||
"my_pydantic": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
|
||||
"my_pydantic_v1": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
|
||||
"my_secret_str": "meow",
|
||||
"my_secret_str_v1": "meow",
|
||||
"person": {"name": "foo"},
|
||||
"a_bool": True,
|
||||
"a_none": None,
|
||||
"a_str": "foo",
|
||||
"a_str_nuc": "foo\x00",
|
||||
"a_str_uc": "foo ⛰️",
|
||||
"a_str_ucuc": "foo ⛰️\x00",
|
||||
"a_str_ucucuc": "foo \\u26f0\\ufe0f",
|
||||
"an_int": 1,
|
||||
"a_float": 1.1,
|
||||
"a_bytes": b"my bytes",
|
||||
"a_bytearray": b"*",
|
||||
"my_item": {
|
||||
"namespace": ["a", "name", " "],
|
||||
"key": "my-key",
|
||||
"value": {},
|
||||
"created_at": "2024-09-24T17:29:10.128397",
|
||||
"updated_at": "2024-09-24T17:29:11.128397",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_serde_jsonplus_bytes() -> None:
|
||||
serde = JsonPlusSerializer()
|
||||
|
||||
|
||||
@@ -68,7 +68,9 @@ class TestMemorySaver:
|
||||
},
|
||||
"metadata": {"run_id": "my_run_id"},
|
||||
}
|
||||
self.memory_saver.put(config, self.chkpnt_2, self.metadata_2, {})
|
||||
self.memory_saver.put(
|
||||
config, self.chkpnt_2, self.metadata_2, self.chkpnt_2["channel_versions"]
|
||||
)
|
||||
checkpoint = self.memory_saver.get_tuple(config)
|
||||
assert checkpoint is not None
|
||||
assert checkpoint.metadata == {
|
||||
@@ -80,9 +82,24 @@ class TestMemorySaver:
|
||||
async def test_search(self) -> None:
|
||||
# set up test
|
||||
# save checkpoints
|
||||
self.memory_saver.put(self.config_1, self.chkpnt_1, self.metadata_1, {})
|
||||
self.memory_saver.put(self.config_2, self.chkpnt_2, self.metadata_2, {})
|
||||
self.memory_saver.put(self.config_3, self.chkpnt_3, self.metadata_3, {})
|
||||
self.memory_saver.put(
|
||||
self.config_1,
|
||||
self.chkpnt_1,
|
||||
self.metadata_1,
|
||||
self.chkpnt_1["channel_versions"],
|
||||
)
|
||||
self.memory_saver.put(
|
||||
self.config_2,
|
||||
self.chkpnt_2,
|
||||
self.metadata_2,
|
||||
self.chkpnt_2["channel_versions"],
|
||||
)
|
||||
self.memory_saver.put(
|
||||
self.config_3,
|
||||
self.chkpnt_3,
|
||||
self.metadata_3,
|
||||
self.chkpnt_3["channel_versions"],
|
||||
)
|
||||
|
||||
# call method / assertions
|
||||
query_1 = {"source": "input"} # search by 1 key
|
||||
@@ -129,9 +146,24 @@ class TestMemorySaver:
|
||||
async def test_asearch(self) -> None:
|
||||
# set up test
|
||||
# save checkpoints
|
||||
self.memory_saver.put(self.config_1, self.chkpnt_1, self.metadata_1, {})
|
||||
self.memory_saver.put(self.config_2, self.chkpnt_2, self.metadata_2, {})
|
||||
self.memory_saver.put(self.config_3, self.chkpnt_3, self.metadata_3, {})
|
||||
self.memory_saver.put(
|
||||
self.config_1,
|
||||
self.chkpnt_1,
|
||||
self.metadata_1,
|
||||
self.chkpnt_1["channel_versions"],
|
||||
)
|
||||
self.memory_saver.put(
|
||||
self.config_2,
|
||||
self.chkpnt_2,
|
||||
self.metadata_2,
|
||||
self.chkpnt_2["channel_versions"],
|
||||
)
|
||||
self.memory_saver.put(
|
||||
self.config_3,
|
||||
self.chkpnt_3,
|
||||
self.metadata_3,
|
||||
self.chkpnt_3["channel_versions"],
|
||||
)
|
||||
|
||||
# call method / assertions
|
||||
query_1 = {"source": "input"} # search by 1 key
|
||||
|
||||
@@ -148,8 +148,8 @@ async def test_async_batch_store(mocker: MockerFixture) -> None:
|
||||
assert abatch.call_count == 1
|
||||
assert [tuple(c.args[0]) for c in abatch.call_args_list] == [
|
||||
(
|
||||
GetOp(("a",), "b"),
|
||||
GetOp(("c",), "d"),
|
||||
GetOp(("a",), "b", refresh_ttl=True),
|
||||
GetOp(("c",), "d", refresh_ttl=True),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -467,8 +467,8 @@ async def test_async_batch_store_deduplication(mocker: MockerFixture) -> None:
|
||||
assert len(abatch.call_args_list) == 1
|
||||
ops = list(abatch.call_args_list[0].args[1])
|
||||
assert len(ops) == 2
|
||||
assert GetOp(("test",), "same") in ops
|
||||
assert GetOp(("test",), "different") in ops
|
||||
assert GetOp(("test",), "same", refresh_ttl=True) in ops
|
||||
assert GetOp(("test",), "different", refresh_ttl=True) in ops
|
||||
|
||||
abatch.reset_mock()
|
||||
|
||||
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
.PHONY: test lint format test-integration
|
||||
.PHONY: test lint format test-integration update-schema
|
||||
|
||||
######################
|
||||
# TESTING AND COVERAGE
|
||||
@@ -31,3 +31,6 @@ lint lint_diff lint_package lint_tests:
|
||||
format format_diff:
|
||||
poetry run ruff format $(PYTHON_FILES)
|
||||
poetry run ruff check --select I --fix $(PYTHON_FILES)
|
||||
|
||||
update-schema:
|
||||
poetry run python generate_schema.py
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ The CLI uses a `langgraph.json` configuration file with these key settings:
|
||||
}
|
||||
```
|
||||
|
||||
See the [full documentation](https://langchain-ai.github.io/langgraph/docs/cloud/reference/cli.html) for detailed configuration options.
|
||||
See the [full documentation](https://langchain-ai.github.io/langgraph/cloud/reference/cli/) for detailed configuration options.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to generate a JSON schema for the langgraph-cli Config class.
|
||||
|
||||
This script creates a schema.json file that can be referenced in langgraph.json files
|
||||
to provide IDE autocompletion and validation.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import json
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
import msgspec
|
||||
|
||||
from langgraph_cli.config import (
|
||||
AuthConfig,
|
||||
Config,
|
||||
CorsConfig,
|
||||
HttpConfig,
|
||||
IndexConfig,
|
||||
SecurityConfig,
|
||||
StoreConfig,
|
||||
)
|
||||
|
||||
|
||||
def add_descriptions_to_schema(schema, cls):
|
||||
"""Add docstring descriptions to the schema properties."""
|
||||
if schema.get("description"):
|
||||
schema["description"] = inspect.cleandoc(schema["description"])
|
||||
elif class_doc := inspect.getdoc(cls):
|
||||
schema["description"] = inspect.cleandoc(class_doc)
|
||||
# Get attribute docstrings from the class
|
||||
attr_docs = {}
|
||||
|
||||
# Also check class annotations for docstrings
|
||||
source_lines = inspect.getsourcelines(cls)[0]
|
||||
current_attr = None
|
||||
docstring_lines = []
|
||||
|
||||
for line in source_lines:
|
||||
line = line.strip()
|
||||
|
||||
# Check for attribute definition (TypedDict style)
|
||||
if ":" in line and not line.startswith("#") and not line.startswith('"""'):
|
||||
parts = line.split(":", 1)
|
||||
if len(parts) == 2 and parts[0].strip().isidentifier():
|
||||
# If we were collecting a docstring, save it for the previous attribute
|
||||
if current_attr and docstring_lines:
|
||||
attr_docs[current_attr] = "\n".join(docstring_lines).strip('"')
|
||||
docstring_lines = []
|
||||
|
||||
current_attr = parts[0].strip()
|
||||
|
||||
# Check for docstring after attribute
|
||||
elif line.startswith('"""') and current_attr:
|
||||
# Start or end of a docstring
|
||||
if len(line) > 3 and line.endswith('"""'):
|
||||
# Single line docstring
|
||||
attr_docs[current_attr] = line.strip('"')
|
||||
current_attr = None
|
||||
elif docstring_lines:
|
||||
# End of multi-line docstring
|
||||
docstring_lines.append(line.rstrip('"'))
|
||||
attr_docs[current_attr] = "\n".join(docstring_lines).strip('"')
|
||||
docstring_lines = []
|
||||
current_attr = None
|
||||
else:
|
||||
# Start of multi-line docstring
|
||||
docstring_lines.append(line.lstrip('"'))
|
||||
|
||||
# Continue multi-line docstring
|
||||
elif docstring_lines and current_attr:
|
||||
docstring_lines.append(line.strip('"'))
|
||||
|
||||
# Add the last docstring if there is one
|
||||
if current_attr and docstring_lines:
|
||||
attr_docs[current_attr] = "\n".join(docstring_lines).strip('"')
|
||||
|
||||
# Add descriptions to properties
|
||||
if "properties" in schema:
|
||||
for prop_name, prop_schema in schema["properties"].items():
|
||||
# First try to get from attribute docstrings
|
||||
if prop_name in attr_docs and "description" not in prop_schema:
|
||||
prop_schema["description"] = textwrap.dedent(attr_docs[prop_name])
|
||||
# Fall back to class docstring parsing
|
||||
elif class_doc:
|
||||
for line in class_doc.split("\n"):
|
||||
if line.strip().startswith(
|
||||
f"{prop_name}:"
|
||||
) or line.strip().startswith(f'"{prop_name}"'):
|
||||
description = line.split(":", 1)[1].strip()
|
||||
if description and "description" not in prop_schema:
|
||||
prop_schema["description"] = description
|
||||
break
|
||||
|
||||
# Recursively process nested definitions
|
||||
if "$defs" in schema:
|
||||
for def_name, def_schema in schema["$defs"].items():
|
||||
# Find the class that corresponds to this definition
|
||||
for potential_cls in [
|
||||
Config,
|
||||
StoreConfig,
|
||||
IndexConfig,
|
||||
AuthConfig,
|
||||
SecurityConfig,
|
||||
HttpConfig,
|
||||
CorsConfig,
|
||||
]:
|
||||
if potential_cls.__name__ == def_name:
|
||||
add_descriptions_to_schema(def_schema, potential_cls)
|
||||
break
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
def generate_schema():
|
||||
"""Generate a JSON schema for the Config class using msgspec."""
|
||||
# Generate the basic schema
|
||||
schema = msgspec.json.schema(Config)
|
||||
|
||||
# Add title and description
|
||||
schema["title"] = "LangGraph CLI Configuration"
|
||||
schema["description"] = "Configuration schema for langgraph-cli"
|
||||
|
||||
# Add docstring descriptions
|
||||
schema = add_descriptions_to_schema(schema, Config)
|
||||
|
||||
# Add constraint that only one of python_version or node_version should be specified
|
||||
config_schema = schema["$defs"]["Config"]
|
||||
|
||||
# Create two subschemas: one with python_version and one with node_version
|
||||
# Define properties specific to Python projects
|
||||
python_specific_props = ["python_version", "pip_config_file"]
|
||||
# Define properties specific to Node.js projects
|
||||
node_specific_props = ["node_version"]
|
||||
# Define properties common to both project types
|
||||
common_props = [
|
||||
k
|
||||
for k in config_schema["properties"]
|
||||
if k not in python_specific_props and k not in node_specific_props
|
||||
]
|
||||
|
||||
# Create Python schema with python_version and pip_config_file
|
||||
python_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
# Include Python-specific properties
|
||||
**{k: config_schema["properties"][k].copy() for k in python_specific_props},
|
||||
# Include common properties
|
||||
**{k: config_schema["properties"][k].copy() for k in common_props},
|
||||
},
|
||||
"required": ["dependencies", "graphs"],
|
||||
}
|
||||
|
||||
# Add enum constraint for python_version
|
||||
if "python_version" in python_schema["properties"]:
|
||||
python_schema["properties"]["python_version"]["enum"] = ["3.11", "3.12"]
|
||||
|
||||
# Create Node.js schema with node_version
|
||||
node_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
# Include Node-specific properties
|
||||
**{k: config_schema["properties"][k].copy() for k in node_specific_props},
|
||||
# Include common properties
|
||||
**{k: config_schema["properties"][k].copy() for k in common_props},
|
||||
},
|
||||
"required": ["node_version", "graphs"],
|
||||
}
|
||||
|
||||
# Add enum constraint for node_version
|
||||
if "node_version" in node_schema["properties"]:
|
||||
node_schema["properties"]["node_version"]["anyOf"] = [
|
||||
{"type": "string", "enum": ["20"]},
|
||||
{"type": "null"},
|
||||
]
|
||||
|
||||
# Replace the Config schema with a oneOf constraint
|
||||
config_schema["oneOf"] = [python_schema, node_schema]
|
||||
|
||||
# Remove the properties field as it's now defined in the oneOf subschemas
|
||||
if "properties" in config_schema:
|
||||
del config_schema["properties"]
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
def main():
|
||||
"""Generate the schema and write it to a file."""
|
||||
schema = generate_schema()
|
||||
|
||||
# Add versioning to the schema
|
||||
import importlib.metadata
|
||||
|
||||
try:
|
||||
version = importlib.metadata.version("langgraph_cli").split(".")
|
||||
schema_version = f"v{version[0]}"
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
schema_version = "v1"
|
||||
|
||||
# Add version to schema
|
||||
schema["version"] = schema_version
|
||||
|
||||
config_dir = Path(__file__).parent / "schemas"
|
||||
|
||||
# Create versioned schema file
|
||||
versioned_path = config_dir / f"schema.{schema_version}.json"
|
||||
with open(versioned_path, "w") as f:
|
||||
json.dump(schema, f, indent=2)
|
||||
|
||||
# Also create a latest version
|
||||
latest_path = config_dir / "schema.json"
|
||||
with open(latest_path, "w") as f:
|
||||
json.dump(schema, f, indent=2)
|
||||
|
||||
print(f"Schema written to {versioned_path} and {latest_path}")
|
||||
print(
|
||||
f"You can now add '$schema: https://raw.githubusercontent.com/langchain-ai/langgraph/refs/heads/main/libs/cli/schemas/schema.json'"
|
||||
f" or '$schema: https://raw.githubusercontent.com/langchain-ai/langgraph/refs/heads/main/libs/cli/schemas/schema.{schema_version}.json'"
|
||||
" to your langgraph.json files"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -574,6 +574,12 @@ def dockerfile(save_path: str, config: pathlib.Path, add_docker_compose: bool) -
|
||||
help="Wait for a debugger client to connect to the debug port before starting the server",
|
||||
default=False,
|
||||
)
|
||||
@click.option(
|
||||
"--studio-url",
|
||||
type=str,
|
||||
default=None,
|
||||
help="URL of the LangGraph Studio instance to connect to. Defaults to https://smith.langchain.com",
|
||||
)
|
||||
@cli.command(
|
||||
"dev",
|
||||
help="🏃♀️➡️ Run LangGraph API server in development mode with hot reloading and debugging support",
|
||||
@@ -588,6 +594,7 @@ def dev(
|
||||
no_browser: bool,
|
||||
debug_port: Optional[int],
|
||||
wait_for_client: bool,
|
||||
studio_url: Optional[str],
|
||||
):
|
||||
"""CLI entrypoint for running the LangGraph API server."""
|
||||
try:
|
||||
@@ -651,6 +658,7 @@ def dev(
|
||||
wait_for_client=wait_for_client,
|
||||
auth=config_json.get("auth"),
|
||||
http=config_json.get("http"),
|
||||
studio_url=studio_url,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
import pathlib
|
||||
import textwrap
|
||||
from collections import Counter
|
||||
from typing import NamedTuple, Optional, TypedDict, Union
|
||||
from typing import Any, NamedTuple, Optional, TypedDict, Union
|
||||
|
||||
import click
|
||||
|
||||
@@ -11,13 +11,43 @@ MIN_NODE_VERSION = "20"
|
||||
MIN_PYTHON_VERSION = "3.11"
|
||||
|
||||
|
||||
class TTLConfig(TypedDict, total=False):
|
||||
"""Configuration for TTL (time-to-live) behavior in the store."""
|
||||
|
||||
refresh_on_read: bool
|
||||
"""Default behavior for refreshing TTLs on read operations (GET and SEARCH).
|
||||
|
||||
If True, TTLs will be refreshed on read operations (get/search) by default.
|
||||
This can be overridden per-operation by explicitly setting refresh_ttl.
|
||||
Defaults to True if not configured.
|
||||
"""
|
||||
default_ttl: Optional[float]
|
||||
"""Optional. Default TTL (time-to-live) in minutes for new items.
|
||||
|
||||
If provided, all new items will have this TTL unless explicitly overridden.
|
||||
If omitted, items will have no TTL by default.
|
||||
"""
|
||||
sweep_interval_minutes: Optional[int]
|
||||
"""Optional. Interval in minutes between TTL sweep iterations.
|
||||
|
||||
If provided, the store will periodically delete expired items based on the TTL.
|
||||
If omitted, no automatic sweeping will occur.
|
||||
"""
|
||||
|
||||
|
||||
class IndexConfig(TypedDict, total=False):
|
||||
"""Configuration for indexing documents for semantic search in the store."""
|
||||
"""Configuration for indexing documents for semantic search in the store.
|
||||
|
||||
This governs how text is converted into embeddings and stored for vector-based lookups.
|
||||
"""
|
||||
|
||||
dims: int
|
||||
"""Number of dimensions in the embedding vectors.
|
||||
"""Required. Dimensionality of the embedding vectors you will store.
|
||||
|
||||
Common embedding models have the following dimensions:
|
||||
Must match the output dimension of your selected embedding model or custom embed function.
|
||||
If mismatched, you will likely encounter shape/size errors when inserting or querying vectors.
|
||||
|
||||
Common embedding model output dimensions:
|
||||
- openai:text-embedding-3-large: 3072
|
||||
- openai:text-embedding-3-small: 1536
|
||||
- openai:text-embedding-ada-002: 1536
|
||||
@@ -28,42 +58,130 @@ class IndexConfig(TypedDict, total=False):
|
||||
"""
|
||||
|
||||
embed: str
|
||||
"""Optional model (string) to generate embeddings from text or path to model or function.
|
||||
"""Required. Identifier or reference to the embedding model or a custom embedding function.
|
||||
|
||||
Examples:
|
||||
The format can vary:
|
||||
- "<provider>:<model_name>" for recognized providers (e.g., "openai:text-embedding-3-large")
|
||||
- "path/to/module.py:function_name" for your own local embedding function
|
||||
- "my_custom_embed" if it's a known alias in your system
|
||||
|
||||
Examples:
|
||||
- "openai:text-embedding-3-large"
|
||||
- "cohere:embed-multilingual-v3.0"
|
||||
- "src/app.py:embeddings
|
||||
- "src/app.py:embeddings"
|
||||
|
||||
Note: Must return embeddings of dimension `dims`.
|
||||
"""
|
||||
|
||||
fields: Optional[list[str]]
|
||||
"""Fields to extract text from for embedding generation.
|
||||
"""Optional. List of JSON fields to extract before generating embeddings.
|
||||
|
||||
Defaults to the root ["$"], which embeds the json object as a whole.
|
||||
Defaults to ["$"], which means the entire JSON object is embedded as one piece of text.
|
||||
If you provide multiple fields (e.g. ["title", "content"]), each is extracted and embedded separately,
|
||||
often saving token usage if you only care about certain parts of the data.
|
||||
|
||||
Example:
|
||||
fields=["title", "abstract", "author.biography"]
|
||||
"""
|
||||
|
||||
|
||||
class StoreConfig(TypedDict, total=False):
|
||||
embed: Optional[IndexConfig]
|
||||
"""Configuration for vector embeddings in store."""
|
||||
"""Configuration for the built-in long-term memory store.
|
||||
|
||||
This store can optionally perform semantic search. If you omit `index`,
|
||||
the store will just handle traditional (non-embedded) data without vector lookups.
|
||||
"""
|
||||
|
||||
index: Optional[IndexConfig]
|
||||
"""Optional. Defines the vector-based semantic search configuration.
|
||||
|
||||
If provided, the store will:
|
||||
- Generate embeddings according to `index.embed`
|
||||
- Enforce the embedding dimension given by `index.dims`
|
||||
- Embed only specified JSON fields (if any) from `index.fields`
|
||||
|
||||
If omitted, no vector index is initialized.
|
||||
"""
|
||||
|
||||
ttl: Optional[TTLConfig]
|
||||
"""Optional. Defines the TTL (time-to-live) behavior configuration.
|
||||
|
||||
If provided, the store will apply TTL settings according to the configuration.
|
||||
If omitted, no TTL behavior is configured.
|
||||
"""
|
||||
|
||||
|
||||
class SecurityConfig(TypedDict, total=False):
|
||||
securitySchemes: dict
|
||||
security: list
|
||||
"""Configuration for OpenAPI security definitions and requirements.
|
||||
|
||||
Useful for specifying global or path-level authentication and authorization flows
|
||||
(e.g., OAuth2, API key headers, etc.).
|
||||
"""
|
||||
|
||||
securitySchemes: dict[str, dict[str, Any]]
|
||||
"""Required. Dict describing each security scheme recognized by your OpenAPI spec.
|
||||
|
||||
Keys are scheme names (e.g. "OAuth2", "ApiKeyAuth") and values are their definitions.
|
||||
Example:
|
||||
{
|
||||
"OAuth2": {
|
||||
"type": "oauth2",
|
||||
"flows": {
|
||||
"password": {
|
||||
"tokenUrl": "/token",
|
||||
"scopes": {"read": "Read data", "write": "Write data"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
security: list[dict[str, list[str]]]
|
||||
"""Optional. Global security requirements across all endpoints.
|
||||
|
||||
Each element in the list maps a security scheme (e.g. "OAuth2") to a list of scopes (e.g. ["read", "write"]).
|
||||
Example:
|
||||
[
|
||||
{"OAuth2": ["read", "write"]},
|
||||
{"ApiKeyAuth": []}
|
||||
]
|
||||
"""
|
||||
# path => {method => security}
|
||||
paths: dict[str, dict[str, list]]
|
||||
paths: dict[str, dict[str, list[dict[str, list[str]]]]]
|
||||
"""Optional. Path-specific security overrides.
|
||||
|
||||
Keys are path templates (e.g., "/items/{item_id}"), mapping to:
|
||||
- Keys that are HTTP methods (e.g., "GET", "POST"),
|
||||
- Values are lists of security definitions (just like `security`) for that method.
|
||||
|
||||
Example:
|
||||
{
|
||||
"/private_data": {
|
||||
"GET": [{"OAuth2": ["read"]}],
|
||||
"POST": [{"OAuth2": ["write"]}]
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class AuthConfig(TypedDict, total=False):
|
||||
path: str
|
||||
"""Path to the authentication function in a Python file."""
|
||||
disable_studio_auth: bool
|
||||
"""Whether to disable auth when connecting from the LangSmith Studio."""
|
||||
openapi: SecurityConfig
|
||||
"""The schema to use for updating the openapi spec.
|
||||
"""Configuration for custom authentication logic and how it integrates into the OpenAPI spec."""
|
||||
|
||||
Example:
|
||||
path: str
|
||||
"""Required. Path to an instance of the Auth() class that implements custom authentication.
|
||||
|
||||
Format: "path/to/file.py:my_auth"
|
||||
"""
|
||||
disable_studio_auth: bool
|
||||
"""Optional. Whether to disable LangSmith API-key authentication for requests originating the Studio.
|
||||
|
||||
Defaults to False, meaning that if a particular header is set, the server will verify the `x-api-key` header
|
||||
value is a valid API key for the deployment's workspace. If True, all requests will go through your custom
|
||||
authentication logic, regardless of origin of the request.
|
||||
"""
|
||||
openapi: SecurityConfig
|
||||
"""Required. Detailed security configuration that merges into your deployment's OpenAPI spec.
|
||||
|
||||
Example (OAuth2):
|
||||
{
|
||||
"securitySchemes": {
|
||||
"OAuth2": {
|
||||
@@ -71,88 +189,185 @@ class AuthConfig(TypedDict, total=False):
|
||||
"flows": {
|
||||
"password": {
|
||||
"tokenUrl": "/token",
|
||||
"scopes": {
|
||||
"me": "Read information about the current user",
|
||||
"items": "Access to create and manage items"
|
||||
}
|
||||
"scopes": {"me": "Read user info", "items": "Manage items"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{"OAuth2": ["me"]} # Default security requirement for all endpoints
|
||||
{"OAuth2": ["me"]}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class CorsConfig(TypedDict, total=False):
|
||||
"""Specifies Cross-Origin Resource Sharing (CORS) rules for your server.
|
||||
|
||||
If omitted, defaults are typically very restrictive (often no cross-origin requests).
|
||||
Configure carefully if you want to allow usage from browsers hosted on other domains.
|
||||
"""
|
||||
|
||||
allow_origins: list[str]
|
||||
"""Optional. List of allowed origins (e.g., "https://example.com").
|
||||
|
||||
Default is often an empty list (no external origins).
|
||||
Use "*" only if you trust all origins, as that bypasses most restrictions.
|
||||
"""
|
||||
allow_methods: list[str]
|
||||
"""Optional. HTTP methods permitted for cross-origin requests (e.g. ["GET", "POST"]).
|
||||
|
||||
Default might be ["GET", "POST", "OPTIONS"] depending on your server framework.
|
||||
"""
|
||||
allow_headers: list[str]
|
||||
"""Optional. HTTP headers that can be used in cross-origin requests (e.g. ["Content-Type", "Authorization"])."""
|
||||
allow_credentials: bool
|
||||
"""Optional. If True, cross-origin requests can include credentials (cookies, auth headers).
|
||||
|
||||
Default False to avoid accidentally exposing secured endpoints to untrusted sites.
|
||||
"""
|
||||
allow_origin_regex: str
|
||||
"""Optional. A regex pattern for matching allowed origins, used if you have dynamic subdomains.
|
||||
|
||||
Example: "^https://.*\.mycompany\.com$"
|
||||
"""
|
||||
expose_headers: list[str]
|
||||
"""Optional. List of headers that browsers are allowed to read from the response in cross-origin contexts."""
|
||||
max_age: int
|
||||
"""Optional. How many seconds the browser may cache preflight responses.
|
||||
|
||||
Default might be 600 (10 minutes). Larger values reduce preflight requests but can cause stale configurations.
|
||||
"""
|
||||
|
||||
|
||||
class HttpConfig(TypedDict, total=False):
|
||||
"""Configuration for the built-in HTTP server that powers your deployment's routes and endpoints."""
|
||||
|
||||
app: str
|
||||
"""Import path for a custom Starlette/FastAPI app to mount"""
|
||||
"""Optional. Import path to a custom Starlette/FastAPI application to mount.
|
||||
|
||||
Format: "path/to/module.py:app_var"
|
||||
If provided, it can override or extend the default routes.
|
||||
"""
|
||||
disable_assistants: bool
|
||||
"""Disable /assistants routes"""
|
||||
"""Optional. If True, /assistants routes are removed from the server.
|
||||
|
||||
Default is False (meaning /assistants is enabled).
|
||||
"""
|
||||
disable_threads: bool
|
||||
"""Disable /threads routes"""
|
||||
"""Optional. If True, /threads routes are removed.
|
||||
|
||||
Default is False.
|
||||
"""
|
||||
disable_runs: bool
|
||||
"""Disable /runs routes"""
|
||||
"""Optional. If True, /runs routes are removed.
|
||||
|
||||
Default is False.
|
||||
"""
|
||||
disable_store: bool
|
||||
"""Disable /store routes"""
|
||||
"""Optional. If True, /store routes are removed, disabling direct store interactions via HTTP.
|
||||
|
||||
Default is False.
|
||||
"""
|
||||
disable_meta: bool
|
||||
"""Disable /ok, /info, /metrics, and /docs routes"""
|
||||
"""Optional. If True, all meta endpoints (/ok, /info, /metrics, /docs) are disabled.
|
||||
|
||||
Default is False.
|
||||
"""
|
||||
cors: Optional[CorsConfig]
|
||||
"""Cross-Origin Resource Sharing (CORS) configuration"""
|
||||
"""Optional. Defines CORS restrictions. If omitted, no special rules are set and
|
||||
cross-origin behavior depends on default server settings.
|
||||
"""
|
||||
|
||||
|
||||
class Config(TypedDict, total=False):
|
||||
"""Configuration for langgraph-cli."""
|
||||
"""Top-level config for langgraph-cli or similar deployment tooling."""
|
||||
|
||||
python_version: str
|
||||
"""Python version to use."""
|
||||
"""Optional. Python version in 'major.minor' format (e.g. '3.11').
|
||||
Must be at least 3.11 or greater for this deployment to function properly.
|
||||
"""
|
||||
|
||||
node_version: Optional[str]
|
||||
"""Node.js version to use."""
|
||||
"""Optional. Node.js version as a major version (e.g. '20'), if your deployment needs Node.
|
||||
Must be >= 20 if provided.
|
||||
"""
|
||||
|
||||
pip_config_file: Optional[str]
|
||||
"""Path to a pip configuration file."""
|
||||
"""Optional. Path to a pip config file (e.g., "/etc/pip.conf" or "pip.ini") for controlling
|
||||
package installation (custom indices, credentials, etc.).
|
||||
|
||||
Only relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.
|
||||
"""
|
||||
|
||||
dockerfile_lines: list[str]
|
||||
"""Additional lines to add to the Dockerfile."""
|
||||
"""Optional. Additional Docker instructions that will be appended to your base Dockerfile.
|
||||
|
||||
Useful for installing OS packages, setting environment variables, etc.
|
||||
Example:
|
||||
dockerfile_lines=[
|
||||
"RUN apt-get update && apt-get install -y libmagic-dev",
|
||||
"ENV MY_CUSTOM_VAR=hello_world"
|
||||
]
|
||||
"""
|
||||
|
||||
dependencies: list[str]
|
||||
"""Additional Python dependencies to install."""
|
||||
"""List of Python dependencies to install, either from PyPI or local paths.
|
||||
|
||||
Examples:
|
||||
- "." or "./src" if you have a local Python package
|
||||
- str (aka "anthropic") for a PyPI package
|
||||
- "git+https://github.com/org/repo.git@main" for a Git-based package
|
||||
Defaults to an empty list, meaning no additional packages installed beyond your base environment.
|
||||
"""
|
||||
|
||||
graphs: dict[str, str]
|
||||
"""Mapping of graph names to their definitions."""
|
||||
"""Optional. Named definitions of graphs, each pointing to a Python object.
|
||||
|
||||
|
||||
Graphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context
|
||||
managers that accept a single configuration argument (of type RunnableConfig) and return a pregel object
|
||||
(instance of Stategraph, etc.).
|
||||
|
||||
Keys are graph names, values are "path/to/file.py:object_name".
|
||||
Example:
|
||||
{
|
||||
"mygraph": "graphs/my_graph.py:graph_definition",
|
||||
"anothergraph": "graphs/another.py:get_graph"
|
||||
}
|
||||
"""
|
||||
|
||||
env: Union[dict[str, str], str]
|
||||
"""Environment variables to set.
|
||||
|
||||
If a dictionary is provided, the keys are environment variable names
|
||||
and the values are the corresponding environment variable values.
|
||||
|
||||
If a string is provided, it is interpreted as a path to a file containing
|
||||
environment variables in the format KEY=VALUE, with one environment variable
|
||||
per line.
|
||||
"""Optional. Environment variables to set for your deployment.
|
||||
|
||||
- If given as a dict, keys are variable names and values are their values.
|
||||
- If given as a string, it must be a path to a file containing lines in KEY=VALUE format.
|
||||
|
||||
Example as a dict:
|
||||
env={"API_TOKEN": "abc123", "DEBUG": "true"}
|
||||
Example as a file path:
|
||||
env=".env"
|
||||
"""
|
||||
|
||||
store: Optional[StoreConfig]
|
||||
"""Configuration for vector embeddings in store."""
|
||||
"""Optional. Configuration for the built-in long-term memory store, including semantic search indexing.
|
||||
|
||||
If omitted, no vector index is set up (the object store will still be present, however).
|
||||
"""
|
||||
|
||||
auth: Optional[AuthConfig]
|
||||
"""Configuration for authentication."""
|
||||
"""Optional. Custom authentication config, including the path to your Python auth logic and
|
||||
the OpenAPI security definitions it uses.
|
||||
"""
|
||||
|
||||
http: Optional[HttpConfig]
|
||||
"""Configuration for HTTP server."""
|
||||
"""Optional. Configuration for the built-in HTTP server, controlling which custom routes are exposed
|
||||
and how cross-origin requests are handled.
|
||||
"""
|
||||
|
||||
ui: Optional[dict[str, str]]
|
||||
"""Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.
|
||||
"""
|
||||
|
||||
|
||||
def _parse_version(version_str: str) -> tuple[int, int]:
|
||||
@@ -189,6 +404,7 @@ def validate_config(config: Config) -> Config:
|
||||
"store": config.get("store"),
|
||||
"auth": config.get("auth"),
|
||||
"http": config.get("http"),
|
||||
"ui": config.get("ui"),
|
||||
}
|
||||
if config.get("node_version")
|
||||
else {
|
||||
@@ -201,6 +417,7 @@ def validate_config(config: Config) -> Config:
|
||||
"store": config.get("store"),
|
||||
"auth": config.get("auth"),
|
||||
"http": config.get("http"),
|
||||
"ui": config.get("ui"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -687,9 +904,11 @@ def python_config_to_docker(
|
||||
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"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}"
|
||||
(
|
||||
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)}'
|
||||
@@ -724,13 +943,15 @@ RUN set -ex && \\
|
||||
)
|
||||
|
||||
local_pkgs_str = os.linesep.join(
|
||||
f"""# -- Adding local package {relpath} --
|
||||
(
|
||||
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} --
|
||||
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()
|
||||
)
|
||||
|
||||
@@ -845,6 +1066,7 @@ ADD . {faux_path}
|
||||
RUN cd {faux_path} && {install_cmd}
|
||||
{env_additional_config}
|
||||
ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
|
||||
{f"ENV LANGGRAPH_UI='{json.dumps(config['ui'])}'" if config.get("ui") else ""}
|
||||
|
||||
WORKDIR {faux_path}
|
||||
|
||||
|
||||
Generated
+71
-19
@@ -535,42 +535,42 @@ langgraph-sdk = ">=0.1.42,<0.2.0"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-api"
|
||||
version = "0.0.27"
|
||||
version = "0.0.32"
|
||||
description = ""
|
||||
optional = true
|
||||
python-versions = "<4.0,>=3.11.0"
|
||||
files = [
|
||||
{file = "langgraph_api-0.0.27-py3-none-any.whl", hash = "sha256:9b21742238b15b8db9c2d3fd760a670332c8897d0bcbbd9d82e43b6ac15a7937"},
|
||||
{file = "langgraph_api-0.0.27.tar.gz", hash = "sha256:c21eb2b7fe3b93998379f7b13ad7d23b3ef06ab821b008c6b12b954acfb587ec"},
|
||||
{file = "langgraph_api-0.0.32-py3-none-any.whl", hash = "sha256:7990cedc65f784813aba867c5bde3fdfae3fa4588baef1aa346cbeac7c3aebf1"},
|
||||
{file = "langgraph_api-0.0.32.tar.gz", hash = "sha256:6f5b698ad8d136b73c2c53bcfa30670e9244a318b08b5e9cf00a707ea57c058c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = ">=43.0.3,<44.0.0"
|
||||
httpx = ">=0.25.0"
|
||||
jsonschema-rs = ">=0.20.0,<0.21.0"
|
||||
jsonschema-rs = ">=0.20.0,<0.30"
|
||||
langchain-core = ">=0.2.38,<0.4.0"
|
||||
langgraph = ">=0.2.56,<0.4.0"
|
||||
langgraph-checkpoint = ">=2.0.15,<3.0"
|
||||
langgraph-sdk = ">=0.1.53,<0.2.0"
|
||||
langgraph-checkpoint = ">=2.0.21,<3.0"
|
||||
langgraph-sdk = ">=0.1.58,<0.2.0"
|
||||
langsmith = ">=0.1.63,<0.4.0"
|
||||
orjson = ">=3.9.7"
|
||||
pyjwt = ">=2.9.0,<3.0.0"
|
||||
sse-starlette = ">=2.1.0,<2.2.0"
|
||||
starlette = ">=0.38.6"
|
||||
structlog = ">=23.1.0,<24.0.0"
|
||||
structlog = ">=24.1.0,<26"
|
||||
tenacity = ">=8.0.0"
|
||||
uvicorn = ">=0.26.0"
|
||||
watchfiles = ">=0.13"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.16"
|
||||
version = "2.0.21"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = true
|
||||
python-versions = "<4.0.0,>=3.9.0"
|
||||
files = [
|
||||
{file = "langgraph_checkpoint-2.0.16-py3-none-any.whl", hash = "sha256:dfab51076a6eddb5f9e146cfe1b977e3dd6419168b2afa23ff3f4e47973bf06f"},
|
||||
{file = "langgraph_checkpoint-2.0.16.tar.gz", hash = "sha256:49ba8cfa12b2aae845ccc3b1fbd1d7a8d3a6c4a2e387ab3a92fca40dd3d4baa5"},
|
||||
{file = "langgraph_checkpoint-2.0.21-py3-none-any.whl", hash = "sha256:ca89c2090cd9729f83f9782226935dc5ff9fe7756c24936f484ccb0ce367f87b"},
|
||||
{file = "langgraph_checkpoint-2.0.21.tar.gz", hash = "sha256:52beeb6dc1bd8c487b8315466cab271093b65eb97f54a0942dfe105cd20b237f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -594,13 +594,13 @@ langgraph-checkpoint = ">=2.0.10,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.53"
|
||||
version = "0.1.58"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
optional = true
|
||||
python-versions = "<4.0.0,>=3.9.0"
|
||||
files = [
|
||||
{file = "langgraph_sdk-0.1.53-py3-none-any.whl", hash = "sha256:4fab62caad73661ffe4c3ababedcd0d7bfaaba986bee4416b9c28948458a3af5"},
|
||||
{file = "langgraph_sdk-0.1.53.tar.gz", hash = "sha256:12906ed965905fa27e0c28d9fa07dc6fd89e6895ff321ff049fdf3965d057cc4"},
|
||||
{file = "langgraph_sdk-0.1.58-py3-none-any.whl", hash = "sha256:65f88cf5582da0c316714dc475126fa03c5f74d72bc0b9221dd42649de8e23d4"},
|
||||
{file = "langgraph_sdk-0.1.58.tar.gz", hash = "sha256:ef8b0e4c08af8c7efd3919497879c87a3627806b51e4ba5e8b06e0717e3d44cd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -707,6 +707,58 @@ files = [
|
||||
{file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "msgspec"
|
||||
version = "0.19.0"
|
||||
description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "msgspec-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d8dd848ee7ca7c8153462557655570156c2be94e79acec3561cf379581343259"},
|
||||
{file = "msgspec-0.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0553bbc77662e5708fe66aa75e7bd3e4b0f209709c48b299afd791d711a93c36"},
|
||||
{file = "msgspec-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe2c4bf29bf4e89790b3117470dea2c20b59932772483082c468b990d45fb947"},
|
||||
{file = "msgspec-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e87ecfa9795ee5214861eab8326b0e75475c2e68a384002aa135ea2a27d909"},
|
||||
{file = "msgspec-0.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3c4ec642689da44618f68c90855a10edbc6ac3ff7c1d94395446c65a776e712a"},
|
||||
{file = "msgspec-0.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2719647625320b60e2d8af06b35f5b12d4f4d281db30a15a1df22adb2295f633"},
|
||||
{file = "msgspec-0.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:695b832d0091edd86eeb535cd39e45f3919f48d997685f7ac31acb15e0a2ed90"},
|
||||
{file = "msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e"},
|
||||
{file = "msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551"},
|
||||
{file = "msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7"},
|
||||
{file = "msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011"},
|
||||
{file = "msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063"},
|
||||
{file = "msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716"},
|
||||
{file = "msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c"},
|
||||
{file = "msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f"},
|
||||
{file = "msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2"},
|
||||
{file = "msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12"},
|
||||
{file = "msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc"},
|
||||
{file = "msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c"},
|
||||
{file = "msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537"},
|
||||
{file = "msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0"},
|
||||
{file = "msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86"},
|
||||
{file = "msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314"},
|
||||
{file = "msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e"},
|
||||
{file = "msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5"},
|
||||
{file = "msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9"},
|
||||
{file = "msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327"},
|
||||
{file = "msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f"},
|
||||
{file = "msgspec-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15c1e86fff77184c20a2932cd9742bf33fe23125fa3fcf332df9ad2f7d483044"},
|
||||
{file = "msgspec-0.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3b5541b2b3294e5ffabe31a09d604e23a88533ace36ac288fa32a420aa38d229"},
|
||||
{file = "msgspec-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f5c043ace7962ef188746e83b99faaa9e3e699ab857ca3f367b309c8e2c6b12"},
|
||||
{file = "msgspec-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca06aa08e39bf57e39a258e1996474f84d0dd8130d486c00bec26d797b8c5446"},
|
||||
{file = "msgspec-0.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e695dad6897896e9384cf5e2687d9ae9feaef50e802f93602d35458e20d1fb19"},
|
||||
{file = "msgspec-0.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3be5c02e1fee57b54130316a08fe40cca53af92999a302a6054cd451700ea7db"},
|
||||
{file = "msgspec-0.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:0684573a821be3c749912acf5848cce78af4298345cb2d7a8b8948a0a5a27cfe"},
|
||||
{file = "msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["attrs", "coverage", "eval-type-backport", "furo", "ipython", "msgpack", "mypy", "pre-commit", "pyright", "pytest", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "tomli", "tomli_w"]
|
||||
doc = ["furo", "ipython", "sphinx", "sphinx-copybutton", "sphinx-design"]
|
||||
test = ["attrs", "eval-type-backport", "msgpack", "pytest", "pyyaml", "tomli", "tomli_w"]
|
||||
toml = ["tomli", "tomli_w"]
|
||||
yaml = ["pyyaml"]
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.15.0"
|
||||
@@ -1305,18 +1357,18 @@ full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart
|
||||
|
||||
[[package]]
|
||||
name = "structlog"
|
||||
version = "23.3.0"
|
||||
version = "25.2.0"
|
||||
description = "Structured Logging for Python"
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "structlog-23.3.0-py3-none-any.whl", hash = "sha256:d6922a88ceabef5b13b9eda9c4043624924f60edbb00397f4d193bd754cde60a"},
|
||||
{file = "structlog-23.3.0.tar.gz", hash = "sha256:24b42b914ac6bc4a4e6f716e82ac70d7fb1e8c3b1035a765591953bfc37101a5"},
|
||||
{file = "structlog-25.2.0-py3-none-any.whl", hash = "sha256:0fecea2e345d5d491b72f3db2e5fcd6393abfc8cd06a4851f21fcd4d1a99f437"},
|
||||
{file = "structlog-25.2.0.tar.gz", hash = "sha256:d9f9776944207d1035b8b26072b9b140c63702fd7aa57c2f85d28ab701bd8e92"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["structlog[tests,typing]"]
|
||||
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "sphinxext-opengraph", "twisted"]
|
||||
dev = ["freezegun (>=0.2.8)", "mypy (>=1.4)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "rich", "simplejson", "twisted"]
|
||||
docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "sphinxext-opengraph", "twisted"]
|
||||
tests = ["freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"]
|
||||
typing = ["mypy (>=1.4)", "rich", "twisted"]
|
||||
|
||||
@@ -1665,4 +1717,4 @@ inmem = ["langgraph-api", "python-dotenv"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
content-hash = "04a0b0e203ae00f30cca7e454c632be0b41cbb4820d0c05315a346d514b5f98e"
|
||||
content-hash = "f5aa4d66f9c0b98b8321a70a82387dc6e5f3a3a7ecedd87ac00d6415199038f9"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-cli"
|
||||
version = "0.1.74"
|
||||
version = "0.1.79"
|
||||
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.27,<0.1.0", optional = true, python = ">=3.11,<4.0" }
|
||||
langgraph-api = { version = ">=0.0.32,<0.1.0", optional = true, python = ">=3.11,<4.0" }
|
||||
python-dotenv = { version = ">=0.8.0", optional = true }
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
@@ -25,6 +25,7 @@ pytest-asyncio = "^0.21.1"
|
||||
pytest-mock = "^3.11.1"
|
||||
pytest-watch = "^4.2.0"
|
||||
mypy = "^1.10.0"
|
||||
msgspec = "^0.19.0"
|
||||
|
||||
[tool.poetry.extras]
|
||||
inmem = ["langgraph-api", "python-dotenv"]
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
{
|
||||
"$ref": "#/$defs/Config",
|
||||
"$defs": {
|
||||
"Config": {
|
||||
"title": "Config",
|
||||
"description": "Top-level config for langgraph-cli or similar deployment tooling.",
|
||||
"type": "object",
|
||||
"required": [],
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"python_version": {
|
||||
"type": "string",
|
||||
"description": "Optional. Python version in 'major.minor' format (e.g. '3.11').\nMust be at least 3.11 or greater for this deployment to function properly.\n",
|
||||
"enum": [
|
||||
"3.11",
|
||||
"3.12"
|
||||
]
|
||||
},
|
||||
"pip_config_file": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Path to a pip config file (e.g., \"/etc/pip.conf\" or \"pip.ini\") for controlling\npackage installation (custom indices, credentials, etc.).\n\nOnly relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.\n"
|
||||
},
|
||||
"auth": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/AuthConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Custom authentication config, including the path to your Python auth logic and\nthe OpenAPI security definitions it uses.\n"
|
||||
},
|
||||
"dependencies": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of Python dependencies to install, either from PyPI or local paths.\n"
|
||||
},
|
||||
"dockerfile_lines": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional. Additional Docker instructions that will be appended to your base Dockerfile.\n\nUseful for installing OS packages, setting environment variables, etc."
|
||||
},
|
||||
"env": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Environment variables to set for your deployment.\n\n- If given as a dict, keys are variable names and values are their values.\n- If given as a string, it must be a path to a file containing lines in KEY=VALUE format.\n\nenv=\".env\n"
|
||||
},
|
||||
"graphs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n"
|
||||
},
|
||||
"http": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/HttpConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Configuration for the built-in HTTP server, controlling which custom routes are exposed\nand how cross-origin requests are handled.\n"
|
||||
},
|
||||
"store": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/StoreConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Configuration for the built-in long-term memory store, including semantic search indexing.\n\nIf omitted, no vector index is set up (the object store will still be present, however).\n"
|
||||
},
|
||||
"ui": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"dependencies",
|
||||
"graphs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"node_version": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"20"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Node.js version as a major version (e.g. '20'), if your deployment needs Node.\nMust be >= 20 if provided.\n"
|
||||
},
|
||||
"auth": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/AuthConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Custom authentication config, including the path to your Python auth logic and\nthe OpenAPI security definitions it uses.\n"
|
||||
},
|
||||
"dependencies": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of Python dependencies to install, either from PyPI or local paths.\n"
|
||||
},
|
||||
"dockerfile_lines": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional. Additional Docker instructions that will be appended to your base Dockerfile.\n\nUseful for installing OS packages, setting environment variables, etc."
|
||||
},
|
||||
"env": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Environment variables to set for your deployment.\n\n- If given as a dict, keys are variable names and values are their values.\n- If given as a string, it must be a path to a file containing lines in KEY=VALUE format.\n\nenv=\".env\n"
|
||||
},
|
||||
"graphs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n"
|
||||
},
|
||||
"http": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/HttpConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Configuration for the built-in HTTP server, controlling which custom routes are exposed\nand how cross-origin requests are handled.\n"
|
||||
},
|
||||
"store": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/StoreConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Configuration for the built-in long-term memory store, including semantic search indexing.\n\nIf omitted, no vector index is set up (the object store will still be present, however).\n"
|
||||
},
|
||||
"ui": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"node_version",
|
||||
"graphs"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"AuthConfig": {
|
||||
"title": "AuthConfig",
|
||||
"description": "Configuration for custom authentication logic and how it integrates into the OpenAPI spec.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"disable_studio_auth": {
|
||||
"type": "boolean",
|
||||
"description": "Optional. Whether to disable LangSmith API-key authentication for requests originating the Studio.\n\nDefaults to False, meaning that if a particular header is set, the server will verify the `x-api-key` header\nvalue is a valid API key for the deployment's workspace. If True, all requests will go through your custom\nauthentication logic, regardless of origin of the request.\n"
|
||||
},
|
||||
"openapi": {
|
||||
"$ref": "#/$defs/SecurityConfig",
|
||||
"description": "Required. Detailed security configuration that merges into your deployment's OpenAPI spec.\n\n{\n}\n}\n}\n},\n]\n}\n"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Required. Path to an instance of the Auth() class that implements custom authentication.\n"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"SecurityConfig": {
|
||||
"title": "SecurityConfig",
|
||||
"description": "Configuration for OpenAPI security definitions and requirements.\n\nUseful for specifying global or path-level authentication and authorization flows\n(e.g., OAuth2, API key headers, etc.).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"paths": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Optional. Path-specific security overrides.\n\n- Keys that are HTTP methods (e.g., \"GET\", \"POST\"),\n- Values are lists of security definitions (just like `security`) for that method.\n"
|
||||
},
|
||||
"security": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Optional. Global security requirements across all endpoints.\n\nEach element in the list maps a security scheme (e.g. \"OAuth2\") to a list of scopes (e.g. [\"read\", \"write\"])."
|
||||
},
|
||||
"securitySchemes": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object"
|
||||
},
|
||||
"description": "Required. Dict describing each security scheme recognized by your OpenAPI spec.\n\nKeys are scheme names (e.g. \"OAuth2\", \"ApiKeyAuth\") and values are their definitions."
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"HttpConfig": {
|
||||
"title": "HttpConfig",
|
||||
"description": "Configuration for the built-in HTTP server that powers your deployment's routes and endpoints.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"app": {
|
||||
"type": "string",
|
||||
"description": "Optional. Import path to a custom Starlette/FastAPI application to mount.\n"
|
||||
},
|
||||
"cors": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CorsConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Defines CORS restrictions. If omitted, no special rules are set and\ncross-origin behavior depends on default server settings.\n"
|
||||
},
|
||||
"disable_assistants": {
|
||||
"type": "boolean",
|
||||
"description": "Optional. If True, /assistants routes are removed from the server.\n\nDefault is False (meaning /assistants is enabled).\n"
|
||||
},
|
||||
"disable_meta": {
|
||||
"type": "boolean",
|
||||
"description": "Optional. If True, all meta endpoints (/ok, /info, /metrics, /docs) are disabled.\n\nDefault is False.\n"
|
||||
},
|
||||
"disable_runs": {
|
||||
"type": "boolean",
|
||||
"description": "Optional. If True, /runs routes are removed.\n\nDefault is False.\n"
|
||||
},
|
||||
"disable_store": {
|
||||
"type": "boolean",
|
||||
"description": "Optional. If True, /store routes are removed, disabling direct store interactions via HTTP.\n\nDefault is False.\n"
|
||||
},
|
||||
"disable_threads": {
|
||||
"type": "boolean",
|
||||
"description": "Optional. If True, /threads routes are removed.\n\nDefault is False.\n"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"CorsConfig": {
|
||||
"title": "CorsConfig",
|
||||
"description": "Specifies Cross-Origin Resource Sharing (CORS) rules for your server.\n\nIf omitted, defaults are typically very restrictive (often no cross-origin requests).\nConfigure carefully if you want to allow usage from browsers hosted on other domains.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow_credentials": {
|
||||
"type": "boolean",
|
||||
"description": "Optional. If True, cross-origin requests can include credentials (cookies, auth headers).\n\nDefault False to avoid accidentally exposing secured endpoints to untrusted sites.\n"
|
||||
},
|
||||
"allow_headers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional. HTTP headers that can be used in cross-origin requests (e.g. [\"Content-Type\", \"Authorization\"])."
|
||||
},
|
||||
"allow_methods": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional. HTTP methods permitted for cross-origin requests (e.g. [\"GET\", \"POST\"]).\n\nDefault might be [\"GET\", \"POST\", \"OPTIONS\"] depending on your server framework.\n"
|
||||
},
|
||||
"allow_origin_regex": {
|
||||
"type": "string",
|
||||
"description": "Optional. A regex pattern for matching allowed origins, used if you have dynamic subdomains.\n"
|
||||
},
|
||||
"allow_origins": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional. List of allowed origins (e.g., \"https://example.com\").\n\nDefault is often an empty list (no external origins).\nUse \"*\" only if you trust all origins, as that bypasses most restrictions.\n"
|
||||
},
|
||||
"expose_headers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional. List of headers that browsers are allowed to read from the response in cross-origin contexts."
|
||||
},
|
||||
"max_age": {
|
||||
"type": "integer",
|
||||
"description": "Optional. How many seconds the browser may cache preflight responses.\n\nDefault might be 600 (10 minutes). Larger values reduce preflight requests but can cause stale configurations.\n"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"StoreConfig": {
|
||||
"title": "StoreConfig",
|
||||
"description": "Configuration for the built-in long-term memory store.\n\nThis store can optionally perform semantic search. If you omit `index`,\nthe store will just handle traditional (non-embedded) data without vector lookups.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"index": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/IndexConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Defines the vector-based semantic search configuration.\n\n- Generate embeddings according to `index.embed`\n- Enforce the embedding dimension given by `index.dims`\n- Embed only specified JSON fields (if any) from `index.fields`\n\nIf omitted, no vector index is initialized.\n"
|
||||
},
|
||||
"ttl": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/TTLConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Defines the TTL (time-to-live) behavior configuration.\n\nIf provided, the store will apply TTL settings according to the configuration.\nIf omitted, no TTL behavior is configured.\n"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"IndexConfig": {
|
||||
"title": "IndexConfig",
|
||||
"description": "Configuration for indexing documents for semantic search in the store.\n\nThis governs how text is converted into embeddings and stored for vector-based lookups.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dims": {
|
||||
"type": "integer",
|
||||
"description": "Required. Dimensionality of the embedding vectors you will store.\n\nMust match the output dimension of your selected embedding model or custom embed function.\nIf mismatched, you will likely encounter shape/size errors when inserting or querying vectors.\n\n"
|
||||
},
|
||||
"embed": {
|
||||
"type": "string",
|
||||
"description": "Required. Identifier or reference to the embedding model or a custom embedding function.\n\n- \"my_custom_embed\" if it's a known alias in your system\n"
|
||||
},
|
||||
"fields": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. List of JSON fields to extract before generating embeddings.\n\nDefaults to [\"$\"], which means the entire JSON object is embedded as one piece of text.\nIf you provide multiple fields (e.g. [\"title\", \"content\"]), each is extracted and embedded separately,\noften saving token usage if you only care about certain parts of the data.\n"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"TTLConfig": {
|
||||
"title": "TTLConfig",
|
||||
"description": "Configuration for TTL (time-to-live) behavior in the store.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default_ttl": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"refresh_on_read": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sweep_interval_minutes": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
},
|
||||
"title": "LangGraph CLI Configuration",
|
||||
"description": "Configuration schema for langgraph-cli",
|
||||
"version": "v0"
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
{
|
||||
"$ref": "#/$defs/Config",
|
||||
"$defs": {
|
||||
"Config": {
|
||||
"title": "Config",
|
||||
"description": "Top-level config for langgraph-cli or similar deployment tooling.",
|
||||
"type": "object",
|
||||
"required": [],
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"python_version": {
|
||||
"type": "string",
|
||||
"description": "Optional. Python version in 'major.minor' format (e.g. '3.11').\nMust be at least 3.11 or greater for this deployment to function properly.\n",
|
||||
"enum": [
|
||||
"3.11",
|
||||
"3.12"
|
||||
]
|
||||
},
|
||||
"pip_config_file": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Path to a pip config file (e.g., \"/etc/pip.conf\" or \"pip.ini\") for controlling\npackage installation (custom indices, credentials, etc.).\n\nOnly relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.\n"
|
||||
},
|
||||
"auth": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/AuthConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Custom authentication config, including the path to your Python auth logic and\nthe OpenAPI security definitions it uses.\n"
|
||||
},
|
||||
"dependencies": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of Python dependencies to install, either from PyPI or local paths.\n"
|
||||
},
|
||||
"dockerfile_lines": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional. Additional Docker instructions that will be appended to your base Dockerfile.\n\nUseful for installing OS packages, setting environment variables, etc."
|
||||
},
|
||||
"env": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Environment variables to set for your deployment.\n\n- If given as a dict, keys are variable names and values are their values.\n- If given as a string, it must be a path to a file containing lines in KEY=VALUE format.\n\nenv=\".env\n"
|
||||
},
|
||||
"graphs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n"
|
||||
},
|
||||
"http": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/HttpConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Configuration for the built-in HTTP server, controlling which custom routes are exposed\nand how cross-origin requests are handled.\n"
|
||||
},
|
||||
"store": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/StoreConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Configuration for the built-in long-term memory store, including semantic search indexing.\n\nIf omitted, no vector index is set up (the object store will still be present, however).\n"
|
||||
},
|
||||
"ui": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"dependencies",
|
||||
"graphs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"node_version": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"20"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Node.js version as a major version (e.g. '20'), if your deployment needs Node.\nMust be >= 20 if provided.\n"
|
||||
},
|
||||
"auth": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/AuthConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Custom authentication config, including the path to your Python auth logic and\nthe OpenAPI security definitions it uses.\n"
|
||||
},
|
||||
"dependencies": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of Python dependencies to install, either from PyPI or local paths.\n"
|
||||
},
|
||||
"dockerfile_lines": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional. Additional Docker instructions that will be appended to your base Dockerfile.\n\nUseful for installing OS packages, setting environment variables, etc."
|
||||
},
|
||||
"env": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Environment variables to set for your deployment.\n\n- If given as a dict, keys are variable names and values are their values.\n- If given as a string, it must be a path to a file containing lines in KEY=VALUE format.\n\nenv=\".env\n"
|
||||
},
|
||||
"graphs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n"
|
||||
},
|
||||
"http": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/HttpConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Configuration for the built-in HTTP server, controlling which custom routes are exposed\nand how cross-origin requests are handled.\n"
|
||||
},
|
||||
"store": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/StoreConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Configuration for the built-in long-term memory store, including semantic search indexing.\n\nIf omitted, no vector index is set up (the object store will still be present, however).\n"
|
||||
},
|
||||
"ui": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"node_version",
|
||||
"graphs"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"AuthConfig": {
|
||||
"title": "AuthConfig",
|
||||
"description": "Configuration for custom authentication logic and how it integrates into the OpenAPI spec.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"disable_studio_auth": {
|
||||
"type": "boolean",
|
||||
"description": "Optional. Whether to disable LangSmith API-key authentication for requests originating the Studio.\n\nDefaults to False, meaning that if a particular header is set, the server will verify the `x-api-key` header\nvalue is a valid API key for the deployment's workspace. If True, all requests will go through your custom\nauthentication logic, regardless of origin of the request.\n"
|
||||
},
|
||||
"openapi": {
|
||||
"$ref": "#/$defs/SecurityConfig",
|
||||
"description": "Required. Detailed security configuration that merges into your deployment's OpenAPI spec.\n\n{\n}\n}\n}\n},\n]\n}\n"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Required. Path to an instance of the Auth() class that implements custom authentication.\n"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"SecurityConfig": {
|
||||
"title": "SecurityConfig",
|
||||
"description": "Configuration for OpenAPI security definitions and requirements.\n\nUseful for specifying global or path-level authentication and authorization flows\n(e.g., OAuth2, API key headers, etc.).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"paths": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Optional. Path-specific security overrides.\n\n- Keys that are HTTP methods (e.g., \"GET\", \"POST\"),\n- Values are lists of security definitions (just like `security`) for that method.\n"
|
||||
},
|
||||
"security": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Optional. Global security requirements across all endpoints.\n\nEach element in the list maps a security scheme (e.g. \"OAuth2\") to a list of scopes (e.g. [\"read\", \"write\"])."
|
||||
},
|
||||
"securitySchemes": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object"
|
||||
},
|
||||
"description": "Required. Dict describing each security scheme recognized by your OpenAPI spec.\n\nKeys are scheme names (e.g. \"OAuth2\", \"ApiKeyAuth\") and values are their definitions."
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"HttpConfig": {
|
||||
"title": "HttpConfig",
|
||||
"description": "Configuration for the built-in HTTP server that powers your deployment's routes and endpoints.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"app": {
|
||||
"type": "string",
|
||||
"description": "Optional. Import path to a custom Starlette/FastAPI application to mount.\n"
|
||||
},
|
||||
"cors": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CorsConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Defines CORS restrictions. If omitted, no special rules are set and\ncross-origin behavior depends on default server settings.\n"
|
||||
},
|
||||
"disable_assistants": {
|
||||
"type": "boolean",
|
||||
"description": "Optional. If True, /assistants routes are removed from the server.\n\nDefault is False (meaning /assistants is enabled).\n"
|
||||
},
|
||||
"disable_meta": {
|
||||
"type": "boolean",
|
||||
"description": "Optional. If True, all meta endpoints (/ok, /info, /metrics, /docs) are disabled.\n\nDefault is False.\n"
|
||||
},
|
||||
"disable_runs": {
|
||||
"type": "boolean",
|
||||
"description": "Optional. If True, /runs routes are removed.\n\nDefault is False.\n"
|
||||
},
|
||||
"disable_store": {
|
||||
"type": "boolean",
|
||||
"description": "Optional. If True, /store routes are removed, disabling direct store interactions via HTTP.\n\nDefault is False.\n"
|
||||
},
|
||||
"disable_threads": {
|
||||
"type": "boolean",
|
||||
"description": "Optional. If True, /threads routes are removed.\n\nDefault is False.\n"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"CorsConfig": {
|
||||
"title": "CorsConfig",
|
||||
"description": "Specifies Cross-Origin Resource Sharing (CORS) rules for your server.\n\nIf omitted, defaults are typically very restrictive (often no cross-origin requests).\nConfigure carefully if you want to allow usage from browsers hosted on other domains.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow_credentials": {
|
||||
"type": "boolean",
|
||||
"description": "Optional. If True, cross-origin requests can include credentials (cookies, auth headers).\n\nDefault False to avoid accidentally exposing secured endpoints to untrusted sites.\n"
|
||||
},
|
||||
"allow_headers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional. HTTP headers that can be used in cross-origin requests (e.g. [\"Content-Type\", \"Authorization\"])."
|
||||
},
|
||||
"allow_methods": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional. HTTP methods permitted for cross-origin requests (e.g. [\"GET\", \"POST\"]).\n\nDefault might be [\"GET\", \"POST\", \"OPTIONS\"] depending on your server framework.\n"
|
||||
},
|
||||
"allow_origin_regex": {
|
||||
"type": "string",
|
||||
"description": "Optional. A regex pattern for matching allowed origins, used if you have dynamic subdomains.\n"
|
||||
},
|
||||
"allow_origins": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional. List of allowed origins (e.g., \"https://example.com\").\n\nDefault is often an empty list (no external origins).\nUse \"*\" only if you trust all origins, as that bypasses most restrictions.\n"
|
||||
},
|
||||
"expose_headers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional. List of headers that browsers are allowed to read from the response in cross-origin contexts."
|
||||
},
|
||||
"max_age": {
|
||||
"type": "integer",
|
||||
"description": "Optional. How many seconds the browser may cache preflight responses.\n\nDefault might be 600 (10 minutes). Larger values reduce preflight requests but can cause stale configurations.\n"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"StoreConfig": {
|
||||
"title": "StoreConfig",
|
||||
"description": "Configuration for the built-in long-term memory store.\n\nThis store can optionally perform semantic search. If you omit `index`,\nthe store will just handle traditional (non-embedded) data without vector lookups.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"index": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/IndexConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Defines the vector-based semantic search configuration.\n\n- Generate embeddings according to `index.embed`\n- Enforce the embedding dimension given by `index.dims`\n- Embed only specified JSON fields (if any) from `index.fields`\n\nIf omitted, no vector index is initialized.\n"
|
||||
},
|
||||
"ttl": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/TTLConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Defines the TTL (time-to-live) behavior configuration.\n\nIf provided, the store will apply TTL settings according to the configuration.\nIf omitted, no TTL behavior is configured.\n"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"IndexConfig": {
|
||||
"title": "IndexConfig",
|
||||
"description": "Configuration for indexing documents for semantic search in the store.\n\nThis governs how text is converted into embeddings and stored for vector-based lookups.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dims": {
|
||||
"type": "integer",
|
||||
"description": "Required. Dimensionality of the embedding vectors you will store.\n\nMust match the output dimension of your selected embedding model or custom embed function.\nIf mismatched, you will likely encounter shape/size errors when inserting or querying vectors.\n\n"
|
||||
},
|
||||
"embed": {
|
||||
"type": "string",
|
||||
"description": "Required. Identifier or reference to the embedding model or a custom embedding function.\n\n- \"my_custom_embed\" if it's a known alias in your system\n"
|
||||
},
|
||||
"fields": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. List of JSON fields to extract before generating embeddings.\n\nDefaults to [\"$\"], which means the entire JSON object is embedded as one piece of text.\nIf you provide multiple fields (e.g. [\"title\", \"content\"]), each is extracted and embedded separately,\noften saving token usage if you only care about certain parts of the data.\n"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"TTLConfig": {
|
||||
"title": "TTLConfig",
|
||||
"description": "Configuration for TTL (time-to-live) behavior in the store.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default_ttl": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"refresh_on_read": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sweep_interval_minutes": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
},
|
||||
"title": "LangGraph CLI Configuration",
|
||||
"description": "Configuration schema for langgraph-cli",
|
||||
"version": "v0"
|
||||
}
|
||||
@@ -33,6 +33,7 @@ def test_validate_config():
|
||||
"store": None,
|
||||
"auth": None,
|
||||
"http": None,
|
||||
"ui": None,
|
||||
**expected_config,
|
||||
}
|
||||
actual_config = validate_config(expected_config)
|
||||
@@ -52,6 +53,7 @@ def test_validate_config():
|
||||
"store": None,
|
||||
"auth": None,
|
||||
"http": None,
|
||||
"ui": None,
|
||||
}
|
||||
actual_config = validate_config(expected_config)
|
||||
assert actual_config == expected_config
|
||||
@@ -467,6 +469,7 @@ def test_config_to_docker_nodejs():
|
||||
"node_version": "20",
|
||||
"graphs": graphs,
|
||||
"dockerfile_lines": ["ARG meow", "ARG foo"],
|
||||
"ui": {"agent": "./graphs/agent.ui.jsx"},
|
||||
}
|
||||
),
|
||||
"langchain/langgraphjs-api",
|
||||
@@ -477,6 +480,7 @@ ARG foo
|
||||
ADD . /deps/unit_tests
|
||||
RUN cd /deps/unit_tests && npm i
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}'
|
||||
ENV LANGGRAPH_UI='{"agent": "./graphs/agent.ui.jsx"}'
|
||||
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"""
|
||||
|
||||
|
||||
@@ -58,9 +58,11 @@ WORKERS ?= auto
|
||||
XDIST_ARGS := $(if $(WORKERS),-n $(WORKERS) --dist worksteal,)
|
||||
MAXFAIL ?=
|
||||
MAXFAIL_ARGS := $(if $(MAXFAIL),--maxfail $(MAXFAIL),)
|
||||
# Add an '-x' if xdist is enabled
|
||||
XDIST_ARGS := $(if $(WORKERS),-x $(XDIST_ARGS),)
|
||||
|
||||
test_watch:
|
||||
make start-postgres && poetry run ptw . -- --ff -vv -x $(XDIST_ARGS) $(MAXFAIL_ARGS) --snapshot-update --tb short $(TEST); \
|
||||
make start-postgres && poetry run ptw . -- --ff -vv $(XDIST_ARGS) $(MAXFAIL_ARGS) --snapshot-update --tb short $(TEST); \
|
||||
EXIT_CODE=$$?; \
|
||||
make stop-postgres; \
|
||||
exit $$EXIT_CODE
|
||||
|
||||
+47
-299
@@ -1,339 +1,87 @@
|
||||
# 🦜🕸️LangGraph
|
||||
<picture class="github-only">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://langchain-ai.github.io/langgraph/static/wordmark_dark.svg">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://langchain-ai.github.io/langgraph/static/wordmark_light.svg">
|
||||
<img alt="LangGraph Logo" src="https://langchain-ai.github.io/langgraph/static/wordmark_dark.svg" width="80%">
|
||||
</picture>
|
||||
|
||||

|
||||
<div>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
[](https://pypi.org/project/langgraph/)
|
||||
[](https://pepy.tech/project/langgraph)
|
||||
[](https://github.com/langchain-ai/langgraph/issues)
|
||||
[](https://langchain-ai.github.io/langgraph/)
|
||||
|
||||
⚡ Building language agents as graphs ⚡
|
||||
|
||||
> [!NOTE]
|
||||
> Looking for the JS version? See the [JS repo](https://github.com/langchain-ai/langgraphjs) and the [JS docs](https://langchain-ai.github.io/langgraphjs/).
|
||||
|
||||
## Overview
|
||||
LangGraph — used by Replit, Uber, LinkedIn, GitLab and more — is a low-level orchestration framework for building controllable agents. While langchain provides integrations and composable components to streamline LLM application development, the LangGraph library enables agent orchestration — offering customizable architectures, long-term memory, and human-in-the-loop to reliably handle complex tasks.
|
||||
|
||||
[LangGraph](https://langchain-ai.github.io/langgraph/) is a library for building
|
||||
stateful, multi-actor applications with LLMs, used to create agent and multi-agent
|
||||
workflows. Check out an introductory tutorial [here](https://langchain-ai.github.io/langgraph/tutorials/introduction/).
|
||||
|
||||
|
||||
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
|
||||
|
||||
### Why use LangGraph?
|
||||
|
||||
LangGraph powers [production-grade agents](https://www.langchain.com/built-with-langgraph), trusted by Linkedin, Uber, Klarna, GitLab, and many more. LangGraph provides fine-grained control over both the flow and state of your agent applications. It implements a central [persistence layer](https://langchain-ai.github.io/langgraph/concepts/persistence/), enabling features that are common to most agent architectures:
|
||||
|
||||
- **Memory**: LangGraph persists arbitrary aspects of your application's state,
|
||||
supporting memory of conversations and other updates within and across user
|
||||
interactions;
|
||||
- **Human-in-the-loop**: Because state is checkpointed, execution can be interrupted
|
||||
and resumed, allowing for decisions, validation, and corrections at key stages via
|
||||
human input.
|
||||
|
||||
Standardizing these components allows individuals and teams to focus on the behavior
|
||||
of their agent, instead of its supporting infrastructure.
|
||||
|
||||
Through [LangGraph Platform](#langgraph-platform), LangGraph also provides tooling for
|
||||
the development, deployment, debugging, and monitoring of your applications.
|
||||
|
||||
LangGraph integrates seamlessly with
|
||||
[LangChain](https://python.langchain.com/docs/introduction/) and
|
||||
[LangSmith](https://docs.smith.langchain.com/) (but does not require them).
|
||||
|
||||
To learn more about LangGraph, check out our first LangChain Academy
|
||||
course, *Introduction to LangGraph*, available for free
|
||||
[here](https://academy.langchain.com/courses/intro-to-langgraph).
|
||||
|
||||
### LangGraph Platform
|
||||
|
||||
[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform) is infrastructure for deploying LangGraph agents. It is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework. The LangGraph Platform consists of several components that work together to support the development, deployment, debugging, and monitoring of LangGraph applications: [LangGraph Server](https://langchain-ai.github.io/langgraph/concepts/langgraph_server) (APIs), [LangGraph SDKs](https://langchain-ai.github.io/langgraph/concepts/sdk) (clients for the APIs), [LangGraph CLI](https://langchain-ai.github.io/langgraph/concepts/langgraph_cli) (command line tool for building the server), and [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio) (UI/debugger).
|
||||
|
||||
See deployment options [here](https://langchain-ai.github.io/langgraph/concepts/deployment_options/)
|
||||
(includes a free tier).
|
||||
|
||||
Here are some common issues that arise in complex deployments, which LangGraph Platform addresses:
|
||||
|
||||
- **Streaming support**: LangGraph Server provides [multiple streaming modes](https://langchain-ai.github.io/langgraph/concepts/streaming) optimized for various application needs
|
||||
- **Background runs**: Runs agents asynchronously in the background
|
||||
- **Support for long running agents**: Infrastructure that can handle long running processes
|
||||
- **[Double texting](https://langchain-ai.github.io/langgraph/concepts/double_texting)**: Handle the case where you get two messages from the user before the agent can respond
|
||||
- **Handle burstiness**: Task queue for ensuring requests are handled consistently without loss, even under heavy loads
|
||||
|
||||
## Installation
|
||||
|
||||
```shell
|
||||
```bash
|
||||
pip install -U langgraph
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
Let's build a tool-calling [ReAct-style](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#react-implementation) agent that uses a search tool!
|
||||
|
||||
```shell
|
||||
pip install langchain-anthropic
|
||||
```
|
||||
|
||||
```shell
|
||||
export ANTHROPIC_API_KEY=sk-...
|
||||
```
|
||||
|
||||
Optionally, we can set up [LangSmith](https://docs.smith.langchain.com/) for best-in-class observability.
|
||||
|
||||
```shell
|
||||
export LANGSMITH_TRACING=true
|
||||
export LANGSMITH_API_KEY=lsv2_sk_...
|
||||
```
|
||||
|
||||
The simplest way to create a tool-calling agent in LangGraph is to use `create_react_agent`:
|
||||
|
||||
<details open>
|
||||
<summary>High-level implementation</summary>
|
||||
To learn more about how to use LangGraph, check out [the docs](https://langchain-ai.github.io/langgraph/). We show a simple example below of how to create a ReAct agent.
|
||||
|
||||
```python
|
||||
# This code depends on pip install langchain[anthropic]
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.tools import tool
|
||||
|
||||
# Define the tools for the agent to use
|
||||
@tool
|
||||
def search(query: str):
|
||||
"""Call to surf the web."""
|
||||
# This is a placeholder, but don't tell the LLM that...
|
||||
if "sf" in query.lower() or "san francisco" in query.lower():
|
||||
return "It's 60 degrees and foggy."
|
||||
return "It's 90 degrees and sunny."
|
||||
|
||||
|
||||
tools = [search]
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0)
|
||||
|
||||
# Initialize memory to persist state between graph runs
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
app = create_react_agent(model, tools, checkpointer=checkpointer)
|
||||
|
||||
# Use the agent
|
||||
final_state = app.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
config={"configurable": {"thread_id": 42}}
|
||||
agent = create_react_agent("anthropic:claude-3-7-sonnet-latest", tools=[search])
|
||||
agent.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]}
|
||||
)
|
||||
final_state["messages"][-1].content
|
||||
```
|
||||
```
|
||||
"Based on the search results, I can tell you that the current weather in San Francisco is:\n\nTemperature: 60 degrees Fahrenheit\nConditions: Foggy\n\nSan Francisco is known for its microclimates and frequent fog, especially during the summer months. The temperature of 60°F (about 15.5°C) is quite typical for the city, which tends to have mild temperatures year-round. The fog, often referred to as "Karl the Fog" by locals, is a characteristic feature of San Francisco\'s weather, particularly in the mornings and evenings.\n\nIs there anything else you\'d like to know about the weather in San Francisco or any other location?"
|
||||
```
|
||||
|
||||
Now when we pass the same <code>"thread_id"</code>, the conversation context is retained via the saved state (i.e. stored list of messages)
|
||||
## Why use LangGraph?
|
||||
|
||||
```python
|
||||
final_state = app.invoke(
|
||||
{"messages": [{"role": "user", "content": "what about ny"}]},
|
||||
config={"configurable": {"thread_id": 42}}
|
||||
)
|
||||
final_state["messages"][-1].content
|
||||
```
|
||||
LangGraph is built for developers who want to build powerful, adaptable AI agents. Developers choose LangGraph for:
|
||||
|
||||
```
|
||||
"Based on the search results, I can tell you that the current weather in New York City is:\n\nTemperature: 90 degrees Fahrenheit (approximately 32.2 degrees Celsius)\nConditions: Sunny\n\nThis weather is quite different from what we just saw in San Francisco. New York is experiencing much warmer temperatures right now. Here are a few points to note:\n\n1. The temperature of 90°F is quite hot, typical of summer weather in New York City.\n2. The sunny conditions suggest clear skies, which is great for outdoor activities but also means it might feel even hotter due to direct sunlight.\n3. This kind of weather in New York often comes with high humidity, which can make it feel even warmer than the actual temperature suggests.\n\nIt's interesting to see the stark contrast between San Francisco's mild, foggy weather and New York's hot, sunny conditions. This difference illustrates how varied weather can be across different parts of the United States, even on the same day.\n\nIs there anything else you'd like to know about the weather in New York or any other location?"
|
||||
```
|
||||
</details>
|
||||
- **Reliability and controllability.** Steer agent actions with moderation checks and human-in-the-loop approvals. LangGraph persists context for long-running workflows, keeping your agents on course.
|
||||
- **Low-level and extensible.** Build custom agents with fully descriptive, low-level primitives – free from rigid abstractions that limit customization. Design scalable multi-agent systems, with each agent serving a specific role tailored to your use case.
|
||||
- **First-class streaming support.** With token-by-token streaming and streaming of intermediate steps, LangGraph gives users clear visibility into agent reasoning and actions as they unfold in real time.
|
||||
|
||||
> [!TIP]
|
||||
> LangGraph is a **low-level** framework that allows you to implement any custom agent
|
||||
architectures. Click on the low-level implementation below to see how to implement a
|
||||
tool-calling agent from scratch.
|
||||
LangGraph is trusted in production and powering agents for companies like:
|
||||
|
||||
<details>
|
||||
<summary>Low-level implementation</summary>
|
||||
- [Klarna](https://blog.langchain.dev/customers-klarna/): Customer support bot for 85 million active users
|
||||
- [Elastic](https://www.elastic.co/blog/elastic-security-generative-ai-features): Security AI assistant for threat detection
|
||||
- [Uber](https://dpe.org/sessions/ty-smith-adam-huda/this-year-in-ubers-ai-driven-developer-productivity-revolution/): Automated unit test generation
|
||||
- [Replit](https://www.langchain.com/breakoutagents/replit): Code generation
|
||||
- And many more ([see list here](https://www.langchain.com/built-with-langgraph))
|
||||
|
||||
```python
|
||||
from typing import Literal
|
||||
## LangGraph’s ecosystem
|
||||
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.tools import tool
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.graph import END, START, StateGraph, MessagesState
|
||||
from langgraph.prebuilt import ToolNode
|
||||
While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:
|
||||
|
||||
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
|
||||
- [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
|
||||
|
||||
# Define the tools for the agent to use
|
||||
@tool
|
||||
def search(query: str):
|
||||
"""Call to surf the web."""
|
||||
# This is a placeholder, but don't tell the LLM that...
|
||||
if "sf" in query.lower() or "san francisco" in query.lower():
|
||||
return "It's 60 degrees and foggy."
|
||||
return "It's 90 degrees and sunny."
|
||||
## Pairing with LangGraph Platform
|
||||
|
||||
While LangGraph is our open-source agent orchestration framework, enterprises that need scalable agent deployment can benefit from [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/).
|
||||
|
||||
tools = [search]
|
||||
LangGraph Platform can help engineering teams:
|
||||
|
||||
tool_node = ToolNode(tools)
|
||||
- **Accelerate agent development**: Quickly create agent UXs with configurable templates and [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/) for visualizing and debugging agent interactions.
|
||||
- **Deploy seamlessly**: We handle the complexity of deploying your agent. LangGraph Platform includes robust APIs for memory, threads, and cron jobs plus auto-scaling task queues & servers.
|
||||
- **Centralize agent management & reusability**: Discover, reuse, and manage agents across the organization. Business users can also modify agents without coding.
|
||||
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0).bind_tools(tools)
|
||||
## Additional resources
|
||||
|
||||
# Define the function that determines whether to continue or not
|
||||
def should_continue(state: MessagesState) -> Literal["tools", END]:
|
||||
messages = state['messages']
|
||||
last_message = messages[-1]
|
||||
# If the LLM makes a tool call, then we route to the "tools" node
|
||||
if last_message.tool_calls:
|
||||
return "tools"
|
||||
# Otherwise, we stop (reply to the user)
|
||||
return END
|
||||
- [LangChain Academy](https://academy.langchain.com/courses/intro-to-langgraph): Learn the basics of LangGraph in our free, structured course.
|
||||
- [Tutorials](https://langchain-ai.github.io/langgraph/tutorials/): Simple walkthroughs with guided examples on getting started with LangGraph.
|
||||
- [Templates](https://langchain-ai.github.io/langgraph/concepts/template_applications/): Pre-built reference apps for common agentic workflows (e.g. ReAct agent, memory, retrieval etc.) that can be cloned and adapted.
|
||||
- [How-to Guides](https://langchain-ai.github.io/langgraph/how-tos/): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
|
||||
- [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components.
|
||||
- [Built with LangGraph](https://www.langchain.com/built-with-langgraph): Hear how industry leaders use LangGraph to ship powerful, production-ready AI applications.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
# Define the function that calls the model
|
||||
def call_model(state: MessagesState):
|
||||
messages = state['messages']
|
||||
response = model.invoke(messages)
|
||||
# We return a list, because this will get added to the existing list
|
||||
return {"messages": [response]}
|
||||
|
||||
|
||||
# Define a new graph
|
||||
workflow = StateGraph(MessagesState)
|
||||
|
||||
# Define the two nodes we will cycle between
|
||||
workflow.add_node("agent", call_model)
|
||||
workflow.add_node("tools", tool_node)
|
||||
|
||||
# Set the entrypoint as `agent`
|
||||
# This means that this node is the first one called
|
||||
workflow.add_edge(START, "agent")
|
||||
|
||||
# We now add a conditional edge
|
||||
workflow.add_conditional_edges(
|
||||
# First, we define the start node. We use `agent`.
|
||||
# This means these are the edges taken after the `agent` node is called.
|
||||
"agent",
|
||||
# Next, we pass in the function that will determine which node is called next.
|
||||
should_continue,
|
||||
)
|
||||
|
||||
# We now add a normal edge from `tools` to `agent`.
|
||||
# This means that after `tools` is called, `agent` node is called next.
|
||||
workflow.add_edge("tools", 'agent')
|
||||
|
||||
# Initialize memory to persist state between graph runs
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
# Finally, we compile it!
|
||||
# This compiles it into a LangChain Runnable,
|
||||
# meaning you can use it as you would any other runnable.
|
||||
# Note that we're (optionally) passing the memory when compiling the graph
|
||||
app = workflow.compile(checkpointer=checkpointer)
|
||||
|
||||
# Use the agent
|
||||
final_state = app.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
config={"configurable": {"thread_id": 42}}
|
||||
)
|
||||
final_state["messages"][-1].content
|
||||
```
|
||||
|
||||
<b>Step-by-step Breakdown</b>:
|
||||
|
||||
<details>
|
||||
<summary>Initialize the model and tools.</summary>
|
||||
<ul>
|
||||
<li>
|
||||
We use <code>ChatAnthropic</code> as our LLM. <strong>NOTE:</strong> we need to make sure the model knows that it has these tools available to call. We can do this by converting the LangChain tools into the format for OpenAI tool calling using the <code>.bind_tools()</code> method.
|
||||
</li>
|
||||
<li>
|
||||
We define the tools we want to use - a search tool in our case. It is really easy to create your own tools - see documentation here on how to do that <a href="https://python.langchain.com/docs/how_to/custom_tools/">here</a>.
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Initialize graph with state.</summary>
|
||||
|
||||
<ul>
|
||||
<li>We initialize graph (<code>StateGraph</code>) by passing state schema (in our case <code>MessagesState</code>)</li>
|
||||
<li><code>MessagesState</code> is a prebuilt state schema that has one attribute -- a list of LangChain <code>Message</code> objects, as well as logic for merging the updates from each node into the state.</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Define graph nodes.</summary>
|
||||
|
||||
There are two main nodes we need:
|
||||
|
||||
<ul>
|
||||
<li>The <code>agent</code> node: responsible for deciding what (if any) actions to take.</li>
|
||||
<li>The <code>tools</code> node that invokes tools: if the agent decides to take an action, this node will then execute that action.</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Define entry point and graph edges.</summary>
|
||||
|
||||
First, we need to set the entry point for graph execution - <code>agent</code> node.
|
||||
|
||||
Then we define one normal and one conditional edge. Conditional edge means that the destination depends on the contents of the graph's state (<code>MessagesState</code>). In our case, the destination is not known until the agent (LLM) decides.
|
||||
|
||||
<ul>
|
||||
<li>Conditional edge: after the agent is called, we should either:
|
||||
<ul>
|
||||
<li>a. Run tools if the agent said to take an action, OR</li>
|
||||
<li>b. Finish (respond to the user) if the agent did not ask to run tools</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Normal edge: after the tools are invoked, the graph should always return to the agent to decide what to do next</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Compile the graph.</summary>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
When we compile the graph, we turn it into a LangChain
|
||||
<a href="https://python.langchain.com/docs/concepts/runnables/">Runnable</a>,
|
||||
which automatically enables calling <code>.invoke()</code>, <code>.stream()</code> and <code>.batch()</code>
|
||||
with your inputs
|
||||
</li>
|
||||
<li>
|
||||
We can also optionally pass checkpointer object for persisting state between graph runs, and enabling memory,
|
||||
human-in-the-loop workflows, time travel and more. In our case we use <code>MemorySaver</code> -
|
||||
a simple in-memory checkpointer
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Execute the graph.</summary>
|
||||
|
||||
<ol>
|
||||
<li>LangGraph adds the input message to the internal state, then passes the state to the entrypoint node, <code>"agent"</code>.</li>
|
||||
<li>The <code>"agent"</code> node executes, invoking the chat model.</li>
|
||||
<li>The chat model returns an <code>AIMessage</code>. LangGraph adds this to the state.</li>
|
||||
<li>Graph cycles the following steps until there are no more <code>tool_calls</code> on <code>AIMessage</code>:
|
||||
<ul>
|
||||
<li>If <code>AIMessage</code> has <code>tool_calls</code>, <code>"tools"</code> node executes</li>
|
||||
<li>The <code>"agent"</code> node executes again and returns <code>AIMessage</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Execution progresses to the special <code>END</code> value and outputs the final state. And as a result, we get a list of all our chat messages as output.</li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
</details>
|
||||
|
||||
## Documentation
|
||||
|
||||
* [Tutorials](https://langchain-ai.github.io/langgraph/tutorials/): Learn to build with LangGraph through guided examples.
|
||||
* [How-to Guides](https://langchain-ai.github.io/langgraph/how-tos/): Accomplish specific things within LangGraph, from streaming, to adding memory & persistence, to common design patterns (branching, subgraphs, etc.), these are the place to go if you want to copy and run a specific code snippet.
|
||||
* [Conceptual Guides](https://langchain-ai.github.io/langgraph/concepts/high_level/): In-depth explanations of the key concepts and principles behind LangGraph, such as nodes, edges, state and more.
|
||||
* [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Review important classes and methods, simple examples of how to use the graph and checkpointing APIs, higher-level prebuilt components and more.
|
||||
* [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform): LangGraph Platform is a commercial solution for deploying agentic applications in production, built on the open-source LangGraph framework.
|
||||
|
||||
## Resources
|
||||
|
||||
* [Built with LangGraph](https://www.langchain.com/built-with-langgraph): Hear how industry leaders use LangGraph to ship powerful, production-ready AI applications.
|
||||
|
||||
## Contributing
|
||||
|
||||
For more information on how to contribute, see [here](https://github.com/langchain-ai/langgraph/blob/main/CONTRIBUTING.md).
|
||||
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
|
||||
@@ -6,9 +6,12 @@ from pyperf._runner import Runner
|
||||
from uvloop import new_event_loop
|
||||
|
||||
from bench.fanout_to_subgraph import fanout_to_subgraph, fanout_to_subgraph_sync
|
||||
from bench.pydantic_state import pydantic_state
|
||||
from bench.react_agent import react_agent
|
||||
from bench.sequential import create_sequential
|
||||
from bench.wide_state import wide_state
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.pregel import Pregel
|
||||
|
||||
|
||||
@@ -27,6 +30,26 @@ async def arun(graph: Pregel, input: dict):
|
||||
)
|
||||
|
||||
|
||||
async def arun_first_event_latency(graph: Pregel, input: dict) -> None:
|
||||
"""Latency for the first event.
|
||||
|
||||
Run the graph until the first event is processed and then stop.
|
||||
"""
|
||||
stream = graph.astream(
|
||||
input,
|
||||
{
|
||||
"configurable": {"thread_id": str(uuid4())},
|
||||
"recursion_limit": 1000000000,
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
async for _ in stream:
|
||||
break
|
||||
finally:
|
||||
await stream.aclose()
|
||||
|
||||
|
||||
def run(graph: Pregel, input: dict):
|
||||
len(
|
||||
[
|
||||
@@ -42,6 +65,31 @@ def run(graph: Pregel, input: dict):
|
||||
)
|
||||
|
||||
|
||||
def run_first_event_latency(graph: Pregel, input: dict) -> None:
|
||||
"""Latency for the first event.
|
||||
|
||||
Run the graph until the first event is processed and then stop.
|
||||
"""
|
||||
stream = graph.stream(
|
||||
input,
|
||||
{
|
||||
"configurable": {"thread_id": str(uuid4())},
|
||||
"recursion_limit": 1000000000,
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
for _ in stream:
|
||||
break
|
||||
finally:
|
||||
stream.close()
|
||||
|
||||
|
||||
def compile_graph(graph: StateGraph) -> None:
|
||||
"""Compile the graph."""
|
||||
graph.compile()
|
||||
|
||||
|
||||
benchmarks = (
|
||||
(
|
||||
"fanout_to_subgraph_10x",
|
||||
@@ -203,12 +251,183 @@ benchmarks = (
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"sequential_20",
|
||||
create_sequential(20).compile(),
|
||||
create_sequential(20).compile(),
|
||||
{"messages": []}, # Empty list of messages
|
||||
),
|
||||
(
|
||||
"sequential_50",
|
||||
create_sequential(50).compile(),
|
||||
create_sequential(50).compile(),
|
||||
{"messages": []}, # Empty list of messages
|
||||
),
|
||||
(
|
||||
"sequential_100",
|
||||
create_sequential(100).compile(),
|
||||
create_sequential(100).compile(),
|
||||
{"messages": []}, # Empty list of messages
|
||||
),
|
||||
(
|
||||
"sequential_200",
|
||||
create_sequential(200).compile(),
|
||||
create_sequential(200).compile(),
|
||||
{"messages": []}, # Empty list of messages
|
||||
),
|
||||
(
|
||||
"pydantic_state_25x300",
|
||||
pydantic_state(300).compile(checkpointer=None),
|
||||
pydantic_state(300).compile(checkpointer=None),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
str(i) * 10: {
|
||||
str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
|
||||
for j in range(5)
|
||||
}
|
||||
for i in range(5)
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"pydantic_state_25x300_checkpoint",
|
||||
pydantic_state(300).compile(checkpointer=MemorySaver()),
|
||||
pydantic_state(300).compile(checkpointer=MemorySaver()),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
str(i) * 10: {
|
||||
str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
|
||||
for j in range(5)
|
||||
}
|
||||
for i in range(5)
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"pydantic_state_15x600",
|
||||
pydantic_state(600).compile(checkpointer=None),
|
||||
pydantic_state(600).compile(checkpointer=None),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
str(i) * 10: {
|
||||
str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
|
||||
for j in range(5)
|
||||
}
|
||||
for i in range(3)
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"pydantic_state_15x600_checkpoint",
|
||||
pydantic_state(600).compile(checkpointer=MemorySaver()),
|
||||
pydantic_state(600).compile(checkpointer=MemorySaver()),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
str(i) * 10: {
|
||||
str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
|
||||
for j in range(5)
|
||||
}
|
||||
for i in range(3)
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"pydantic_state_9x1200",
|
||||
pydantic_state(1200).compile(checkpointer=None),
|
||||
pydantic_state(1200).compile(checkpointer=None),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
str(i) * 10: {
|
||||
str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
|
||||
for j in range(3)
|
||||
}
|
||||
for i in range(3)
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"pydantic_state_9x1200_checkpoint",
|
||||
pydantic_state(1200).compile(checkpointer=MemorySaver()),
|
||||
pydantic_state(1200).compile(checkpointer=MemorySaver()),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
str(i) * 10: {
|
||||
str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
|
||||
for j in range(3)
|
||||
}
|
||||
for i in range(3)
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
r = Runner()
|
||||
|
||||
# Full graph run time
|
||||
for name, agraph, graph, input in benchmarks:
|
||||
r.bench_async_func(name, arun, agraph, input, loop_factory=new_event_loop)
|
||||
if graph is not None:
|
||||
r.bench_func(name + "_sync", run, graph, input)
|
||||
|
||||
|
||||
# First event latency
|
||||
for name, agraph, graph, input in benchmarks:
|
||||
r.bench_async_func(
|
||||
name + "_first_event_latency",
|
||||
arun_first_event_latency,
|
||||
agraph,
|
||||
input,
|
||||
loop_factory=new_event_loop,
|
||||
)
|
||||
if graph is not None:
|
||||
r.bench_func(
|
||||
name + "_first_event_latency_sync", run_first_event_latency, graph, input
|
||||
)
|
||||
|
||||
# Graph compilation times
|
||||
compilation_benchmarks = (
|
||||
(
|
||||
"sequential_1000",
|
||||
create_sequential(1_000),
|
||||
),
|
||||
(
|
||||
"sequential_10000",
|
||||
create_sequential(10_000),
|
||||
),
|
||||
(
|
||||
"pydantic_state_25x300",
|
||||
pydantic_state(300),
|
||||
),
|
||||
(
|
||||
"pydantic_state_15x600",
|
||||
pydantic_state(600),
|
||||
),
|
||||
(
|
||||
"pydantic_state_9x1200",
|
||||
pydantic_state(1200),
|
||||
),
|
||||
(
|
||||
"wide_state_15x600",
|
||||
wide_state(600),
|
||||
),
|
||||
(
|
||||
"wide_state_9x1200",
|
||||
wide_state(1200),
|
||||
),
|
||||
)
|
||||
|
||||
for name, graph in compilation_benchmarks:
|
||||
r.bench_func(name + "_compilation", compile_graph, graph)
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
import operator
|
||||
from functools import partial
|
||||
from random import choice
|
||||
from typing import Annotated, Optional, Sequence
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from langgraph.constants import END, START
|
||||
from langgraph.graph.state import StateGraph
|
||||
|
||||
|
||||
def pydantic_state(n: int) -> StateGraph:
|
||||
class State(BaseModel):
|
||||
messages: Annotated[list, operator.add] = Field(default_factory=list)
|
||||
|
||||
@field_validator("messages", mode="after")
|
||||
@classmethod
|
||||
def validate_messages(cls, v):
|
||||
if not isinstance(v, list):
|
||||
raise TypeError("messages must be a list")
|
||||
for msg in v:
|
||||
if not isinstance(msg, dict):
|
||||
raise TypeError("messages must be a list of dicts")
|
||||
if not all(isinstance(k, str) for k in msg.keys()):
|
||||
raise TypeError("messages must be a list of dicts with str keys")
|
||||
return v
|
||||
|
||||
trigger_events: Annotated[list, operator.add] = Field(default_factory=list)
|
||||
"""The external events that are converted by the graph."""
|
||||
|
||||
@field_validator("trigger_events", mode="after")
|
||||
@classmethod
|
||||
def validate_trigger_events(cls, v):
|
||||
if not isinstance(v, list):
|
||||
raise TypeError("trigger_events must be a list")
|
||||
for event in v:
|
||||
if not isinstance(event, dict):
|
||||
raise TypeError("trigger_events must be a list of dicts")
|
||||
if not all(isinstance(k, str) for k in event.keys()):
|
||||
raise TypeError(
|
||||
"trigger_events must be a list of dicts with str keys"
|
||||
)
|
||||
return v
|
||||
|
||||
primary_issue_medium: Annotated[str, lambda x, y: y or x] = Field(
|
||||
default="email"
|
||||
)
|
||||
"""The primary issue medium for the current conversation."""
|
||||
|
||||
@field_validator("primary_issue_medium", mode="after")
|
||||
@classmethod
|
||||
def validate_primary_issue_medium(cls, v):
|
||||
if not isinstance(v, str):
|
||||
raise TypeError("primary_issue_medium must be a string")
|
||||
return v
|
||||
|
||||
autoresponse: Annotated[Optional[dict], lambda _, y: y] = Field(
|
||||
default=None
|
||||
) # Always overwrite
|
||||
|
||||
@field_validator("autoresponse", mode="after")
|
||||
@classmethod
|
||||
def validate_autoresponse(cls, v):
|
||||
if v is not None and not isinstance(v, dict):
|
||||
raise TypeError("autoresponse must be a dict or None")
|
||||
return v
|
||||
|
||||
issue: Annotated[dict | None, lambda x, y: y if y else x] = Field(default=None)
|
||||
|
||||
@field_validator("issue", mode="after")
|
||||
@classmethod
|
||||
def validate_issue(cls, v):
|
||||
if v is not None and not isinstance(v, dict):
|
||||
raise TypeError("issue must be a dict or None")
|
||||
return v
|
||||
|
||||
relevant_rules: Optional[list[dict]] = Field(default=None)
|
||||
"""SOPs fetched from the rulebook that are relevant to the current conversation."""
|
||||
|
||||
@field_validator("relevant_rules", mode="after")
|
||||
@classmethod
|
||||
def validate_relevant_rules(cls, v):
|
||||
if v is None:
|
||||
return v
|
||||
if not isinstance(v, list):
|
||||
raise TypeError("relevant_rules must be a list or None")
|
||||
for rule in v:
|
||||
if not isinstance(rule, dict):
|
||||
raise TypeError("relevant_rules must be a list of dicts")
|
||||
if not all(isinstance(k, str) for k in rule.keys()):
|
||||
raise TypeError(
|
||||
"relevant_rules must be a list of dicts with str keys"
|
||||
)
|
||||
return v
|
||||
|
||||
memory_docs: Optional[list[dict]] = Field(default=None)
|
||||
"""Memory docs fetched from the memory service that are relevant to the current conversation."""
|
||||
|
||||
@field_validator("memory_docs", mode="after")
|
||||
@classmethod
|
||||
def validate_memory_docs(cls, v):
|
||||
if v is None:
|
||||
return v
|
||||
if not isinstance(v, list):
|
||||
raise TypeError("memory_docs must be a list or None")
|
||||
for doc in v:
|
||||
if not isinstance(doc, dict):
|
||||
raise TypeError("memory_docs must be a list of dicts")
|
||||
if not all(isinstance(k, str) for k in doc.keys()):
|
||||
raise TypeError("memory_docs must be a list of dicts with str keys")
|
||||
return v
|
||||
|
||||
categorizations: Annotated[list[dict], operator.add] = Field(
|
||||
default_factory=list
|
||||
)
|
||||
"""The issue categorizations auto-generated by the AI."""
|
||||
|
||||
@field_validator("categorizations", mode="after")
|
||||
@classmethod
|
||||
def validate_categorizations(cls, v):
|
||||
if not isinstance(v, list):
|
||||
raise TypeError("categorizations must be a list")
|
||||
for categorization in v:
|
||||
if not isinstance(categorization, dict):
|
||||
raise TypeError("categorizations must be a list of dicts")
|
||||
if not all(isinstance(k, str) for k in categorization.keys()):
|
||||
raise TypeError(
|
||||
"categorizations must be a list of dicts with str keys"
|
||||
)
|
||||
return v
|
||||
|
||||
responses: Annotated[list[dict], operator.add] = Field(default_factory=list)
|
||||
"""The draft responses recommended by the AI."""
|
||||
|
||||
@field_validator("responses", mode="after")
|
||||
@classmethod
|
||||
def validate_responses(cls, v):
|
||||
if not isinstance(v, list):
|
||||
raise TypeError("responses must be a list")
|
||||
for response in v:
|
||||
if not isinstance(response, dict):
|
||||
raise TypeError("responses must be a list of dicts")
|
||||
if not all(isinstance(k, str) for k in response.keys()):
|
||||
raise TypeError("responses must be a list of dicts with str keys")
|
||||
return v
|
||||
|
||||
user_info: Annotated[Optional[dict], lambda x, y: y if y is not None else x] = (
|
||||
Field(default=None)
|
||||
)
|
||||
"""The current user state (by email)."""
|
||||
|
||||
@field_validator("user_info", mode="after")
|
||||
@classmethod
|
||||
def validate_user_info(cls, v):
|
||||
if v is not None and not isinstance(v, dict):
|
||||
raise TypeError("user_info must be a dict or None")
|
||||
return v
|
||||
|
||||
crm_info: Annotated[Optional[dict], lambda x, y: y if y is not None else x] = (
|
||||
Field(default=None)
|
||||
)
|
||||
"""The CRM information for organization the current user is from."""
|
||||
|
||||
@field_validator("crm_info", mode="after")
|
||||
@classmethod
|
||||
def validate_crm_info(cls, v):
|
||||
if v is not None and not isinstance(v, dict):
|
||||
raise TypeError("crm_info must be a dict or None")
|
||||
return v
|
||||
|
||||
email_thread_id: Annotated[
|
||||
Optional[str], lambda x, y: y if y is not None else x
|
||||
] = Field(default=None)
|
||||
"""The current email thread ID."""
|
||||
|
||||
@field_validator("email_thread_id", mode="after")
|
||||
@classmethod
|
||||
def validate_email_thread_id(cls, v):
|
||||
if v is not None and not isinstance(v, str):
|
||||
raise TypeError("email_thread_id must be a string or None")
|
||||
return v
|
||||
|
||||
slack_participants: Annotated[dict, operator.or_] = Field(default_factory=dict)
|
||||
"""The growing list of current slack participants."""
|
||||
|
||||
@field_validator("slack_participants", mode="after")
|
||||
@classmethod
|
||||
def validate_slack_participants(cls, v):
|
||||
if not isinstance(v, dict):
|
||||
raise TypeError("slack_participants must be a dict")
|
||||
for participant in v:
|
||||
if not isinstance(participant, str):
|
||||
raise TypeError("slack_participants must be a dict with str keys")
|
||||
return v
|
||||
|
||||
bot_id: Optional[str] = Field(default=None)
|
||||
"""The ID of the bot user in the slack channel."""
|
||||
|
||||
@field_validator("bot_id", mode="after")
|
||||
@classmethod
|
||||
def validate_bot_id(cls, v):
|
||||
if v is not None and not isinstance(v, str):
|
||||
raise TypeError("bot_id must be a string or None")
|
||||
return v
|
||||
|
||||
notified_assignees: Annotated[dict, operator.or_] = Field(default_factory=dict)
|
||||
|
||||
@field_validator("notified_assignees", mode="after")
|
||||
def validate_notified_assignees(cls, v):
|
||||
if not isinstance(v, dict):
|
||||
raise TypeError("notified_assignees must be a dict")
|
||||
for assignee in v:
|
||||
if not isinstance(assignee, str):
|
||||
raise TypeError("notified_assignees must be a dict with str keys")
|
||||
return v
|
||||
|
||||
list_fields = {
|
||||
"messages",
|
||||
"trigger_events",
|
||||
"categorizations",
|
||||
"responses",
|
||||
"memory_docs",
|
||||
"relevant_rules",
|
||||
}
|
||||
dict_fields = {
|
||||
"user_info",
|
||||
"crm_info",
|
||||
"slack_participants",
|
||||
"notified_assignees",
|
||||
"autoresponse",
|
||||
"issue",
|
||||
}
|
||||
|
||||
def read_write(read: str, write: Sequence[str], input: State) -> dict:
|
||||
val = getattr(input, read)
|
||||
val = {val: val} if isinstance(val, str) else val
|
||||
val_single = val[-1] if isinstance(val, list) else val
|
||||
val_list = val if isinstance(val, list) else [val]
|
||||
return {
|
||||
k: val_list
|
||||
if k in list_fields
|
||||
else val_single
|
||||
if k in dict_fields
|
||||
else "".join(choice("abcdefghijklmnopqrstuvwxyz") for _ in range(n))
|
||||
for k in write
|
||||
}
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_edge(START, "one")
|
||||
builder.add_node(
|
||||
"one",
|
||||
partial(read_write, "messages", ["trigger_events", "primary_issue_medium"]),
|
||||
)
|
||||
builder.add_edge("one", "two")
|
||||
builder.add_node(
|
||||
"two",
|
||||
partial(read_write, "trigger_events", ["autoresponse", "issue"]),
|
||||
)
|
||||
builder.add_edge("two", "three")
|
||||
builder.add_edge("two", "four")
|
||||
builder.add_node(
|
||||
"three",
|
||||
partial(read_write, "autoresponse", ["relevant_rules"]),
|
||||
)
|
||||
builder.add_node(
|
||||
"four",
|
||||
partial(
|
||||
read_write,
|
||||
"trigger_events",
|
||||
["categorizations", "responses", "memory_docs"],
|
||||
),
|
||||
)
|
||||
builder.add_node(
|
||||
"five",
|
||||
partial(
|
||||
read_write,
|
||||
"categorizations",
|
||||
[
|
||||
"user_info",
|
||||
"crm_info",
|
||||
"email_thread_id",
|
||||
"slack_participants",
|
||||
"bot_id",
|
||||
"notified_assignees",
|
||||
],
|
||||
),
|
||||
)
|
||||
builder.add_edge(["three", "four"], "five")
|
||||
builder.add_edge("five", "six")
|
||||
builder.add_node(
|
||||
"six",
|
||||
partial(read_write, "responses", ["messages"]),
|
||||
)
|
||||
builder.add_conditional_edges(
|
||||
"six", lambda state: END if len(state.messages) > n else "one"
|
||||
)
|
||||
|
||||
return builder
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
import uvloop
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
graph = pydantic_state(1000).compile(checkpointer=MemorySaver())
|
||||
input = {
|
||||
"messages": [
|
||||
{
|
||||
str(i) * 10: {
|
||||
str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
|
||||
for j in range(5)
|
||||
}
|
||||
for i in range(5)
|
||||
}
|
||||
]
|
||||
}
|
||||
config = {"configurable": {"thread_id": "1"}, "recursion_limit": 20000000000}
|
||||
|
||||
async def run():
|
||||
async for c in graph.astream(input, config=config):
|
||||
print(c.keys())
|
||||
|
||||
uvloop.install()
|
||||
asyncio.run(run())
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Create a sequential no-op graph consisting of a few hundred nodes."""
|
||||
|
||||
from langgraph.graph import MessagesState, StateGraph
|
||||
from langgraph.utils.runnable import RunnableCallable
|
||||
|
||||
|
||||
def create_sequential(number_nodes: int) -> StateGraph:
|
||||
"""Create a sequential no-op graph consisting of a few hundred nodes."""
|
||||
builder = StateGraph(MessagesState)
|
||||
|
||||
def noop(state: MessagesState) -> None:
|
||||
"""No-op function."""
|
||||
pass
|
||||
|
||||
async def anoop(state: MessagesState) -> None:
|
||||
"""No-op function."""
|
||||
pass
|
||||
|
||||
prev_node = "__start__"
|
||||
|
||||
for i in range(number_nodes):
|
||||
name = f"node_{i}"
|
||||
builder.add_node(name, RunnableCallable(noop, anoop))
|
||||
builder.add_edge(prev_node, name)
|
||||
prev_node = name
|
||||
|
||||
builder.add_edge(prev_node, "__end__")
|
||||
return builder
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
import uvloop
|
||||
|
||||
graph = create_sequential(2000).compile()
|
||||
input = {"messages": []} # Empty list of messages
|
||||
config = {"recursion_limit": 20000000000}
|
||||
|
||||
async def run():
|
||||
len([c async for c in graph.astream(input, config=config)])
|
||||
|
||||
uvloop.install()
|
||||
start = time.time()
|
||||
asyncio.run(run())
|
||||
end = time.time()
|
||||
print(f"Time taken: {end - start:.4f} seconds")
|
||||
@@ -1,8 +1,9 @@
|
||||
from typing import Generic, Optional, Sequence, Type
|
||||
from typing import Any, Generic, Optional, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError
|
||||
|
||||
|
||||
@@ -12,6 +13,10 @@ class AnyValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
|
||||
__slots__ = ("typ", "value")
|
||||
|
||||
def __init__(self, typ: Any, key: str = "") -> None:
|
||||
super().__init__(typ, key)
|
||||
self.value = MISSING
|
||||
|
||||
def __eq__(self, value: object) -> bool:
|
||||
return isinstance(value, AnyValue)
|
||||
|
||||
@@ -34,17 +39,19 @@ class AnyValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
|
||||
def update(self, values: Sequence[Value]) -> bool:
|
||||
if len(values) == 0:
|
||||
try:
|
||||
del self.value
|
||||
return True
|
||||
except AttributeError:
|
||||
if self.value is MISSING:
|
||||
return False
|
||||
else:
|
||||
self.value = MISSING
|
||||
return True
|
||||
|
||||
self.value = values[-1]
|
||||
return True
|
||||
|
||||
def get(self) -> Value:
|
||||
try:
|
||||
return self.value
|
||||
except AttributeError:
|
||||
if self.value is MISSING:
|
||||
raise EmptyChannelError()
|
||||
return self.value
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self.value is not MISSING
|
||||
|
||||
@@ -64,6 +64,17 @@ class BaseChannel(Generic[Value, Update, C], ABC):
|
||||
"""
|
||||
return False
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Return True if the channel is available (not empty), False otherwise.
|
||||
Subclasses should override this method to provide a more efficient
|
||||
implementation than calling get() and catching EmptyChannelError.
|
||||
"""
|
||||
try:
|
||||
self.get()
|
||||
return True
|
||||
except EmptyChannelError:
|
||||
return False
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BaseChannel",
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing import (
|
||||
from typing_extensions import NotRequired, Required, Self
|
||||
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError
|
||||
|
||||
|
||||
@@ -51,7 +52,7 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
try:
|
||||
self.value = typ()
|
||||
except Exception:
|
||||
pass
|
||||
self.value = MISSING
|
||||
|
||||
def __eq__(self, value: object) -> bool:
|
||||
return isinstance(value, BinaryOperatorAggregate) and (
|
||||
@@ -81,7 +82,7 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
def update(self, values: Sequence[Value]) -> bool:
|
||||
if not values:
|
||||
return False
|
||||
if not hasattr(self, "value"):
|
||||
if self.value is MISSING:
|
||||
self.value = values[0]
|
||||
values = values[1:]
|
||||
for value in values:
|
||||
@@ -89,7 +90,9 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
return True
|
||||
|
||||
def get(self) -> Value:
|
||||
try:
|
||||
return self.value
|
||||
except AttributeError:
|
||||
if self.value is MISSING:
|
||||
raise EmptyChannelError()
|
||||
return self.value
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self.value is not MISSING
|
||||
|
||||
@@ -85,6 +85,9 @@ class DynamicBarrierValue(
|
||||
raise EmptyChannelError()
|
||||
return None
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self.seen == self.names
|
||||
|
||||
def consume(self) -> bool:
|
||||
if self.seen == self.names:
|
||||
self.seen = set()
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Any, Generic, Optional, Sequence, Type
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError, InvalidUpdateError
|
||||
|
||||
|
||||
@@ -14,6 +15,7 @@ class EphemeralValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
def __init__(self, typ: Any, guard: bool = True) -> None:
|
||||
super().__init__(typ)
|
||||
self.guard = guard
|
||||
self.value = MISSING
|
||||
|
||||
def __eq__(self, value: object) -> bool:
|
||||
return isinstance(value, EphemeralValue) and value.guard == self.guard
|
||||
@@ -37,10 +39,10 @@ class EphemeralValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
|
||||
def update(self, values: Sequence[Value]) -> bool:
|
||||
if len(values) == 0:
|
||||
try:
|
||||
del self.value
|
||||
if self.value is not MISSING:
|
||||
self.value = MISSING
|
||||
return True
|
||||
except AttributeError:
|
||||
else:
|
||||
return False
|
||||
if len(values) != 1 and self.guard:
|
||||
raise InvalidUpdateError(
|
||||
@@ -51,7 +53,9 @@ class EphemeralValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
return True
|
||||
|
||||
def get(self) -> Value:
|
||||
try:
|
||||
return self.value
|
||||
except AttributeError:
|
||||
if self.value is MISSING:
|
||||
raise EmptyChannelError()
|
||||
return self.value
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self.value is not MISSING
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from typing import Generic, Optional, Sequence, Type
|
||||
from typing import Any, Generic, Optional, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import (
|
||||
EmptyChannelError,
|
||||
ErrorCode,
|
||||
@@ -16,6 +17,10 @@ class LastValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
|
||||
__slots__ = ("value",)
|
||||
|
||||
def __init__(self, typ: Any, key: str = "") -> None:
|
||||
super().__init__(typ, key)
|
||||
self.value = MISSING
|
||||
|
||||
def __eq__(self, value: object) -> bool:
|
||||
return isinstance(value, LastValue)
|
||||
|
||||
@@ -50,7 +55,9 @@ class LastValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
return True
|
||||
|
||||
def get(self) -> Value:
|
||||
try:
|
||||
return self.value
|
||||
except AttributeError:
|
||||
if self.value is MISSING:
|
||||
raise EmptyChannelError()
|
||||
return self.value
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self.value is not MISSING
|
||||
|
||||
@@ -60,6 +60,9 @@ class NamedBarrierValue(Generic[Value], BaseChannel[Value, Value, set[Value]]):
|
||||
raise EmptyChannelError()
|
||||
return None
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self.seen == self.names
|
||||
|
||||
def consume(self) -> bool:
|
||||
if self.seen == self.names:
|
||||
self.seen = set()
|
||||
|
||||
@@ -75,3 +75,6 @@ class Topic(
|
||||
return list(self.values)
|
||||
else:
|
||||
raise EmptyChannelError
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return bool(self.values)
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Generic, Optional, Sequence, Type
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError, InvalidUpdateError
|
||||
|
||||
|
||||
@@ -14,6 +15,7 @@ class UntrackedValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
def __init__(self, typ: Type[Value], guard: bool = True) -> None:
|
||||
super().__init__(typ)
|
||||
self.guard = guard
|
||||
self.value = MISSING
|
||||
|
||||
def __eq__(self, value: object) -> bool:
|
||||
return isinstance(value, UntrackedValue) and value.guard == self.guard
|
||||
@@ -48,7 +50,9 @@ class UntrackedValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
return True
|
||||
|
||||
def get(self) -> Value:
|
||||
try:
|
||||
return self.value
|
||||
except AttributeError:
|
||||
if self.value is MISSING:
|
||||
raise EmptyChannelError()
|
||||
return self.value
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self.value is not MISSING
|
||||
|
||||
@@ -36,23 +36,31 @@ from langgraph.types import _DC_KWARGS, RetryPolicy, StreamMode
|
||||
|
||||
@overload
|
||||
def task(
|
||||
*, name: Optional[str] = None, retry: Optional[RetryPolicy] = None
|
||||
) -> Callable[[Callable[P, T]], Callable[P, SyncAsyncFuture[T]]]: ...
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
retry: Optional[RetryPolicy] = None,
|
||||
) -> Callable[
|
||||
[Union[Callable[P, Awaitable[T]], Callable[P, T]]],
|
||||
Callable[P, SyncAsyncFuture[T]],
|
||||
]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def task(
|
||||
__func_or_none__: Callable[P, T],
|
||||
__func_or_none__: Union[Callable[P, Awaitable[T]], Callable[P, T]],
|
||||
) -> Callable[P, SyncAsyncFuture[T]]: ...
|
||||
|
||||
|
||||
def task(
|
||||
__func_or_none__: Optional[Union[Callable[P, T], Callable[P, Awaitable[T]]]] = None,
|
||||
__func_or_none__: Optional[Union[Callable[P, Awaitable[T]], Callable[P, T]]] = None,
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
retry: Optional[RetryPolicy] = None,
|
||||
) -> Union[
|
||||
Callable[[Callable[P, T]], Callable[P, SyncAsyncFuture[T]]],
|
||||
Callable[
|
||||
[Union[Callable[P, Awaitable[T]], Callable[P, T]]],
|
||||
Callable[P, SyncAsyncFuture[T]],
|
||||
],
|
||||
Callable[P, SyncAsyncFuture[T]],
|
||||
]:
|
||||
"""Define a LangGraph task using the `task` decorator.
|
||||
@@ -345,7 +353,7 @@ class entrypoint:
|
||||
value: R
|
||||
"""Value to return. A value will always be returned even if it is None."""
|
||||
save: S
|
||||
"""The value for the state for the next checkpoint.
|
||||
"""The value for the state for the next checkpoint.
|
||||
|
||||
A value will always be saved even if it is None.
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import asyncio
|
||||
from inspect import (
|
||||
isfunction,
|
||||
ismethod,
|
||||
signature,
|
||||
)
|
||||
from types import FunctionType
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Hashable,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Sequence,
|
||||
Type,
|
||||
Union,
|
||||
cast,
|
||||
get_args,
|
||||
get_origin,
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
from langchain_core.runnables import (
|
||||
Runnable,
|
||||
RunnableConfig,
|
||||
RunnableLambda,
|
||||
)
|
||||
|
||||
from langgraph.constants import END, START
|
||||
from langgraph.errors import InvalidUpdateError
|
||||
from langgraph.pregel.write import ChannelWrite
|
||||
from langgraph.types import Send
|
||||
from langgraph.utils.runnable import (
|
||||
RunnableCallable,
|
||||
)
|
||||
|
||||
|
||||
def _get_branch_path_input_schema(
|
||||
path: Union[
|
||||
Callable[..., Union[Hashable, list[Hashable]]],
|
||||
Callable[..., Awaitable[Union[Hashable, list[Hashable]]]],
|
||||
Runnable[Any, Union[Hashable, list[Hashable]]],
|
||||
],
|
||||
) -> Optional[Type[Any]]:
|
||||
input = None
|
||||
# detect input schema annotation in the branch callable
|
||||
try:
|
||||
callable_: Optional[
|
||||
Union[
|
||||
Callable[..., Union[Hashable, list[Hashable]]],
|
||||
Callable[..., Awaitable[Union[Hashable, list[Hashable]]]],
|
||||
]
|
||||
] = None
|
||||
if isinstance(path, (RunnableCallable, RunnableLambda)):
|
||||
if isfunction(path.func) or ismethod(path.func):
|
||||
callable_ = path.func
|
||||
elif (callable_method := getattr(path.func, "__call__", None)) and ismethod(
|
||||
callable_method
|
||||
):
|
||||
callable_ = callable_method
|
||||
elif isfunction(path.afunc) or ismethod(path.afunc):
|
||||
callable_ = path.afunc
|
||||
elif (
|
||||
callable_method := getattr(path.afunc, "__call__", None)
|
||||
) and ismethod(callable_method):
|
||||
callable_ = callable_method
|
||||
elif callable(path):
|
||||
callable_ = path
|
||||
|
||||
if callable_ is not None and (hints := get_type_hints(callable_)):
|
||||
first_parameter_name = next(
|
||||
iter(signature(cast(FunctionType, callable_)).parameters.keys())
|
||||
)
|
||||
if input_hint := hints.get(first_parameter_name):
|
||||
if isinstance(input_hint, type) and get_type_hints(input_hint):
|
||||
input = input_hint
|
||||
except (TypeError, StopIteration):
|
||||
pass
|
||||
|
||||
return input
|
||||
|
||||
|
||||
class Branch(NamedTuple):
|
||||
path: Runnable[Any, Union[Hashable, list[Hashable]]]
|
||||
ends: Optional[dict[Hashable, str]]
|
||||
then: Optional[str] = None
|
||||
input_schema: Optional[Type[Any]] = None
|
||||
|
||||
@classmethod
|
||||
def from_path(
|
||||
cls,
|
||||
path: Runnable[Any, Union[Hashable, list[Hashable]]],
|
||||
path_map: Optional[Union[dict[Hashable, str], list[str]]],
|
||||
then: Optional[str] = None,
|
||||
infer_schema: bool = False,
|
||||
) -> "Branch":
|
||||
# coerce path_map to a dictionary
|
||||
path_map_: Optional[dict[Hashable, str]] = None
|
||||
try:
|
||||
if isinstance(path_map, dict):
|
||||
path_map_ = path_map.copy()
|
||||
elif isinstance(path_map, list):
|
||||
path_map_ = {name: name for name in path_map}
|
||||
else:
|
||||
# find func
|
||||
func: Optional[Callable] = None
|
||||
if isinstance(path, (RunnableCallable, RunnableLambda)):
|
||||
func = path.func or path.afunc
|
||||
if func is not None:
|
||||
# find callable method
|
||||
if (cal := getattr(path, "__call__", None)) and ismethod(cal):
|
||||
func = cal
|
||||
# get the return type
|
||||
if rtn_type := get_type_hints(func).get("return"):
|
||||
if get_origin(rtn_type) is Literal:
|
||||
path_map_ = {name: name for name in get_args(rtn_type)}
|
||||
except Exception:
|
||||
pass
|
||||
# infer input schema
|
||||
input_schema = _get_branch_path_input_schema(path) if infer_schema else None
|
||||
# create branch
|
||||
return cls(path=path, ends=path_map_, then=then, input_schema=input_schema)
|
||||
|
||||
def run(
|
||||
self,
|
||||
writer: Callable[
|
||||
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
|
||||
],
|
||||
reader: Optional[Callable[[RunnableConfig], Any]] = None,
|
||||
) -> RunnableCallable:
|
||||
return ChannelWrite.register_writer(
|
||||
RunnableCallable(
|
||||
func=self._route,
|
||||
afunc=self._aroute,
|
||||
writer=writer,
|
||||
reader=reader,
|
||||
name=None,
|
||||
trace=False,
|
||||
func_accepts_config=True,
|
||||
)
|
||||
)
|
||||
|
||||
def _route(
|
||||
self,
|
||||
input: Any,
|
||||
config: RunnableConfig,
|
||||
*,
|
||||
reader: Optional[Callable[[RunnableConfig], Any]],
|
||||
writer: Callable[
|
||||
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
|
||||
],
|
||||
) -> Runnable:
|
||||
if reader:
|
||||
value = reader(config)
|
||||
# passthrough additional keys from node to branch
|
||||
# only doable when using dict states
|
||||
if (
|
||||
isinstance(value, dict)
|
||||
and isinstance(input, dict)
|
||||
and self.input_schema is None
|
||||
):
|
||||
value = {**input, **value}
|
||||
else:
|
||||
value = input
|
||||
result = self.path.invoke(value, config)
|
||||
return self._finish(writer, input, result, config)
|
||||
|
||||
async def _aroute(
|
||||
self,
|
||||
input: Any,
|
||||
config: RunnableConfig,
|
||||
*,
|
||||
reader: Optional[Callable[[RunnableConfig], Any]],
|
||||
writer: Callable[
|
||||
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
|
||||
],
|
||||
) -> Runnable:
|
||||
if reader:
|
||||
value = await asyncio.to_thread(reader, config)
|
||||
# passthrough additional keys from node to branch
|
||||
# only doable when using dict states
|
||||
if (
|
||||
isinstance(value, dict)
|
||||
and isinstance(input, dict)
|
||||
and self.input_schema is None
|
||||
):
|
||||
value = {**input, **value}
|
||||
else:
|
||||
value = input
|
||||
result = await self.path.ainvoke(value, config)
|
||||
return self._finish(writer, input, result, config)
|
||||
|
||||
def _finish(
|
||||
self,
|
||||
writer: Callable[
|
||||
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
|
||||
],
|
||||
input: Any,
|
||||
result: Any,
|
||||
config: RunnableConfig,
|
||||
) -> Union[Runnable, Any]:
|
||||
if not isinstance(result, (list, tuple)):
|
||||
result = [result]
|
||||
if self.ends:
|
||||
destinations: Sequence[Union[Send, str]] = [
|
||||
r if isinstance(r, Send) else self.ends[r] for r in result
|
||||
]
|
||||
else:
|
||||
destinations = cast(Sequence[Union[Send, str]], result)
|
||||
if any(dest is None or dest == START for dest in destinations):
|
||||
raise ValueError("Branch did not return a valid destination")
|
||||
if any(p.node == END for p in destinations if isinstance(p, Send)):
|
||||
raise InvalidUpdateError("Cannot send a packet to the END node")
|
||||
return writer(destinations, config) or input
|
||||
@@ -6,15 +6,11 @@ from typing import (
|
||||
Awaitable,
|
||||
Callable,
|
||||
Hashable,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Sequence,
|
||||
Union,
|
||||
cast,
|
||||
get_args,
|
||||
get_origin,
|
||||
get_type_hints,
|
||||
overload,
|
||||
)
|
||||
|
||||
@@ -34,12 +30,13 @@ from langgraph.constants import (
|
||||
TAG_HIDDEN,
|
||||
Send,
|
||||
)
|
||||
from langgraph.errors import InvalidUpdateError
|
||||
from langgraph.graph.branch import Branch
|
||||
from langgraph.pregel import Channel, Pregel
|
||||
from langgraph.pregel.protocol import PregelProtocol
|
||||
from langgraph.pregel.read import PregelNode
|
||||
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.types import All, Checkpointer
|
||||
from langgraph.utils.runnable import RunnableCallable, RunnableLike, coerce_to_runnable
|
||||
from langgraph.utils.runnable import RunnableLike, coerce_to_runnable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -50,95 +47,6 @@ class NodeSpec(NamedTuple):
|
||||
ends: Optional[Union[tuple[str, ...], dict[str, str]]] = EMPTY_SEQ
|
||||
|
||||
|
||||
class Branch(NamedTuple):
|
||||
path: Runnable[Any, Union[Hashable, list[Hashable]]]
|
||||
ends: Optional[dict[Hashable, str]]
|
||||
then: Optional[str] = None
|
||||
|
||||
def run(
|
||||
self,
|
||||
writer: Callable[
|
||||
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
|
||||
],
|
||||
reader: Optional[Callable[[RunnableConfig], Any]] = None,
|
||||
) -> RunnableCallable:
|
||||
return ChannelWrite.register_writer(
|
||||
RunnableCallable(
|
||||
func=self._route,
|
||||
afunc=self._aroute,
|
||||
writer=writer,
|
||||
reader=reader,
|
||||
name=None,
|
||||
trace=False,
|
||||
)
|
||||
)
|
||||
|
||||
def _route(
|
||||
self,
|
||||
input: Any,
|
||||
config: RunnableConfig,
|
||||
*,
|
||||
reader: Optional[Callable[[RunnableConfig], Any]],
|
||||
writer: Callable[
|
||||
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
|
||||
],
|
||||
) -> Runnable:
|
||||
if reader:
|
||||
value = reader(config)
|
||||
# passthrough additional keys from node to branch
|
||||
# only doable when using dict states
|
||||
if isinstance(value, dict) and isinstance(input, dict):
|
||||
value = {**input, **value}
|
||||
else:
|
||||
value = input
|
||||
result = self.path.invoke(value, config)
|
||||
return self._finish(writer, input, result, config)
|
||||
|
||||
async def _aroute(
|
||||
self,
|
||||
input: Any,
|
||||
config: RunnableConfig,
|
||||
*,
|
||||
reader: Optional[Callable[[RunnableConfig], Any]],
|
||||
writer: Callable[
|
||||
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
|
||||
],
|
||||
) -> Runnable:
|
||||
if reader:
|
||||
value = await asyncio.to_thread(reader, config)
|
||||
# passthrough additional keys from node to branch
|
||||
# only doable when using dict states
|
||||
if isinstance(value, dict) and isinstance(input, dict):
|
||||
value = {**input, **value}
|
||||
else:
|
||||
value = input
|
||||
result = await self.path.ainvoke(value, config)
|
||||
return self._finish(writer, input, result, config)
|
||||
|
||||
def _finish(
|
||||
self,
|
||||
writer: Callable[
|
||||
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
|
||||
],
|
||||
input: Any,
|
||||
result: Any,
|
||||
config: RunnableConfig,
|
||||
) -> Union[Runnable, Any]:
|
||||
if not isinstance(result, (list, tuple)):
|
||||
result = [result]
|
||||
if self.ends:
|
||||
destinations: Sequence[Union[Send, str]] = [
|
||||
r if isinstance(r, Send) else self.ends[r] for r in result
|
||||
]
|
||||
else:
|
||||
destinations = cast(Sequence[Union[Send, str]], result)
|
||||
if any(dest is None or dest == START for dest in destinations):
|
||||
raise ValueError("Branch did not return a valid destination")
|
||||
if any(p.node == END for p in destinations if isinstance(p, Send)):
|
||||
raise InvalidUpdateError("Cannot send a packet to the END node")
|
||||
return writer(destinations, config) or input
|
||||
|
||||
|
||||
class Graph:
|
||||
def __init__(self) -> None:
|
||||
self.nodes: dict[str, NodeSpec] = {}
|
||||
@@ -267,25 +175,7 @@ class Graph:
|
||||
"Adding an edge to a graph that has already been compiled. This will "
|
||||
"not be reflected in the compiled graph."
|
||||
)
|
||||
# coerce path_map to a dictionary
|
||||
try:
|
||||
if isinstance(path_map, dict):
|
||||
path_map_ = path_map.copy()
|
||||
elif isinstance(path_map, list):
|
||||
path_map_ = {name: name for name in path_map}
|
||||
elif isinstance(path, Runnable):
|
||||
path_map_ = None
|
||||
elif rtn_type := get_type_hints(path.__call__).get( # type: ignore[operator]
|
||||
"return"
|
||||
) or get_type_hints(path).get("return"):
|
||||
if get_origin(rtn_type) is Literal:
|
||||
path_map_ = {name: name for name in get_args(rtn_type)}
|
||||
else:
|
||||
path_map_ = None
|
||||
else:
|
||||
path_map_ = None
|
||||
except Exception:
|
||||
path_map_ = None
|
||||
|
||||
# find a name for the condition
|
||||
path = coerce_to_runnable(path, name=None, trace=True)
|
||||
name = path.name or "condition"
|
||||
@@ -295,7 +185,7 @@ class Graph:
|
||||
f"Branch with name `{path.name}` already exists for node " f"`{source}`"
|
||||
)
|
||||
# save it
|
||||
self.branches[source][name] = Branch(path, path_map_, then)
|
||||
self.branches[source][name] = Branch.from_path(path, path_map, then, False)
|
||||
return self
|
||||
|
||||
def set_entry_point(self, key: str) -> Self:
|
||||
@@ -530,7 +420,38 @@ class CompiledGraph(Pregel):
|
||||
*,
|
||||
xray: Union[int, bool] = False,
|
||||
) -> DrawableGraph:
|
||||
return self.get_graph(config, xray=xray)
|
||||
"""Returns a drawable representation of the computation graph."""
|
||||
from langgraph.pregel.remote import RemoteGraph
|
||||
|
||||
# gather subgraphs
|
||||
if xray:
|
||||
subpregels: dict[str, PregelProtocol] = {
|
||||
k: v
|
||||
async for k, v in self.aget_subgraphs()
|
||||
if isinstance(v, (CompiledGraph, RemoteGraph))
|
||||
}
|
||||
subgraphs = {
|
||||
k: v
|
||||
for k, v in zip(
|
||||
subpregels,
|
||||
await asyncio.gather(
|
||||
*(
|
||||
p.aget_graph(
|
||||
config,
|
||||
xray=xray
|
||||
if isinstance(xray, bool) or xray <= 0
|
||||
else xray - 1,
|
||||
)
|
||||
for p in subpregels.values()
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
else:
|
||||
subgraphs = {}
|
||||
|
||||
# draw the graph
|
||||
return self._draw_graph(config, subgraphs=subgraphs)
|
||||
|
||||
def get_graph(
|
||||
self,
|
||||
@@ -539,17 +460,36 @@ class CompiledGraph(Pregel):
|
||||
xray: Union[int, bool] = False,
|
||||
) -> DrawableGraph:
|
||||
"""Returns a drawable representation of the computation graph."""
|
||||
from langgraph.pregel.remote import RemoteGraph
|
||||
|
||||
# gather subgraphs
|
||||
if xray:
|
||||
subgraphs = {
|
||||
k: v.get_graph(
|
||||
config,
|
||||
xray=xray if isinstance(xray, bool) or xray <= 0 else xray - 1,
|
||||
)
|
||||
for k, v in self.get_subgraphs()
|
||||
if isinstance(v, (CompiledGraph, RemoteGraph))
|
||||
}
|
||||
else:
|
||||
subgraphs = {}
|
||||
|
||||
# draw the graph
|
||||
return self._draw_graph(config, subgraphs=subgraphs)
|
||||
|
||||
def _draw_graph(
|
||||
self,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
*,
|
||||
subgraphs: dict[str, DrawableGraph] = {},
|
||||
) -> DrawableGraph:
|
||||
# create the graph
|
||||
graph = DrawableGraph()
|
||||
start_nodes: dict[str, DrawableNode] = {
|
||||
START: graph.add_node(self.get_input_schema(config), START)
|
||||
}
|
||||
end_nodes: dict[str, DrawableNode] = {}
|
||||
if xray:
|
||||
subgraphs = {
|
||||
k: v for k, v in self.get_subgraphs() if isinstance(v, CompiledGraph)
|
||||
}
|
||||
else:
|
||||
subgraphs = {}
|
||||
|
||||
def add_edge(
|
||||
start: str,
|
||||
@@ -575,16 +515,11 @@ class CompiledGraph(Pregel):
|
||||
metadata["__interrupt"] = "before"
|
||||
elif key in self.interrupt_after_nodes:
|
||||
metadata["__interrupt"] = "after"
|
||||
if xray and key in subgraphs:
|
||||
subgraph = subgraphs[key].get_graph(
|
||||
config=config,
|
||||
xray=xray - 1
|
||||
if isinstance(xray, int) and not isinstance(xray, bool) and xray > 0
|
||||
else xray,
|
||||
)
|
||||
if key in subgraphs:
|
||||
subgraph = subgraphs[key]
|
||||
subgraph.trim_first_node()
|
||||
subgraph.trim_last_node()
|
||||
if len(subgraph.nodes) > 1:
|
||||
if len(subgraph.nodes) >= 1:
|
||||
e, s = graph.extend(subgraph, prefix=key)
|
||||
if e is None:
|
||||
raise ValueError(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user