mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-10-30 10:51:40 +01:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91f6fea068 | ||
|
|
57f0e846e8 | ||
|
|
4b27c10aca | ||
|
|
0e8252fd31 | ||
|
|
d18da208b9 | ||
|
|
6deac3abdf | ||
|
|
f02f7a5a27 | ||
|
|
6669634a98 | ||
|
|
bbe8346c8b |
No files matched your search
@@ -48,7 +48,7 @@ jobs:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
- name: Upload build
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: test-dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
name: Deploy Docs Redirects
|
||||
name: Deploy Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -20,6 +23,18 @@ defaults:
|
||||
working-directory: docs
|
||||
|
||||
jobs:
|
||||
get-changed-files:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changed-files: ${{ steps.changed-files.outputs.added_modified }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: Ana06/get-changed-files@v2.3.0
|
||||
with:
|
||||
filter: "docs/docs/**"
|
||||
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10 # Job will be cancelled if it runs for more than 10 minutes
|
||||
@@ -47,21 +62,84 @@ jobs:
|
||||
uv run pip install "git+https://${GITHUB_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git"
|
||||
fi
|
||||
|
||||
- name: Run unit tests
|
||||
# Run unit tests on the docs build pipeline
|
||||
run: make tests
|
||||
- name: Lint Docs
|
||||
# This step lints the docs using the existing linting set up.
|
||||
# It should be very fast and should not require any external services.
|
||||
run: make lint-docs
|
||||
- name: Build llms-text
|
||||
run: make llms-text
|
||||
|
||||
- name: Build site (redirects only)
|
||||
run: make build-docs
|
||||
- name: Build site
|
||||
run: |
|
||||
# If this is main branch, then we want to download stats. we do this
|
||||
# with the env variable DOWNLOAD_STATS=true
|
||||
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
|
||||
DOWNLOAD_STATS=true make build-docs
|
||||
else
|
||||
make build-docs
|
||||
fi
|
||||
env:
|
||||
MKDOCS_GIT_COMMITTERS_APIKEY: ${{ secrets.MKDOCS_GIT_COMMITTERS_APIKEY }}
|
||||
OPENAI_API_KEY: sf-proj-1234567890 # fake placeholder, shouldn't actually be used
|
||||
ANTHROPIC_API_KEY: sk-ant-api03-1234567890 # fake placeholder, shouldn't actually be used
|
||||
- name: Check links in notebooks
|
||||
env:
|
||||
LANGCHAIN_API_KEY: test
|
||||
if: github.event_name == 'schedule'
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "schedule" ]; then
|
||||
echo "Running link check on all HTML files matching notebooks in docs directory..."
|
||||
uv run pytest -v \
|
||||
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://academy\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://x.com/.*" \
|
||||
--check-links-ignore "https://twitter.com/.*" \
|
||||
--check-links-ignore "https://github\.com/.*" \
|
||||
--check-links-ignore "http://localhost:8123/.*" \
|
||||
--check-links-ignore "http://localhost:2024.*" \
|
||||
--check-links-ignore "http://127.0.0.1:.*" \
|
||||
--check-links-ignore "/.*\.(ipynb|html)$" \
|
||||
--check-links-ignore "https://python\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://openai\.com/.*" \
|
||||
--check-links-ignore "https://www\.uber\.com/.*" \
|
||||
--check-links-ignore "https://pepy\.tech/.*" \
|
||||
--check-links-ignore "docs/docs/static/wordmark_*" \
|
||||
--check-links $(find site -name "index.html" | grep -v 'storm/index.html')
|
||||
|
||||
else
|
||||
echo "Fetching changes from origin/main..."
|
||||
git fetch origin main
|
||||
echo "Checking for changed notebook files..."
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=d origin/main | grep 'docs/docs/.*\.ipynb$' | grep -v 'storm.ipynb' | sed -E 's|^docs/docs/|site/|; s/\.ipynb$/\/index.html/' || true)
|
||||
echo "Changed files: ${CHANGED_FILES}"
|
||||
if [ -n "${CHANGED_FILES}" ]; then
|
||||
echo "Running link check on HTML files matching changed notebook files..."
|
||||
uv run pytest -v \
|
||||
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://academy\.langchain\.com/.*" \
|
||||
--check-links-ignore "http://localhost:8123/.*" \
|
||||
--check-links-ignore "http://localhost:2024.*" \
|
||||
--check-links-ignore "http://127.0.0.1:.*" \
|
||||
--check-links-ignore "https://x.com/.*" \
|
||||
--check-links-ignore "https://twitter.com/.*" \
|
||||
--check-links-ignore "https://github\.com/.*" \
|
||||
--check-links-ignore "/.*\.(ipynb|html)$" \
|
||||
--check-links-ignore "docs/docs/static/wordmark_*" \
|
||||
--check-links ${CHANGED_FILES} \
|
||||
|| ([ $? = 5 ] && exit 0 || exit $?)
|
||||
else
|
||||
echo "No notebook files changed."
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Configure GitHub Pages
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Upload Pages Artifact
|
||||
if: github.ref == 'refs/heads/main'
|
||||
# if: github.ref == 'refs/heads/main'
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: ./docs/site/
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
- name: Upload build
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
@@ -269,7 +269,7 @@ jobs:
|
||||
enable-cache: true
|
||||
cache-suffix: "release"
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
@@ -310,7 +310,7 @@ jobs:
|
||||
enable-cache: true
|
||||
cache-suffix: "release"
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
|
||||
+88
-366
@@ -27,66 +27,66 @@ DISABLED = os.getenv("DISABLE_NOTEBOOK_CONVERT") in ("1", "true", "True")
|
||||
|
||||
REDIRECT_MAP = {
|
||||
# lib redirects
|
||||
"how-tos/stream-values.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"how-tos/stream-updates.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"how-tos/streaming-content.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"how-tos/stream-multiple.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"how-tos/streaming-tokens-without-langchain.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"how-tos/streaming-from-final-node.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"how-tos/streaming-events-from-within-tools-without-langchain.ipynb": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"how-tos/stream-values.ipynb": "how-tos/streaming.md#stream-graph-state",
|
||||
"how-tos/stream-updates.ipynb": "how-tos/streaming.md#stream-graph-state",
|
||||
"how-tos/streaming-content.ipynb": "how-tos/streaming.md",
|
||||
"how-tos/stream-multiple.ipynb": "how-tos/streaming.md#stream-multiple-nodes",
|
||||
"how-tos/streaming-tokens-without-langchain.ipynb": "how-tos/streaming.md#use-with-any-llm",
|
||||
"how-tos/streaming-from-final-node.ipynb": "how-tos/streaming-specific-nodes.ipynb",
|
||||
"how-tos/streaming-events-from-within-tools-without-langchain.ipynb": "how-tos/streaming-events-from-within-tools.ipynb#example-without-langchain",
|
||||
# graph-api
|
||||
"how-tos/state-reducers.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#define-and-update-state",
|
||||
"how-tos/sequence.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#create-a-sequence-of-steps",
|
||||
"how-tos/branching.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#create-branches",
|
||||
"how-tos/recursion-limit.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#create-and-control-loops",
|
||||
"how-tos/visualization.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#visualize-your-graph",
|
||||
"how-tos/input_output_schema.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#define-input-and-output-schemas",
|
||||
"how-tos/pass_private_state.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#pass-private-state-between-nodes",
|
||||
"how-tos/state-model.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#use-pydantic-models-for-graph-state",
|
||||
"how-tos/map-reduce.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#map-reduce-and-the-send-api",
|
||||
"how-tos/command.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#combine-control-flow-and-state-updates-with-command",
|
||||
"how-tos/configuration.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#add-runtime-configuration",
|
||||
"how-tos/node-retries.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#add-retry-policies",
|
||||
"how-tos/return-when-recursion-limit-hits.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#impose-a-recursion-limit",
|
||||
"how-tos/async.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api#async",
|
||||
"how-tos/state-reducers.ipynb": "how-tos/graph-api.md#define-and-update-state",
|
||||
"how-tos/sequence.ipynb": "how-tos/graph-api.md#create-a-sequence-of-steps",
|
||||
"how-tos/branching.ipynb": "how-tos/graph-api.md#create-branches",
|
||||
"how-tos/recursion-limit.ipynb": "how-tos/graph-api.md#create-and-control-loops",
|
||||
"how-tos/visualization.ipynb": "how-tos/graph-api.md#visualize-your-graph",
|
||||
"how-tos/input_output_schema.ipynb": "how-tos/graph-api.md#define-input-and-output-schemas",
|
||||
"how-tos/pass_private_state.ipynb": "how-tos/graph-api.md#pass-private-state-between-nodes",
|
||||
"how-tos/state-model.ipynb": "how-tos/graph-api.md#use-pydantic-models-for-graph-state",
|
||||
"how-tos/map-reduce.ipynb": "how-tos/graph-api.md#map-reduce-and-the-send-api",
|
||||
"how-tos/command.ipynb": "how-tos/graph-api.md#combine-control-flow-and-state-updates-with-command",
|
||||
"how-tos/configuration.ipynb": "how-tos/graph-api.md#add-runtime-configuration",
|
||||
"how-tos/node-retries.ipynb": "how-tos/graph-api.md#add-retry-policies",
|
||||
"how-tos/return-when-recursion-limit-hits.ipynb": "how-tos/graph-api.md#impose-a-recursion-limit",
|
||||
"how-tos/async.ipynb": "how-tos/graph-api.md#async",
|
||||
# memory how-tos
|
||||
"how-tos/memory/manage-conversation-history.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"how-tos/memory/delete-messages.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#delete-messages",
|
||||
"how-tos/memory/add-summary-conversation-history.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#summarize-messages",
|
||||
"how-tos/memory.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"agents/memory.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"how-tos/memory/manage-conversation-history.ipynb": "how-tos/memory/add-memory.md",
|
||||
"how-tos/memory/delete-messages.ipynb": "how-tos/memory/add-memory.md#delete-messages",
|
||||
"how-tos/memory/add-summary-conversation-history.ipynb": "how-tos/memory/add-memory.md#summarize-messages",
|
||||
"how-tos/memory.ipynb": "how-tos/memory/add-memory.md",
|
||||
"agents/memory.ipynb": "how-tos/memory/add-memory.md",
|
||||
# subgraph how-tos
|
||||
"how-tos/subgraph-transform-state.ipynb": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs#different-state-schemas",
|
||||
"how-tos/subgraphs-manage-state.ipynb": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs#add-persistence",
|
||||
"how-tos/subgraph-transform-state.ipynb": "how-tos/subgraph.md#different-state-schemas",
|
||||
"how-tos/subgraphs-manage-state.ipynb": "how-tos/subgraph.md#add-persistence",
|
||||
# persistence how-tos
|
||||
"how-tos/persistence_postgres.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-in-production",
|
||||
"how-tos/persistence_mongodb.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-in-production",
|
||||
"how-tos/persistence_redis.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-in-production",
|
||||
"how-tos/subgraph-persistence.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#use-with-subgraphs",
|
||||
"how-tos/cross-thread-persistence.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory#add-long-term-memory",
|
||||
"cloud/how-tos/copy_threads": "https://docs.langchain.com/langsmith/use-threads",
|
||||
"cloud/how-tos/check-thread-status": "https://docs.langchain.com/langsmith/use-threads",
|
||||
"cloud/concepts/threads.md": "https://docs.langchain.com/oss/python/langgraph/persistence#threads",
|
||||
"how-tos/persistence.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"how-tos/persistence_postgres.ipynb": "how-tos/memory/add-memory.md#use-in-production",
|
||||
"how-tos/persistence_mongodb.ipynb": "how-tos/memory/add-memory.md#use-in-production",
|
||||
"how-tos/persistence_redis.ipynb": "how-tos/memory/add-memory.md#use-in-production",
|
||||
"how-tos/subgraph-persistence.ipynb": "how-tos/memory/add-memory.md#use-with-subgraphs",
|
||||
"how-tos/cross-thread-persistence.ipynb": "how-tos/memory/add-memory.md#add-long-term-memory",
|
||||
"cloud/how-tos/copy_threads": "cloud/how-tos/use_threads",
|
||||
"cloud/how-tos/check-thread-status": "cloud/how-tos/use_threads",
|
||||
"cloud/concepts/threads.md": "concepts/persistence.md#threads",
|
||||
"how-tos/persistence.ipynb": "how-tos/memory/add-memory.md",
|
||||
# tool calling how-tos
|
||||
"how-tos/tool-calling-errors.ipynb": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"how-tos/pass-config-to-tools.ipynb": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"how-tos/pass-run-time-values-to-tools.ipynb": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"how-tos/update-state-from-tools.ipynb": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"agents/tools.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"how-tos/tool-calling-errors.ipynb": "how-tos/tool-calling.ipynb#handle-errors",
|
||||
"how-tos/pass-config-to-tools.ipynb": "how-tos/tool-calling.ipynb#access-config",
|
||||
"how-tos/pass-run-time-values-to-tools.ipynb": "how-tos/tool-calling.ipynb#read-state",
|
||||
"how-tos/update-state-from-tools.ipynb": "how-tos/tool-calling.ipynb#update-state",
|
||||
"agents/tools.md": "how-tos/tool-calling.md",
|
||||
# multi-agent how-tos
|
||||
"how-tos/agent-handoffs.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"how-tos/multi-agent-network.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"how-tos/multi-agent-multi-turn-convo.ipynb": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"how-tos/agent-handoffs.ipynb": "how-tos/multi_agent.md#handoffs",
|
||||
"how-tos/multi-agent-network.ipynb": "how-tos/multi_agent.md#use-in-a-multi-agent-system",
|
||||
"how-tos/multi-agent-multi-turn-convo.ipynb": "how-tos/multi_agent.md#multi-turn-conversation",
|
||||
# cloud redirects
|
||||
"cloud/index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"cloud/how-tos/index.md": "https://docs.langchain.com/langsmith/home",
|
||||
"cloud/concepts/api.md": "https://docs.langchain.com/langsmith/langgraph-server",
|
||||
"cloud/concepts/cloud.md": "https://docs.langchain.com/langsmith/cloud",
|
||||
"cloud/faq/studio.md": "https://docs.langchain.com/langsmith/studio",
|
||||
"cloud/how-tos/human_in_the_loop_edit_state.md": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
||||
"cloud/how-tos/human_in_the_loop_user_input.md": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
||||
"concepts/platform_architecture.md": "https://docs.langchain.com/langsmith/cloud#architecture",
|
||||
"cloud/index.md": "index.md",
|
||||
"cloud/how-tos/index.md": "concepts/langgraph_platform",
|
||||
"cloud/concepts/api.md": "concepts/langgraph_server.md",
|
||||
"cloud/concepts/cloud.md": "concepts/langgraph_cloud.md",
|
||||
"cloud/faq/studio.md": "concepts/langgraph_studio.md#studio-faqs",
|
||||
"cloud/how-tos/human_in_the_loop_edit_state.md": "cloud/how-tos/add-human-in-the-loop.md",
|
||||
"cloud/how-tos/human_in_the_loop_user_input.md": "cloud/how-tos/add-human-in-the-loop.md",
|
||||
"concepts/platform_architecture.md": "concepts/langgraph_cloud#architecture",
|
||||
# cloud streaming redirects
|
||||
"cloud/how-tos/stream_values.md": "https://docs.langchain.com/langsmith/streaming",
|
||||
"cloud/how-tos/stream_updates.md": "https://docs.langchain.com/langsmith/streaming",
|
||||
@@ -94,39 +94,39 @@ REDIRECT_MAP = {
|
||||
"cloud/how-tos/stream_events.md": "https://docs.langchain.com/langsmith/streaming",
|
||||
"cloud/how-tos/stream_debug.md": "https://docs.langchain.com/langsmith/streaming",
|
||||
"cloud/how-tos/stream_multiple.md": "https://docs.langchain.com/langsmith/streaming",
|
||||
"cloud/concepts/streaming.md": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"agents/streaming.md": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"cloud/concepts/streaming.md": "concepts/streaming.md",
|
||||
"agents/streaming.md": "how-tos/streaming.md",
|
||||
# prebuilt redirects
|
||||
"how-tos/create-react-agent.ipynb": "https://docs.langchain.com/oss/python/langchain/agents#basic-configuration",
|
||||
"how-tos/create-react-agent-memory.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"how-tos/create-react-agent-system-prompt.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"how-tos/create-react-agent-structured-output.ipynb": "https://docs.langchain.com/oss/python/langchain/agents#structured-output",
|
||||
"how-tos/create-react-agent.ipynb": "agents/agents.md#basic-configuration",
|
||||
"how-tos/create-react-agent-memory.ipynb": "agents/memory.md",
|
||||
"how-tos/create-react-agent-system-prompt.ipynb": "agents/context.md#prompts",
|
||||
"how-tos/create-react-agent-structured-output.ipynb": "agents/agents.md#structured-output",
|
||||
# misc
|
||||
"prebuilt.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
||||
"reference/prebuilt.md": "https://reference.langchain.com/python/langgraph/agents/",
|
||||
"concepts/high_level.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"concepts/index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"concepts/v0-human-in-the-loop.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"how-tos/index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"tutorials/introduction.ipynb": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"agents/deployment.md": "https://docs.langchain.com/oss/python/langgraph/local-server",
|
||||
"prebuilt.md": "agents/prebuilt.md",
|
||||
"reference/prebuilt.md": "reference/agents.md",
|
||||
"concepts/high_level.md": "index.md",
|
||||
"concepts/index.md": "index.md",
|
||||
"concepts/v0-human-in-the-loop.md": "concepts/human-in-the-loop.md",
|
||||
"how-tos/index.md": "index.md",
|
||||
"tutorials/introduction.ipynb": "concepts/why-langgraph.md",
|
||||
"agents/deployment.md": "tutorials/langgraph-platform/local-server.md",
|
||||
# deployment redirects
|
||||
"how-tos/deploy-self-hosted.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"concepts/self_hosted.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"tutorials/deployment.md": "https://docs.langchain.com/langsmith/deployments",
|
||||
"how-tos/deploy-self-hosted.md": "cloud/deployment/self_hosted_data_plane.md",
|
||||
"concepts/self_hosted.md": "concepts/langgraph_self_hosted_data_plane.md",
|
||||
"tutorials/deployment.md": "concepts/deployment_options.md",
|
||||
# assistant redirects
|
||||
"cloud/how-tos/assistant_versioning.md": "https://docs.langchain.com/langsmith/configuration-cloud",
|
||||
"cloud/concepts/runs.md": "https://docs.langchain.com/langsmith/assistants#execution",
|
||||
"cloud/how-tos/assistant_versioning.md": "cloud/how-tos/configuration_cloud.md",
|
||||
"cloud/concepts/runs.md": "concepts/assistants.md#execution",
|
||||
# hitl redirects
|
||||
"how-tos/wait-user-input-functional.ipynb": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
||||
"how-tos/review-tool-calls-functional.ipynb": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
||||
"how-tos/create-react-agent-hitl.ipynb": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"agents/human-in-the-loop.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"how-tos/human_in_the_loop/dynamic_breakpoints.ipynb": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"concepts/breakpoints.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"how-tos/human_in_the_loop/breakpoints.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"cloud/how-tos/human_in_the_loop_breakpoint.md": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
||||
"how-tos/human_in_the_loop/edit-graph-state.ipynb": "https://docs.langchain.com/oss/python/langgraph/use-time-travel",
|
||||
"how-tos/wait-user-input-functional.ipynb": "how-tos/use-functional-api.md",
|
||||
"how-tos/review-tool-calls-functional.ipynb": "how-tos/use-functional-api.md",
|
||||
"how-tos/create-react-agent-hitl.ipynb": "how-tos/human_in_the_loop/add-human-in-the-loop.md",
|
||||
"agents/human-in-the-loop.md": "how-tos/human_in_the_loop/add-human-in-the-loop.md",
|
||||
"how-tos/human_in_the_loop/dynamic_breakpoints.ipynb": "how-tos/human_in_the_loop/breakpoints.md",
|
||||
"concepts/breakpoints.md": "concepts/human_in_the_loop.md",
|
||||
"how-tos/human_in_the_loop/breakpoints.md": "how-tos/human_in_the_loop/add-human-in-the-loop.md",
|
||||
"cloud/how-tos/human_in_the_loop_breakpoint.md": "cloud/how-tos/add-human-in-the-loop.md",
|
||||
"how-tos/human_in_the_loop/edit-graph-state.ipynb": "how-tos/human_in_the_loop/time-travel.md",
|
||||
|
||||
# LGP mintlify migration redirects
|
||||
"tutorials/auth/getting_started.md": "https://docs.langchain.com/langsmith/auth",
|
||||
@@ -141,7 +141,7 @@ REDIRECT_MAP = {
|
||||
"concepts/langgraph_server.md": "https://docs.langchain.com/langsmith/langgraph-server",
|
||||
"concepts/langgraph_data_plane.md": "https://docs.langchain.com/langsmith/data-plane",
|
||||
"concepts/langgraph_control_plane.md": "https://docs.langchain.com/langsmith/control-plane",
|
||||
"concepts/langgraph_cli.md": "https://docs.langchain.com/langsmith/cli",
|
||||
"concepts/langgraph_cli.md": "https://docs.langchain.com/langsmith/langgraph-cli",
|
||||
"concepts/langgraph_studio.md": "https://docs.langchain.com/langsmith/studio",
|
||||
"cloud/how-tos/studio/quick_start.md": "https://docs.langchain.com/langsmith/quick-start-studio",
|
||||
"cloud/how-tos/invoke_studio.md": "https://docs.langchain.com/langsmith/use-studio#run-application",
|
||||
@@ -206,234 +206,6 @@ REDIRECT_MAP = {
|
||||
"cloud/reference/cli.md": "https://docs.langchain.com/langsmith/cli",
|
||||
"cloud/reference/env_var.md": "https://docs.langchain.com/langsmith/env-var",
|
||||
"troubleshooting/studio.md": "https://docs.langchain.com/langsmith/troubleshooting-studio",
|
||||
|
||||
# LangGraph mintlify migration redirects
|
||||
"index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"agents/agents.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
||||
"concepts/why-langgraph.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"tutorials/get-started/1-build-basic-chatbot.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"tutorials/get-started/2-add-tools.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"tutorials/get-started/3-add-memory.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"tutorials/get-started/4-human-in-the-loop.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"tutorials/get-started/5-customize-state.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"tutorials/get-started/6-time-travel.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"tutorials/langsmith/local-server.md": "https://docs.langchain.com/oss/python/langgraph/local-server",
|
||||
"tutorials/workflows.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"concepts/agentic_concepts.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"guides/index.md": "https://docs.langchain.com/oss/python/langchain/overview",
|
||||
"agents/overview.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
||||
"agents/run_agents.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"concepts/low_level.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"how-tos/graph-api.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"concepts/functional_api.md": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
||||
"how-tos/use-functional-api.md": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
||||
"concepts/pregel.md": "https://docs.langchain.com/oss/python/langgraph/pregel",
|
||||
"concepts/streaming.md": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"how-tos/streaming.md": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"concepts/persistence.md": "https://docs.langchain.com/oss/python/langgraph/persistence",
|
||||
"concepts/durable_execution.md": "https://docs.langchain.com/oss/python/langgraph/durable-execution",
|
||||
"concepts/memory.md": "https://docs.langchain.com/oss/python/langgraph/memory",
|
||||
"how-tos/memory/add-memory.md": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"agents/context.md": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"agents/models.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"concepts/tools.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"how-tos/tool-calling.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"concepts/human_in_the_loop.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"how-tos/human_in_the_loop/add-human-in-the-loop.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"concepts/time-travel.md": "https://docs.langchain.com/oss/python/langgraph/persistence",
|
||||
"how-tos/human_in_the_loop/time-travel.md": "https://docs.langchain.com/oss/python/langgraph/use-time-travel",
|
||||
"concepts/subgraphs.md": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs",
|
||||
"how-tos/subgraph.md": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs",
|
||||
"concepts/multi_agent.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"agents/multi-agent.md": "https://docs.langchain.com/oss/python/langchain/multi-agent",
|
||||
"how-tos/multi_agent.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"concepts/mcp.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"agents/mcp.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"concepts/tracing.md": "https://docs.langchain.com/oss/python/langgraph/observability",
|
||||
"how-tos/enable-tracing.md": "https://docs.langchain.com/oss/python/langgraph/observability",
|
||||
"agents/evals.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"examples/index.md": "https://docs.langchain.com/oss/python/langgraph/case-studies",
|
||||
"concepts/template_applications.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"tutorials/rag/langgraph_agentic_rag.md": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
|
||||
"tutorials/multi_agent/agent_supervisor.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"tutorials/sql/sql-agent.md": "https://docs.langchain.com/oss/python/langgraph/sql-agent",
|
||||
"agents/ui.md": "https://docs.langchain.com/oss/python/langgraph/ui",
|
||||
"how-tos/run-id-langsmith.md": "https://docs.langchain.com/oss/python/langgraph/observability",
|
||||
"troubleshooting/errors/index.md": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
||||
"troubleshooting/errors/INVALID_CHAT_HISTORY.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_CHAT_HISTORY",
|
||||
"troubleshooting/errors/INVALID_LICENSE.md": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
||||
"adopters.md": "https://docs.langchain.com/oss/python/langgraph/case-studies",
|
||||
"concepts/faq.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"agents/prebuilt.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
||||
"reference/index.md": "https://reference.langchain.com/python/langgraph/",
|
||||
"reference/graphs.md": "https://reference.langchain.com/python/langgraph/graphs/",
|
||||
"reference/func.md": "https://reference.langchain.com/python/langgraph/func/",
|
||||
"reference/pregel.md": "https://reference.langchain.com/python/langgraph/pregel/",
|
||||
"reference/checkpoints.md": "https://reference.langchain.com/python/langgraph/checkpoints/",
|
||||
"reference/store.md": "https://reference.langchain.com/python/langgraph/store/",
|
||||
"reference/cache.md": "https://reference.langchain.com/python/langgraph/cache/",
|
||||
"reference/types.md": "https://reference.langchain.com/python/langgraph/types/",
|
||||
"reference/runtime.md": "https://reference.langchain.com/python/langgraph/runtime/",
|
||||
"reference/config.md": "https://reference.langchain.com/python/langgraph/config/",
|
||||
"reference/errors.md": "https://reference.langchain.com/python/langgraph/errors/",
|
||||
"reference/constants.md": "https://reference.langchain.com/python/langgraph/constants/",
|
||||
"reference/channels.md": "https://reference.langchain.com/python/langgraph/channels/",
|
||||
"reference/agents.md": "https://reference.langchain.com/python/langgraph/agents/",
|
||||
"reference/supervisor.md": "https://reference.langchain.com/python/langgraph/supervisor/",
|
||||
"reference/swarm.md": "https://reference.langchain.com/python/langgraph/swarm/",
|
||||
"reference/mcp.md": "https://reference.langchain.com/python/langgraph/mcp/",
|
||||
"cloud/reference/sdk/python_sdk_ref.md": "https://reference.langchain.com/python/platform/python_sdk/",
|
||||
"reference/remote_graph.md": "https://reference.langchain.com/python/platform/remote_graph/",
|
||||
|
||||
# additional exclude-search entries from mkdocs.yml
|
||||
"additional-resources/index.md": "https://docs.langchain.com/oss/python/langchain/overview",
|
||||
"cloud/concepts/cron_jobs.md": "https://docs.langchain.com/langsmith/cron-jobs",
|
||||
"cloud/concepts/data_storage_and_privacy.md": "https://docs.langchain.com/langsmith/data-storage-and-privacy",
|
||||
"cloud/concepts/webhooks.md": "https://docs.langchain.com/langsmith/use-webhooks",
|
||||
"cloud/deployment/cloud.md": "https://docs.langchain.com/langsmith/cloud",
|
||||
"cloud/deployment/custom_docker.md": "https://docs.langchain.com/langsmith/custom-docker",
|
||||
"cloud/deployment/egress.md": "https://docs.langchain.com/langsmith/env-var",
|
||||
"cloud/deployment/graph_rebuild.md": "https://docs.langchain.com/langsmith/graph-rebuild",
|
||||
"cloud/deployment/self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"cloud/deployment/self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"cloud/deployment/semantic_search.md": "https://docs.langchain.com/langsmith/semantic-search",
|
||||
"cloud/deployment/setup_javascript.md": "https://docs.langchain.com/langsmith/setup-javascript",
|
||||
"cloud/deployment/setup_pyproject.md": "https://docs.langchain.com/langsmith/setup-pyproject",
|
||||
"cloud/deployment/setup.md": "https://docs.langchain.com/langsmith/setup-app-requirements-txt",
|
||||
"cloud/deployment/standalone_container.md": "https://docs.langchain.com/langsmith/docker",
|
||||
"cloud/how-tos/add-human-in-the-loop.md": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
||||
"cloud/how-tos/background_run.md": "https://docs.langchain.com/langsmith/background-run",
|
||||
"cloud/how-tos/clone_traces_studio.md": "https://docs.langchain.com/langsmith/observability",
|
||||
"cloud/how-tos/configurable_headers.md": "https://docs.langchain.com/langsmith/configurable-headers",
|
||||
"cloud/how-tos/configuration_cloud.md": "https://docs.langchain.com/langsmith/configuration-cloud",
|
||||
"cloud/how-tos/cron_jobs.md": "https://docs.langchain.com/langsmith/cron-jobs",
|
||||
"cloud/how-tos/datasets_studio.md": "https://docs.langchain.com/langsmith/use-studio",
|
||||
"cloud/how-tos/enqueue_concurrent.md": "https://docs.langchain.com/langsmith/enqueue-concurrent",
|
||||
"cloud/how-tos/generative_ui_react.md": "https://docs.langchain.com/langsmith/generative-ui-react",
|
||||
"cloud/how-tos/human_in_the_loop_time_travel.md": "https://docs.langchain.com/langsmith/human-in-the-loop-time-travel",
|
||||
"cloud/how-tos/interrupt_concurrent.md": "https://docs.langchain.com/langsmith/interrupt-concurrent",
|
||||
"cloud/how-tos/invoke_studio.md": "https://docs.langchain.com/langsmith/use-studio",
|
||||
"cloud/how-tos/iterate_graph_studio.md": "https://docs.langchain.com/langsmith/use-studio",
|
||||
"cloud/how-tos/reject_concurrent.md": "https://docs.langchain.com/langsmith/reject-concurrent",
|
||||
"cloud/how-tos/rollback_concurrent.md": "https://docs.langchain.com/langsmith/rollback-concurrent",
|
||||
"cloud/how-tos/same-thread.md": "https://docs.langchain.com/langsmith/same-thread",
|
||||
"cloud/how-tos/stateless_runs.md": "https://docs.langchain.com/langsmith/stateless-runs",
|
||||
"cloud/how-tos/streaming.md": "https://docs.langchain.com/langsmith/streaming",
|
||||
"cloud/how-tos/studio/manage_assistants.md": "https://docs.langchain.com/langsmith/use-studio",
|
||||
"cloud/how-tos/studio/quick_start.md": "https://docs.langchain.com/langsmith/quick-start-studio",
|
||||
"cloud/how-tos/studio/run_evals.md": "https://docs.langchain.com/langsmith/observability",
|
||||
"cloud/how-tos/threads_studio.md": "https://docs.langchain.com/langsmith/use-threads",
|
||||
"cloud/how-tos/use_stream_react.md": "https://docs.langchain.com/langsmith/use-stream-react",
|
||||
"cloud/how-tos/use_threads.md": "https://docs.langchain.com/langsmith/use-threads",
|
||||
"cloud/how-tos/webhooks.md": "https://docs.langchain.com/langsmith/use-webhooks",
|
||||
"cloud/quick_start.md": "https://docs.langchain.com/langsmith/deployment-quickstart",
|
||||
"cloud/reference/api/api_ref_control_plane.md": "https://docs.langchain.com/langsmith/api-ref-control-plane",
|
||||
"cloud/reference/api/api_ref.md": "https://docs.langchain.com/langsmith/server-api-ref",
|
||||
"cloud/reference/cli.md": "https://docs.langchain.com/langsmith/cli",
|
||||
"cloud/reference/env_var.md": "https://docs.langchain.com/langsmith/env-var",
|
||||
"cloud/reference/langgraph_server_changelog.md": "https://docs.langchain.com/langsmith/langgraph-server-changelog",
|
||||
"cloud/reference/sdk/js_ts_sdk_ref.md": "https://reference.langchain.com/javascript/modules/langsmith.html",
|
||||
"concepts/application_structure.md": "https://docs.langchain.com/langsmith/application-structure",
|
||||
"concepts/assistants.md": "https://docs.langchain.com/langsmith/assistants",
|
||||
"concepts/auth.md": "https://docs.langchain.com/langsmith/auth",
|
||||
"concepts/deployment_options.md": "https://docs.langchain.com/langsmith/deployments",
|
||||
"concepts/double_texting.md": "https://docs.langchain.com/langsmith/double-texting",
|
||||
"concepts/faq.md": "https://docs.langchain.com/langsmith/faq",
|
||||
"concepts/langgraph_cli.md": "https://docs.langchain.com/langsmith/cli",
|
||||
"concepts/langgraph_cloud.md": "https://docs.langchain.com/langsmith/cloud",
|
||||
"concepts/langgraph_components.md": "https://docs.langchain.com/langsmith/components",
|
||||
"concepts/langgraph_control_plane.md": "https://docs.langchain.com/langsmith/control-plane",
|
||||
"concepts/langgraph_data_plane.md": "https://docs.langchain.com/langsmith/data-plane",
|
||||
"concepts/langgraph_platform.md": "https://docs.langchain.com/langsmith/home",
|
||||
"concepts/langgraph_self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"concepts/langgraph_self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"concepts/langgraph_server.md": "https://docs.langchain.com/langsmith/langgraph-server",
|
||||
"concepts/langgraph_standalone_container.md": "https://docs.langchain.com/langsmith/docker",
|
||||
"concepts/langgraph_studio.md": "https://docs.langchain.com/langsmith/studio",
|
||||
"concepts/plans.md": "https://docs.langchain.com/langsmith/home",
|
||||
"concepts/scalability_and_resilience.md": "https://docs.langchain.com/langsmith/scalability-and-resilience",
|
||||
"concepts/sdk.md": "https://docs.langchain.com/langsmith/sdk",
|
||||
"concepts/server-mcp.md": "https://docs.langchain.com/langsmith/server-mcp",
|
||||
"concepts/template_applications.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"concepts/why-langgraph.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"examples/index.md": "https://docs.langchain.com/oss/python/langgraph/case-studies",
|
||||
"guides/index.md": "https://docs.langchain.com/oss/python/langchain/overview",
|
||||
"how-tos/auth/custom_auth.md": "https://docs.langchain.com/langsmith/custom-auth",
|
||||
"how-tos/auth/openapi_security.md": "https://docs.langchain.com/langsmith/openapi-security",
|
||||
"how-tos/autogen-integration.md": "https://docs.langchain.com/langsmith/autogen-integration",
|
||||
"how-tos/http/custom_lifespan.md": "https://docs.langchain.com/langsmith/custom-lifespan",
|
||||
"how-tos/http/custom_middleware.md": "https://docs.langchain.com/langsmith/custom-middleware",
|
||||
"how-tos/http/custom_routes.md": "https://docs.langchain.com/langsmith/custom-routes",
|
||||
"how-tos/ttl/configure_ttl.md": "https://docs.langchain.com/langsmith/configure-ttl",
|
||||
"how-tos/use-remote-graph.md": "https://docs.langchain.com/langsmith/use-remote-graph",
|
||||
"index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"snippets/chat_model_tabs.md": "https://docs.langchain.com/oss/python/langchain/overview",
|
||||
"troubleshooting/errors/GRAPH_RECURSION_LIMIT.md": "https://docs.langchain.com/oss/python/langgraph/GRAPH_RECURSION_LIMIT",
|
||||
"troubleshooting/errors/index.md": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
||||
"troubleshooting/errors/INVALID_CHAT_HISTORY.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_CHAT_HISTORY",
|
||||
"troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_CONCURRENT_GRAPH_UPDATE",
|
||||
"troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_GRAPH_NODE_RETURN_VALUE",
|
||||
"troubleshooting/errors/INVALID_LICENSE.md": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
||||
"troubleshooting/errors/MULTIPLE_SUBGRAPHS.md": "https://docs.langchain.com/oss/python/langgraph/MULTIPLE_SUBGRAPHS",
|
||||
"troubleshooting/studio.md": "https://docs.langchain.com/langsmith/troubleshooting-studio",
|
||||
"tutorials/auth/add_auth_server.md": "https://docs.langchain.com/langsmith/add-auth-server",
|
||||
"tutorials/auth/getting_started.md": "https://docs.langchain.com/langsmith/auth",
|
||||
"tutorials/auth/resource_auth.md": "https://docs.langchain.com/langsmith/resource-auth",
|
||||
"agents/agents.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
||||
"concepts/why-langgraph.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"tutorials/langsmith/local-server.md": "https://docs.langchain.com/oss/python/langgraph/local-server",
|
||||
"tutorials/workflows.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"concepts/agentic_concepts.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"guides/index.md": "https://docs.langchain.com/oss/python/langchain/overview",
|
||||
"agents/overview.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
||||
"concepts/agentic_concepts.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"agents/run_agents.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
|
||||
"concepts/low_level.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"how-tos/graph-api.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"concepts/functional_api.md": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
||||
"how-tos/use-functional-api.md": "https://docs.langchain.com/oss/python/langgraph/functional-api",
|
||||
"concepts/pregel.md": "https://docs.langchain.com/oss/python/langgraph/pregel",
|
||||
"concepts/streaming.md": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"how-tos/streaming.md": "https://docs.langchain.com/oss/python/langgraph/streaming",
|
||||
"concepts/persistence.md": "https://docs.langchain.com/oss/python/langgraph/persistence",
|
||||
"concepts/durable_execution.md": "https://docs.langchain.com/oss/python/langgraph/durable-execution",
|
||||
"concepts/memory.md": "https://docs.langchain.com/oss/python/langgraph/memory",
|
||||
"how-tos/memory/add-memory.md": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"agents/context.md": "https://docs.langchain.com/oss/python/langgraph/add-memory",
|
||||
"agents/models.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"concepts/tools.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"how-tos/tool-calling.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"concepts/human_in_the_loop.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"how-tos/human_in_the_loop/add-human-in-the-loop.md": "https://docs.langchain.com/oss/python/langgraph/interrupts",
|
||||
"concepts/time-travel.md": "https://docs.langchain.com/oss/python/langgraph/persistence",
|
||||
"how-tos/human_in_the_loop/time-travel.md": "https://docs.langchain.com/oss/python/langgraph/use-time-travel",
|
||||
"concepts/subgraphs.md": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs",
|
||||
"how-tos/subgraph.md": "https://docs.langchain.com/oss/python/langgraph/use-subgraphs",
|
||||
"concepts/multi_agent.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"agents/multi-agent.md": "https://docs.langchain.com/oss/python/langchain/multi-agent",
|
||||
"how-tos/multi_agent.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
|
||||
"concepts/mcp.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"agents/mcp.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"concepts/tracing.md": "https://docs.langchain.com/oss/python/langgraph/observability",
|
||||
"how-tos/enable-tracing.md": "https://docs.langchain.com/oss/python/langgraph/observability",
|
||||
"agents/evals.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"examples/index.md": "https://docs.langchain.com/oss/python/langgraph/case-studies",
|
||||
"concepts/template_applications.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"tutorials/rag/langgraph_agentic_rag.md": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
|
||||
"tutorials/multi_agent/agent_supervisor.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
|
||||
"tutorials/sql/sql-agent.md": "https://docs.langchain.com/oss/python/langgraph/sql-agent",
|
||||
"agents/ui.md": "https://docs.langchain.com/oss/python/langgraph/ui",
|
||||
"how-tos/run-id-langsmith.md": "https://docs.langchain.com/oss/python/langgraph/observability",
|
||||
"troubleshooting/errors/index.md": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
||||
"troubleshooting/errors/GRAPH_RECURSION_LIMIT.md": "https://docs.langchain.com/oss/python/langgraph/GRAPH_RECURSION_LIMIT",
|
||||
"troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_CONCURRENT_GRAPH_UPDATE",
|
||||
"troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_GRAPH_NODE_RETURN_VALUE",
|
||||
"troubleshooting/errors/MULTIPLE_SUBGRAPHS.md": "https://docs.langchain.com/oss/python/langgraph/MULTIPLE_SUBGRAPHS",
|
||||
"troubleshooting/errors/INVALID_CHAT_HISTORY.md": "https://docs.langchain.com/oss/python/langgraph/INVALID_CHAT_HISTORY",
|
||||
"troubleshooting/errors/INVALID_LICENSE.md": "https://docs.langchain.com/oss/python/langgraph/common-errors",
|
||||
"adopters.md": "https://docs.langchain.com/oss/python/langgraph/case-studies",
|
||||
"concepts/faq.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"agents/prebuilt.md": "https://docs.langchain.com/oss/python/langchain/agents",
|
||||
}
|
||||
|
||||
|
||||
@@ -788,16 +560,10 @@ def on_post_page(html: str, page: Page, config: MkDocsConfig) -> str:
|
||||
# Create HTML files for redirects after site dir has been built
|
||||
def on_post_build(config):
|
||||
use_directory_urls = config.get("use_directory_urls")
|
||||
site_dir = config["site_dir"]
|
||||
|
||||
# Track which paths have explicit redirects
|
||||
redirected_paths = set()
|
||||
|
||||
# Process explicit redirects from REDIRECT_MAP
|
||||
for page_old, page_new in REDIRECT_MAP.items():
|
||||
# Convert .ipynb to .md for path calculation
|
||||
page_old = page_old.replace(".ipynb", ".md")
|
||||
|
||||
|
||||
# Calculate the HTML path for the old page (whether it exists or not)
|
||||
if use_directory_urls:
|
||||
# With directory URLs: /path/to/page/ becomes /path/to/page/index.html
|
||||
@@ -811,18 +577,15 @@ def on_post_build(config):
|
||||
old_html_path = page_old[:-3] + ".html"
|
||||
else:
|
||||
old_html_path = page_old + ".html"
|
||||
|
||||
# Track this path as redirected
|
||||
redirected_paths.add(old_html_path)
|
||||
|
||||
|
||||
if isinstance(page_new, str) and page_new.startswith("http"):
|
||||
# Handle external redirects
|
||||
_write_html(site_dir, old_html_path, page_new)
|
||||
_write_html(config["site_dir"], old_html_path, page_new)
|
||||
else:
|
||||
# Handle internal redirects
|
||||
page_new = page_new.replace(".ipynb", ".md")
|
||||
page_new_before_hash, hash, suffix = page_new.partition("#")
|
||||
|
||||
|
||||
# Try to get the new path using File class, but fallback to manual calculation
|
||||
try:
|
||||
new_html_path = File(page_new_before_hash, "", "", True).url
|
||||
@@ -844,46 +607,5 @@ def on_post_build(config):
|
||||
else:
|
||||
new_html_path = page_new_before_hash + ".html"
|
||||
new_html_path += hash + suffix
|
||||
|
||||
_write_html(site_dir, old_html_path, new_html_path)
|
||||
|
||||
# Create root index.html redirect
|
||||
root_redirect_html = """<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Redirecting to LangGraph Documentation</title>
|
||||
<link rel="canonical" href="https://docs.langchain.com/oss/python/langgraph/overview">
|
||||
<meta name="robots" content="noindex">
|
||||
<script>var anchor=window.location.hash.substr(1);location.href="https://docs.langchain.com/oss/python/langgraph/overview"+(anchor?"#"+anchor:"")</script>
|
||||
<meta http-equiv="refresh" content="0; url=https://docs.langchain.com/oss/python/langgraph/overview">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Documentation has moved</h1>
|
||||
<p>The LangGraph documentation has moved to <a href="https://docs.langchain.com/oss/python/langgraph/overview">docs.langchain.com</a>.</p>
|
||||
<p>Redirecting you now...</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
root_index_path = os.path.join(site_dir, "index.html")
|
||||
with open(root_index_path, "w", encoding="utf-8") as f:
|
||||
f.write(root_redirect_html)
|
||||
|
||||
# Create server-side catch-all redirect file for Netlify/Cloudflare Pages
|
||||
# This handles any pages not explicitly mapped in REDIRECT_MAP
|
||||
# Note: This won't work on GitHub Pages, but kept for potential future use
|
||||
redirects_content = """# Netlify/Cloudflare Pages redirect rules
|
||||
# Specific redirects are handled by individual HTML redirect pages
|
||||
# This is the catch-all for any unmapped pages
|
||||
|
||||
# Exclude reference docs from catch-all
|
||||
/reference/* 200
|
||||
|
||||
# Catch-all: redirect any page not explicitly mapped
|
||||
/* https://docs.langchain.com/oss/python/langgraph/overview 301
|
||||
"""
|
||||
|
||||
redirects_path = os.path.join(site_dir, "_redirects")
|
||||
with open(redirects_path, "w", encoding="utf-8") as f:
|
||||
f.write(redirects_content)
|
||||
|
||||
_write_html(config["site_dir"], old_html_path, new_html_path)
|
||||
@@ -294,9 +294,9 @@ Now that you have a LangGraph app running locally, take your journey further by
|
||||
:::python
|
||||
|
||||
- [Python SDK Reference](../../cloud/reference/sdk/python_sdk_ref.md): Explore the Python SDK API Reference.
|
||||
:::
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
- [JS/TS SDK Reference](../../cloud/reference/sdk/js_ts_sdk_ref.md): Explore the JS/TS SDK API Reference.
|
||||
:::
|
||||
:::
|
||||
+98
-62
@@ -149,67 +149,6 @@ plugins:
|
||||
- tutorials/auth/add_auth_server.md
|
||||
- tutorials/auth/getting_started.md
|
||||
- tutorials/auth/resource_auth.md
|
||||
- agents/agents.md
|
||||
- concepts/why-langgraph.md
|
||||
- tutorials/get-started/1-build-basic-chatbot.md
|
||||
- tutorials/get-started/2-add-tools.md
|
||||
- tutorials/get-started/3-add-memory.md
|
||||
- tutorials/get-started/4-human-in-the-loop.md
|
||||
- tutorials/get-started/5-customize-state.md
|
||||
- tutorials/get-started/6-time-travel.md
|
||||
- tutorials/langgraph-platform/local-server.md
|
||||
- tutorials/workflows.md
|
||||
- concepts/agentic_concepts.md
|
||||
- guides/index.md
|
||||
- agents/overview.md
|
||||
- agents/run_agents.md
|
||||
- concepts/low_level.md
|
||||
- how-tos/graph-api.md
|
||||
- concepts/functional_api.md
|
||||
- how-tos/use-functional-api.md
|
||||
- concepts/pregel.md
|
||||
- concepts/streaming.md
|
||||
- how-tos/streaming.md
|
||||
- concepts/persistence.md
|
||||
- concepts/durable_execution.md
|
||||
- concepts/memory.md
|
||||
- how-tos/memory/add-memory.md
|
||||
- agents/context.md
|
||||
- agents/models.md
|
||||
- concepts/tools.md
|
||||
- how-tos/tool-calling.md
|
||||
- concepts/human_in_the_loop.md
|
||||
- how-tos/human_in_the_loop/add-human-in-the-loop.md
|
||||
- concepts/time-travel.md
|
||||
- how-tos/human_in_the_loop/time-travel.md
|
||||
- concepts/subgraphs.md
|
||||
- how-tos/subgraph.md
|
||||
- concepts/multi_agent.md
|
||||
- agents/multi-agent.md
|
||||
- how-tos/multi_agent.md
|
||||
- concepts/mcp.md
|
||||
- agents/mcp.md
|
||||
- concepts/tracing.md
|
||||
- how-tos/enable-tracing.md
|
||||
- agents/evals.md
|
||||
- examples/index.md
|
||||
- concepts/template_applications.md # TODO: make tutorial
|
||||
- tutorials/rag/langgraph_agentic_rag.md
|
||||
- tutorials/multi_agent/agent_supervisor.md
|
||||
- tutorials/sql/sql-agent.md
|
||||
- agents/ui.md
|
||||
- how-tos/run-id-langsmith.md
|
||||
- troubleshooting/errors/index.md
|
||||
- troubleshooting/errors/GRAPH_RECURSION_LIMIT.md
|
||||
- troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md
|
||||
- troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md
|
||||
- troubleshooting/errors/MULTIPLE_SUBGRAPHS.md
|
||||
- troubleshooting/errors/INVALID_CHAT_HISTORY.md
|
||||
- troubleshooting/errors/INVALID_LICENSE.md
|
||||
- adopters.md
|
||||
- concepts/faq.md
|
||||
- agents/prebuilt.md # NOTE: prebuilt.md is auto-generated by `make build-prebuilt`
|
||||
|
||||
- tags
|
||||
- include-markdown
|
||||
- mkdocstrings:
|
||||
@@ -247,6 +186,75 @@ plugins:
|
||||
- "!^_"
|
||||
|
||||
nav:
|
||||
- Get started:
|
||||
- index.md
|
||||
- Quickstarts:
|
||||
- Start with a prebuilt agent: agents/agents.md
|
||||
- Build a custom workflow:
|
||||
- concepts/why-langgraph.md
|
||||
- 1. Build a basic chatbot: tutorials/get-started/1-build-basic-chatbot.md
|
||||
- 2. Add tools: tutorials/get-started/2-add-tools.md
|
||||
- 3. Add memory: tutorials/get-started/3-add-memory.md
|
||||
- 4. Add human-in-the-loop: tutorials/get-started/4-human-in-the-loop.md
|
||||
- 5. Customize state: tutorials/get-started/5-customize-state.md
|
||||
- 6. Time travel: tutorials/get-started/6-time-travel.md
|
||||
- Run a local server: tutorials/langgraph-platform/local-server.md
|
||||
- General concepts:
|
||||
- Workflows & agents: tutorials/workflows.md
|
||||
- Agent architectures: concepts/agentic_concepts.md
|
||||
|
||||
- Guides:
|
||||
- guides/index.md
|
||||
- Agent development:
|
||||
- Overview: agents/overview.md
|
||||
- Run an agent: agents/run_agents.md
|
||||
- LangGraph APIs:
|
||||
- Graph API:
|
||||
- Overview: concepts/low_level.md
|
||||
- Use the Graph API: how-tos/graph-api.md
|
||||
- Functional API:
|
||||
- Overview: concepts/functional_api.md
|
||||
- Use the Functional API: how-tos/use-functional-api.md
|
||||
- Runtime: concepts/pregel.md
|
||||
- Core capabilities:
|
||||
- Streaming:
|
||||
- Overview: concepts/streaming.md
|
||||
- Stream outputs: how-tos/streaming.md
|
||||
- Persistence:
|
||||
- Overview: concepts/persistence.md
|
||||
- Durable execution:
|
||||
- Overview: concepts/durable_execution.md
|
||||
- Memory:
|
||||
- Overview: concepts/memory.md
|
||||
- Add memory: how-tos/memory/add-memory.md
|
||||
- Context:
|
||||
- Add context: agents/context.md
|
||||
- Models:
|
||||
- Configure model: agents/models.md
|
||||
- Tools:
|
||||
- Overview: concepts/tools.md
|
||||
- Call tools: how-tos/tool-calling.md
|
||||
- Human-in-the-loop:
|
||||
- Overview: concepts/human_in_the_loop.md
|
||||
- Add human intervention: how-tos/human_in_the_loop/add-human-in-the-loop.md
|
||||
- Time travel:
|
||||
- Overview: concepts/time-travel.md
|
||||
- Use time travel: how-tos/human_in_the_loop/time-travel.md
|
||||
- Subgraphs:
|
||||
- Overview: concepts/subgraphs.md
|
||||
- Use subgraphs: how-tos/subgraph.md
|
||||
- Multi-agent:
|
||||
- Overview: concepts/multi_agent.md
|
||||
- Prebuilt implementation: agents/multi-agent.md
|
||||
- Custom implementation: how-tos/multi_agent.md
|
||||
- MCP:
|
||||
- Overview: concepts/mcp.md
|
||||
- Use MCP: agents/mcp.md
|
||||
- Tracing:
|
||||
- Overview: concepts/tracing.md
|
||||
- Enable tracing: how-tos/enable-tracing.md
|
||||
- Evaluate performance: agents/evals.md
|
||||
|
||||
- Reference:
|
||||
- reference/index.md
|
||||
- LangGraph:
|
||||
@@ -270,7 +278,35 @@ nav:
|
||||
- LangGraph Platform:
|
||||
- SDK (Python): cloud/reference/sdk/python_sdk_ref.md
|
||||
- SDK (JS/TS): https://langchain-ai.github.io/langgraphjs/reference/modules/sdk.html
|
||||
- RemoteGraph: reference/remote_graph.md
|
||||
- RemoteGraph: reference/remote_graph.md
|
||||
|
||||
- Examples:
|
||||
- examples/index.md
|
||||
- Template applications: concepts/template_applications.md # TODO: make tutorial
|
||||
- Agentic RAG: tutorials/rag/langgraph_agentic_rag.md
|
||||
- Agent Supervisor: tutorials/multi_agent/agent_supervisor.md
|
||||
- SQL agent: tutorials/sql/sql-agent.md
|
||||
- Prebuilt chat UI: agents/ui.md
|
||||
- Graph runs in LangSmith: how-tos/run-id-langsmith.md
|
||||
|
||||
- Additional resources:
|
||||
- additional-resources/index.md
|
||||
- agents/prebuilt.md # NOTE: prebuilt.md is auto-generated by `make build-prebuilt`
|
||||
- LangGraph Academy course: https://academy.langchain.com/courses/intro-to-langgraph
|
||||
- Case studies: adopters.md
|
||||
- concepts/faq.md
|
||||
- llms.txt: llms-txt-overview.md
|
||||
- LangChain Forum: https://forum.langchain.com/
|
||||
- Troubleshooting:
|
||||
- Errors:
|
||||
- troubleshooting/errors/index.md
|
||||
- troubleshooting/errors/GRAPH_RECURSION_LIMIT.md
|
||||
- troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md
|
||||
- troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md
|
||||
- troubleshooting/errors/MULTIPLE_SUBGRAPHS.md
|
||||
- troubleshooting/errors/INVALID_CHAT_HISTORY.md
|
||||
- troubleshooting/errors/INVALID_LICENSE.md
|
||||
|
||||
|
||||
markdown_extensions:
|
||||
- abbr
|
||||
|
||||
@@ -275,11 +275,7 @@ def test_serde_jsonplus_json_mode() -> None:
|
||||
}
|
||||
|
||||
if sys.version_info < (3, 14):
|
||||
expected_result["my_pydantic_v1"] = {
|
||||
"foo": "foo",
|
||||
"bar": 1,
|
||||
"inner": {"hello": "hello"},
|
||||
}
|
||||
expected_result["my_pydantic_v1"] = {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}}
|
||||
expected_result["my_secret_str_v1"] = "meow"
|
||||
|
||||
assert result == expected_result
|
||||
@@ -385,12 +381,7 @@ def test_serde_jsonplus_numpy_array_json_hook(arr: np.ndarray) -> None:
|
||||
"str_col": ["a", None, "c"],
|
||||
}
|
||||
),
|
||||
pytest.param(
|
||||
pd.DataFrame({"cat_col": pd.Categorical(["a", "b", "a", "c"])}),
|
||||
marks=pytest.mark.skipif(
|
||||
sys.version_info >= (3, 14), reason="NotImplementedError on Python 3.14"
|
||||
),
|
||||
),
|
||||
pd.DataFrame({"cat_col": pd.Categorical(["a", "b", "a", "c"])}),
|
||||
pd.DataFrame(
|
||||
{
|
||||
"int8": pd.array([1, 2, 3], dtype="int8"),
|
||||
@@ -418,25 +409,11 @@ def test_serde_jsonplus_numpy_array_json_hook(arr: np.ndarray) -> None:
|
||||
"col3": np.random.rand(1000),
|
||||
}
|
||||
),
|
||||
pytest.param(
|
||||
pd.DataFrame(
|
||||
{
|
||||
"tz_datetime": pd.date_range(
|
||||
"2024-01-01", periods=3, freq="D", tz="UTC"
|
||||
)
|
||||
}
|
||||
),
|
||||
marks=pytest.mark.skipif(
|
||||
sys.version_info >= (3, 14), reason="NotImplementedError on Python 3.14"
|
||||
),
|
||||
pd.DataFrame(
|
||||
{"tz_datetime": pd.date_range("2024-01-01", periods=3, freq="D", tz="UTC")}
|
||||
),
|
||||
pd.DataFrame({"timedelta": pd.to_timedelta([1, 2, 3], unit="D")}),
|
||||
pytest.param(
|
||||
pd.DataFrame({"period": pd.period_range("2024-01", periods=3, freq="M")}),
|
||||
marks=pytest.mark.skipif(
|
||||
sys.version_info >= (3, 14), reason="NotImplementedError on Python 3.14"
|
||||
),
|
||||
),
|
||||
pd.DataFrame({"period": pd.period_range("2024-01", periods=3, freq="M")}),
|
||||
pd.DataFrame({"interval": pd.interval_range(start=0, end=3, periods=3)}),
|
||||
pd.DataFrame({"unicode": ["Hello 🌍", "Python 🐍", "Data 📊"]}),
|
||||
pd.DataFrame({"mixed": [1, "string", [1, 2, 3], {"key": "value"}]}),
|
||||
@@ -473,12 +450,7 @@ def test_serde_jsonplus_pandas_dataframe(df: pd.DataFrame) -> None:
|
||||
pd.Series([1, 2, None]),
|
||||
pd.Series([1.1, None, 3.3]),
|
||||
pd.Series(["a", None, "c"]),
|
||||
pytest.param(
|
||||
pd.Series(pd.Categorical(["a", "b", "a", "c"])),
|
||||
marks=pytest.mark.skipif(
|
||||
sys.version_info >= (3, 14), reason="NotImplementedError on Python 3.14"
|
||||
),
|
||||
),
|
||||
pd.Series(pd.Categorical(["a", "b", "a", "c"])),
|
||||
pd.Series([1, 2, 3], dtype="int8"),
|
||||
pd.Series([10, 20, 30], dtype="int16"),
|
||||
pd.Series([100, 200, 300], dtype="int32"),
|
||||
|
||||
@@ -77,8 +77,6 @@ CONF = cast(Literal["configurable"], sys.intern("configurable"))
|
||||
# key for the configurable dict in RunnableConfig
|
||||
NULL_TASK_ID = sys.intern("00000000-0000-0000-0000-000000000000")
|
||||
# the task_id to use for writes that are not associated with a task
|
||||
OVERWRITE = sys.intern("__overwrite__")
|
||||
# dict key for the overwrite value, used as `{'__overwrite__': value}`
|
||||
|
||||
# redefined to avoid circular import with langgraph.constants
|
||||
_TAG_HIDDEN = sys.intern("langsmith:hidden")
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import collections.abc
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import Any, Generic
|
||||
from typing import Generic
|
||||
|
||||
from typing_extensions import NotRequired, Required, Self
|
||||
|
||||
from langgraph._internal._constants import OVERWRITE
|
||||
from langgraph._internal._typing import MISSING
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.errors import (
|
||||
EmptyChannelError,
|
||||
ErrorCode,
|
||||
InvalidUpdateError,
|
||||
create_error_message,
|
||||
)
|
||||
from langgraph.types import Overwrite
|
||||
from langgraph.errors import EmptyChannelError
|
||||
|
||||
__all__ = ("BinaryOperatorAggregate",)
|
||||
|
||||
@@ -29,15 +22,6 @@ def _strip_extras(t): # type: ignore[no-untyped-def]
|
||||
return t
|
||||
|
||||
|
||||
def _get_overwrite(value: Any) -> tuple[bool, Any]:
|
||||
"""Inspects the given value and returns (is_overwrite, overwrite_value)."""
|
||||
if isinstance(value, Overwrite):
|
||||
return True, value.value
|
||||
if isinstance(value, dict) and set(value.keys()) == {OVERWRITE}:
|
||||
return True, value[OVERWRITE]
|
||||
return False, None
|
||||
|
||||
|
||||
class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
"""Stores the result of applying a binary operator to the current value and each new value.
|
||||
|
||||
@@ -105,21 +89,8 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
if self.value is MISSING:
|
||||
self.value = values[0]
|
||||
values = values[1:]
|
||||
seen_overwrite: bool = False
|
||||
for value in values:
|
||||
is_overwrite, overwrite_value = _get_overwrite(value)
|
||||
if is_overwrite:
|
||||
if seen_overwrite:
|
||||
msg = create_error_message(
|
||||
message="Can receive only one Overwrite value per super-step.",
|
||||
error_code=ErrorCode.INVALID_CONCURRENT_GRAPH_UPDATE,
|
||||
)
|
||||
raise InvalidUpdateError(msg)
|
||||
self.value = overwrite_value
|
||||
seen_overwrite = True
|
||||
continue
|
||||
if not seen_overwrite:
|
||||
self.value = self.operator(self.value, value)
|
||||
self.value = self.operator(self.value, value)
|
||||
return True
|
||||
|
||||
def get(self) -> Value:
|
||||
|
||||
@@ -1113,19 +1113,18 @@ class LazyAtomicCounter:
|
||||
def sanitize_untracked_values_in_send(
|
||||
packet: Send, channels: Mapping[str, BaseChannel]
|
||||
) -> Send:
|
||||
"""Pop any values belonging to UntrackedValue channels in Send.arg for safe checkpointing.
|
||||
"""Pop any UntrackedValue contents in Send.arg for safe checkpointing.
|
||||
|
||||
Send is often called with state to be passed to the dest node, which may contain
|
||||
UntrackedValues at the top level. Send is not typed and arg may be a nested dict."""
|
||||
Send is not typed and arg may be a nested dict. We only look at the top level."""
|
||||
|
||||
if not isinstance(packet.arg, dict):
|
||||
# Command
|
||||
return packet
|
||||
|
||||
# top level keys should be the channel names
|
||||
sanitized_arg = {
|
||||
k: v
|
||||
for k, v in packet.arg.items()
|
||||
if not isinstance(channels.get(k), UntrackedValue)
|
||||
}
|
||||
|
||||
return Send(node=packet.node, arg=sanitized_arg)
|
||||
@@ -516,43 +516,3 @@ def interrupt(value: Any) -> Any:
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Overwrite:
|
||||
"""Bypass a reducer and write the wrapped value directly to a BinaryOperatorAggregate channel.
|
||||
|
||||
Receiving multiple Overwrite values for the same channel in a single super-step will raise an InvalidUpdateError.
|
||||
|
||||
Example:
|
||||
>>> from typing import Annotated
|
||||
>>> import operator
|
||||
>>> from langgraph.graph import StateGraph
|
||||
>>> from langgraph.types import Overwrite
|
||||
>>>
|
||||
>>> class State(TypedDict):
|
||||
... messages: Annotated[list, operator.add]
|
||||
>>>
|
||||
>>> def node_a(state: TypedDict):
|
||||
... # Normal update: uses the reducer (operator.add)
|
||||
... return {"messages": ["a"]}
|
||||
>>>
|
||||
>>> def node_b(state: State):
|
||||
... # Overwrite: bypasses the reducer and replaces the entire value
|
||||
... return {"messages": Overwrite(value=["b"])}
|
||||
>>>
|
||||
>>> builder = StateGraph(State)
|
||||
>>> builder.add_node("node_a", node_a)
|
||||
>>> builder.add_node("node_b", node_b)
|
||||
>>> builder.set_entry_point("node_a")
|
||||
>>> builder.add_edge("node_a", "node_b")
|
||||
>>> graph = builder.compile()
|
||||
>>>
|
||||
>>> # Without Overwrite in node_b, messages would be ["START", "a", "b"]
|
||||
>>> # With Overwrite, messages is just ["b"]
|
||||
>>> result = graph.invoke({"messages": ["START"]})
|
||||
>>> assert result == {"messages": ["b"]}
|
||||
"""
|
||||
|
||||
value: Any
|
||||
"""The value to write directly to the channel, bypassing any reducer."""
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph"
|
||||
version = "1.0.2"
|
||||
version = "1.0.1"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
requires-python = ">=3.10"
|
||||
@@ -27,7 +27,7 @@ dependencies = [
|
||||
"langchain-core>=0.1",
|
||||
"langgraph-checkpoint>=2.1.0,<4.0.0",
|
||||
"langgraph-sdk>=0.2.2,<0.3.0",
|
||||
"langgraph-prebuilt>=1.0.1,<1.1.0",
|
||||
"langgraph-prebuilt>=1.0.0,<1.1.0",
|
||||
"xxhash>=3.5.0",
|
||||
"pydantic>=2.7.4",
|
||||
]
|
||||
@@ -46,7 +46,7 @@ test = [
|
||||
"pytest-watcher",
|
||||
"pytest-xdist[psutil]",
|
||||
"pytest-repeat",
|
||||
"langchain-core>=1.0.0",
|
||||
"langchain-core==1.0.0a1",
|
||||
"langgraph-prebuilt",
|
||||
"langgraph-checkpoint",
|
||||
"langgraph-checkpoint-sqlite",
|
||||
|
||||
@@ -88,10 +88,10 @@
|
||||
'''
|
||||
# ---
|
||||
# name: test_message_graph[memory]
|
||||
'{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAn `AIMessage` is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model and standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI (yielded when streaming).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"default": [], "items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}, "chunk_position": {"anyOf": [{"const": "last", "type": "string"}, {"type": "null"}], "default": null, "title": "Chunk Position"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\n`FunctionMessage` are an older version of the `ToolMessage` schema, and\\ndo not contain the `tool_call_id` field.\\n\\nThe `tool_call_id` field is uLine truncated
|
||||
'{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"default": [], "items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}, "chunk_position": {"anyOf": [{"const": "last", "type": "string"}, {"type": "null"}], "default": null, "title": "Chunk Position"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older versiLine truncated
|
||||
# ---
|
||||
# name: test_message_graph[memory].1
|
||||
'{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAn `AIMessage` is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model and standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI (yielded when streaming).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"default": [], "items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}, "chunk_position": {"anyOf": [{"const": "last", "type": "string"}, {"type": "null"}], "default": null, "title": "Chunk Position"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\n`FunctionMessage` are an older version of the `ToolMessage` schema, and\\ndo not contain the `tool_call_id` field.\\n\\nThe `tool_call_id` field is uLine truncated
|
||||
'{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"default": [], "items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}, "chunk_position": {"anyOf": [{"const": "last", "type": "string"}, {"type": "null"}], "default": null, "title": "Chunk Position"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older versiLine truncated
|
||||
# ---
|
||||
# name: test_message_graph[memory].2
|
||||
'''
|
||||
@@ -175,10 +175,10 @@
|
||||
'''
|
||||
# ---
|
||||
# name: test_prebuilt_tool_chat
|
||||
'{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of a chat model.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}}, "deprecated": true, "description": "The state of the agent.", "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}, "remaining_steps": {"title": "Remaining Steps", "type": "integer"}}, "required": ["messages"], "title": "AgentState", "type": "object"}'
|
||||
'{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}}, "deprecated": true, "description": "The state of the agent.", "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}, "remaining_steps": {"title": "Remaining Steps", "type": "integer"}}, "required": ["messages"], "title": "AgentState", "type": "object"}'
|
||||
# ---
|
||||
# name: test_prebuilt_tool_chat.1
|
||||
'{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of a chat model.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}}, "deprecated": true, "description": "The state of the agent.", "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}, "remaining_steps": {"title": "Remaining Steps", "type": "integer"}}, "required": ["messages"], "title": "AgentState", "type": "object"}'
|
||||
'{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}}, "deprecated": true, "description": "The state of the agent.", "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}, "remaining_steps": {"title": "Remaining Steps", "type": "integer"}}, "required": ["messages"], "title": "AgentState", "type": "object"}'
|
||||
# ---
|
||||
# name: test_prebuilt_tool_chat.2
|
||||
'''
|
||||
@@ -295,13 +295,13 @@
|
||||
__end__([<p>__end__</p>]):::last
|
||||
__start__ --> router_node;
|
||||
router_node -.-> normal_llm_node;
|
||||
router_node -.-> weather_graph\3amodel_node;
|
||||
router_node -.-> weather_graph_model_node;
|
||||
normal_llm_node --> __end__;
|
||||
weather_graph\3aweather_node --> __end__;
|
||||
weather_graph_weather_node --> __end__;
|
||||
subgraph weather_graph
|
||||
weather_graph\3amodel_node(model_node)
|
||||
weather_graph\3aweather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
|
||||
weather_graph\3amodel_node --> weather_graph\3aweather_node;
|
||||
weather_graph_model_node(model_node)
|
||||
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
|
||||
weather_graph_model_node --> weather_graph_weather_node;
|
||||
end
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
classDef first fill-opacity:0
|
||||
|
||||
@@ -666,18 +666,18 @@
|
||||
__end__([<p>__end__</p>]):::last
|
||||
__start__ --> uno;
|
||||
uno -.-> dos;
|
||||
uno -.-> subgraph\3aone;
|
||||
uno -.-> subgraph_one;
|
||||
dos --> __end__;
|
||||
subgraph\3a__end__ --> __end__;
|
||||
subgraph___end__ --> __end__;
|
||||
subgraph subgraph
|
||||
subgraph\3aone(one)
|
||||
subgraph\3atwo(two)
|
||||
subgraph\3athree(three)
|
||||
subgraph\3a__end__(<p>__end__</p>)
|
||||
subgraph\3aone -.-> subgraph\3athree;
|
||||
subgraph\3aone -.-> subgraph\3atwo;
|
||||
subgraph\3athree --> subgraph\3a__end__;
|
||||
subgraph\3atwo --> subgraph\3a__end__;
|
||||
subgraph_one(one)
|
||||
subgraph_two(two)
|
||||
subgraph_three(three)
|
||||
subgraph___end__(<p>__end__</p>)
|
||||
subgraph_one -.-> subgraph_three;
|
||||
subgraph_one -.-> subgraph_two;
|
||||
subgraph_three --> subgraph___end__;
|
||||
subgraph_two --> subgraph___end__;
|
||||
end
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
classDef first fill-opacity:0
|
||||
@@ -705,11 +705,11 @@
|
||||
__start__([<p>__start__</p>]):::first
|
||||
side(side)
|
||||
__end__([<p>__end__</p>]):::last
|
||||
__start__ --> inner\3aup;
|
||||
inner\3aup --> side;
|
||||
__start__ --> inner_up;
|
||||
inner_up --> side;
|
||||
side --> __end__;
|
||||
subgraph inner
|
||||
inner\3aup(up)
|
||||
inner_up(up)
|
||||
end
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
classDef first fill-opacity:0
|
||||
@@ -868,19 +868,19 @@
|
||||
__end__([<p>__end__</p>]):::last
|
||||
__start__ -.-> tool_one;
|
||||
__start__ -.-> tool_three;
|
||||
__start__ -.-> tool_two\3a__start__;
|
||||
__start__ -.-> tool_two___start__;
|
||||
tool_one --> __end__;
|
||||
tool_three --> __end__;
|
||||
tool_two\3a__end__ --> __end__;
|
||||
tool_two___end__ --> __end__;
|
||||
subgraph tool_two
|
||||
tool_two\3a__start__(<p>__start__</p>)
|
||||
tool_two\3atool_two_slow(tool_two_slow)
|
||||
tool_two\3atool_two_fast(tool_two_fast)
|
||||
tool_two\3a__end__(<p>__end__</p>)
|
||||
tool_two\3a__start__ -.-> tool_two\3atool_two_fast;
|
||||
tool_two\3a__start__ -.-> tool_two\3atool_two_slow;
|
||||
tool_two\3atool_two_fast --> tool_two\3a__end__;
|
||||
tool_two\3atool_two_slow --> tool_two\3a__end__;
|
||||
tool_two___start__(<p>__start__</p>)
|
||||
tool_two_tool_two_slow(tool_two_slow)
|
||||
tool_two_tool_two_fast(tool_two_fast)
|
||||
tool_two___end__(<p>__end__</p>)
|
||||
tool_two___start__ -.-> tool_two_tool_two_fast;
|
||||
tool_two___start__ -.-> tool_two_tool_two_slow;
|
||||
tool_two_tool_two_fast --> tool_two___end__;
|
||||
tool_two_tool_two_slow --> tool_two___end__;
|
||||
end
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
classDef first fill-opacity:0
|
||||
@@ -891,13 +891,13 @@
|
||||
# name: test_repeat_condition
|
||||
'''
|
||||
graph TD;
|
||||
Call\20Tool -.-> Chart\20Generator;
|
||||
Call\20Tool -.-> Researcher;
|
||||
Chart\20Generator -. call_tool .-> Call\20Tool;
|
||||
Chart\20Generator -. continue .-> Researcher;
|
||||
Chart\20Generator -. end .-> __end__;
|
||||
Researcher -. call_tool .-> Call\20Tool;
|
||||
Researcher -. continue .-> Chart\20Generator;
|
||||
Call_Tool -.-> Chart_Generator;
|
||||
Call_Tool -.-> Researcher;
|
||||
Chart_Generator -. call_tool .-> Call_Tool;
|
||||
Chart_Generator -. continue .-> Researcher;
|
||||
Chart_Generator -. end .-> __end__;
|
||||
Researcher -. call_tool .-> Call_Tool;
|
||||
Researcher -. continue .-> Chart_Generator;
|
||||
Researcher -. end .-> __end__;
|
||||
__start__ --> Researcher;
|
||||
Researcher -. redo .-> Researcher;
|
||||
@@ -939,25 +939,25 @@
|
||||
__end__([<p>__end__</p>]):::last
|
||||
__start__ --> gp_one;
|
||||
gp_one -. 1 .-> __end__;
|
||||
gp_one -. 0 .-> gp_two\3a__start__;
|
||||
gp_two\3a__end__ --> gp_one;
|
||||
gp_one -. 0 .-> gp_two___start__;
|
||||
gp_two___end__ --> gp_one;
|
||||
subgraph gp_two
|
||||
gp_two\3a__start__(<p>__start__</p>)
|
||||
gp_two\3ap_one(p_one)
|
||||
gp_two\3a__end__(<p>__end__</p>)
|
||||
gp_two\3a__start__ --> gp_two\3ap_one;
|
||||
gp_two\3ap_one -. 1 .-> gp_two\3a__end__;
|
||||
gp_two\3ap_one -. 0 .-> gp_two\3ap_two\3a__start__;
|
||||
gp_two\3ap_two\3a__end__ --> gp_two\3ap_one;
|
||||
gp_two___start__(<p>__start__</p>)
|
||||
gp_two_p_one(p_one)
|
||||
gp_two___end__(<p>__end__</p>)
|
||||
gp_two___start__ --> gp_two_p_one;
|
||||
gp_two_p_one -. 1 .-> gp_two___end__;
|
||||
gp_two_p_one -. 0 .-> gp_two_p_two___start__;
|
||||
gp_two_p_two___end__ --> gp_two_p_one;
|
||||
subgraph p_two
|
||||
gp_two\3ap_two\3a__start__(<p>__start__</p>)
|
||||
gp_two\3ap_two\3ac_one(c_one)
|
||||
gp_two\3ap_two\3ac_two(c_two)
|
||||
gp_two\3ap_two\3a__end__(<p>__end__</p>)
|
||||
gp_two\3ap_two\3a__start__ --> gp_two\3ap_two\3ac_one;
|
||||
gp_two\3ap_two\3ac_one -. 1 .-> gp_two\3ap_two\3a__end__;
|
||||
gp_two\3ap_two\3ac_one -. 0 .-> gp_two\3ap_two\3ac_two;
|
||||
gp_two\3ap_two\3ac_two --> gp_two\3ap_two\3ac_one;
|
||||
gp_two_p_two___start__(<p>__start__</p>)
|
||||
gp_two_p_two_c_one(c_one)
|
||||
gp_two_p_two_c_two(c_two)
|
||||
gp_two_p_two___end__(<p>__end__</p>)
|
||||
gp_two_p_two___start__ --> gp_two_p_two_c_one;
|
||||
gp_two_p_two_c_one -. 1 .-> gp_two_p_two___end__;
|
||||
gp_two_p_two_c_one -. 0 .-> gp_two_p_two_c_two;
|
||||
gp_two_p_two_c_two --> gp_two_p_two_c_one;
|
||||
end
|
||||
end
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
@@ -979,17 +979,17 @@
|
||||
__end__([<p>__end__</p>]):::last
|
||||
__start__ --> p_one;
|
||||
p_one -. 1 .-> __end__;
|
||||
p_one -. 0 .-> p_two\3a__start__;
|
||||
p_two\3a__end__ --> p_one;
|
||||
p_one -. 0 .-> p_two___start__;
|
||||
p_two___end__ --> p_one;
|
||||
subgraph p_two
|
||||
p_two\3a__start__(<p>__start__</p>)
|
||||
p_two\3ac_one(c_one)
|
||||
p_two\3ac_two(c_two)
|
||||
p_two\3a__end__(<p>__end__</p>)
|
||||
p_two\3a__start__ --> p_two\3ac_one;
|
||||
p_two\3ac_one -. 1 .-> p_two\3a__end__;
|
||||
p_two\3ac_one -. 0 .-> p_two\3ac_two;
|
||||
p_two\3ac_two --> p_two\3ac_one;
|
||||
p_two___start__(<p>__start__</p>)
|
||||
p_two_c_one(c_one)
|
||||
p_two_c_two(c_two)
|
||||
p_two___end__(<p>__end__</p>)
|
||||
p_two___start__ --> p_two_c_one;
|
||||
p_two_c_one -. 1 .-> p_two___end__;
|
||||
p_two_c_one -. 0 .-> p_two_c_two;
|
||||
p_two_c_two --> p_two_c_one;
|
||||
end
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
classDef first fill-opacity:0
|
||||
|
||||
@@ -61,7 +61,6 @@ from langgraph.types import (
|
||||
Command,
|
||||
Durability,
|
||||
Interrupt,
|
||||
Overwrite,
|
||||
PregelTask,
|
||||
RetryPolicy,
|
||||
Send,
|
||||
@@ -8698,110 +8697,3 @@ def test_send_with_untracked_value_overlapping_keys(
|
||||
state = app.get_state(config)
|
||||
assert "session_resource" not in state.values
|
||||
assert state.values.get("dictionary") == {"session_resource": "legal_value"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("as_json", [False, True])
|
||||
def test_overwrite_sequential(
|
||||
sync_checkpointer: BaseCheckpointSaver, as_json: bool
|
||||
) -> None:
|
||||
"""Test a sequential chain of nodes where the last node uses Overwrite to bypass a reducer and write a value directly to the channel."""
|
||||
|
||||
class State(TypedDict):
|
||||
messages: Annotated[list, operator.add]
|
||||
|
||||
def node_a(state: State):
|
||||
return {"messages": ["a"]}
|
||||
|
||||
def node_b(state: State):
|
||||
overwrite = {"__overwrite__": ["b"]} if as_json else Overwrite(["b"])
|
||||
return {"messages": overwrite}
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("node_a", node_a)
|
||||
builder.add_node("node_b", node_b)
|
||||
builder.add_edge(START, "node_a")
|
||||
builder.add_edge("node_a", "node_b")
|
||||
|
||||
graph = builder.compile(checkpointer=sync_checkpointer)
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
result = graph.invoke({"messages": ["START"]}, config)
|
||||
# a is overwritten by b
|
||||
assert result == {"messages": ["b"]}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("as_json", [False, True])
|
||||
def test_overwrite_parallel(
|
||||
sync_checkpointer: BaseCheckpointSaver, as_json: bool
|
||||
) -> None:
|
||||
"""Test parallel nodes where max one node uses Overwrite to bypass a reducer and write a value directly to the channel."""
|
||||
|
||||
class State(TypedDict):
|
||||
messages: Annotated[list, operator.add]
|
||||
|
||||
def node_a(state: State):
|
||||
return {"messages": ["a"]}
|
||||
|
||||
def node_b(state: State):
|
||||
overwrite = {"__overwrite__": ["b"]} if as_json else Overwrite(["b"])
|
||||
return {"messages": overwrite}
|
||||
|
||||
def node_c(state: State):
|
||||
return {"messages": ["c"]}
|
||||
|
||||
def node_d(state: State):
|
||||
return {"messages": ["d"]}
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("node_a", node_a)
|
||||
builder.add_node("node_b", node_b)
|
||||
builder.add_node("node_c", node_c)
|
||||
builder.add_node("node_d", node_d)
|
||||
builder.add_edge(START, "node_a")
|
||||
builder.add_edge("node_a", "node_b")
|
||||
builder.add_edge("node_a", "node_c")
|
||||
builder.add_edge("node_b", "node_d")
|
||||
builder.add_edge("node_c", "node_d")
|
||||
|
||||
graph = builder.compile(checkpointer=sync_checkpointer)
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
result = graph.invoke({"messages": ["START"]}, config)
|
||||
# a, c are overwritten by b, then d is written
|
||||
assert result == {"messages": ["b", "d"]}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("as_json", [False, True])
|
||||
def test_overwrite_parallel_error(
|
||||
sync_checkpointer: BaseCheckpointSaver, as_json: bool
|
||||
) -> None:
|
||||
"""Test parallel nodes where more than one node uses Overwrite to bypass a reducer and write a value directly to the channel. In this case, InvalidUpdateError should be raised."""
|
||||
|
||||
class State(TypedDict):
|
||||
messages: Annotated[list, operator.add]
|
||||
|
||||
def node_a(state: State):
|
||||
return {"messages": ["a"]}
|
||||
|
||||
def node_b(state: State):
|
||||
overwrite = {"__overwrite__": ["b"]} if as_json else Overwrite(["b"])
|
||||
return {"messages": overwrite}
|
||||
|
||||
def node_c(state: State):
|
||||
overwrite = {"__overwrite__": ["c"]} if as_json else Overwrite(["c"])
|
||||
return {"messages": overwrite}
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("node_a", node_a)
|
||||
builder.add_node("node_b", node_b)
|
||||
builder.add_node("node_c", node_c)
|
||||
builder.add_edge(START, "node_a")
|
||||
builder.add_edge("node_a", "node_b")
|
||||
builder.add_edge("node_a", "node_c")
|
||||
builder.add_edge("node_b", END)
|
||||
builder.add_edge("node_c", END)
|
||||
|
||||
graph = builder.compile(checkpointer=sync_checkpointer)
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
with pytest.raises(
|
||||
InvalidUpdateError, match="Can receive only one Overwrite value per super-step."
|
||||
):
|
||||
graph.invoke({"messages": ["START"]}, config)
|
||||
@@ -1124,6 +1124,7 @@ async def test_remote_graph_basic_invoke(remote_graph: RemoteGraph) -> None:
|
||||
"type": "ai",
|
||||
"name": None,
|
||||
"id": "ai3",
|
||||
"example": False,
|
||||
"tool_calls": [],
|
||||
"invalid_tool_calls": [],
|
||||
"usage_metadata": None,
|
||||
|
||||
Generated
+9
-11
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.10"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.14'",
|
||||
@@ -1327,7 +1327,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "1.0.0"
|
||||
version = "1.0.0a1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -1338,14 +1338,14 @@ dependencies = [
|
||||
{ name = "tenacity" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/d0/9db6d375ecf8bd498fcc87016e43c3d930ddbfbacf9a1e99018ada4e824f/langchain_core-1.0.0.tar.gz", hash = "sha256:8e81c94a22fa3a362a0f101bbd1271bf3725e50cf1e31c84e8f4a1c731279785", size = 764484, upload-time = "2025-10-17T13:48:24.408Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/c8/25d9ff74f8d4184dfce0e5c3332d5a1c9bead5db5fcf9ed1f1d4a826a804/langchain_core-1.0.0a1.tar.gz", hash = "sha256:b1acc3342911c3a95db0e0bbfa600c5a528b2e46d6e8faa90cbc6f154a29c582", size = 603792, upload-time = "2025-08-27T17:41:44.264Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/68/6a/8dd566cb7379d6e3a921f94713babba2f71cbed65c73c784c649c1fd7d4e/langchain_core-1.0.0-py3-none-any.whl", hash = "sha256:a94561bf75dd097c7d6e3864950f28dadc963f0bd810114de4095f41f634059b", size = 467157, upload-time = "2025-10-17T13:48:23.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/41/925fb1f2dfb9d85c492c9d5654cbede160e8e3aab5a91c25b557235e5578/langchain_core-1.0.0a1-py3-none-any.whl", hash = "sha256:83fe163134bc52245962f45f7098ec348eb85c1f02a75eb35d1771f80fa40420", size = 473126, upload-time = "2025-08-27T17:41:42.896Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "1.0.2"
|
||||
version = "1.0.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -1429,7 +1429,7 @@ requires-dist = [
|
||||
dev = [
|
||||
{ name = "httpx" },
|
||||
{ name = "jupyter" },
|
||||
{ name = "langchain-core", specifier = ">=1.0.0" },
|
||||
{ name = "langchain-core", specifier = "==1.0.0a1" },
|
||||
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
|
||||
{ name = "langgraph-checkpoint-postgres", editable = "../checkpoint-postgres" },
|
||||
{ name = "langgraph-checkpoint-sqlite", editable = "../checkpoint-sqlite" },
|
||||
@@ -1462,7 +1462,7 @@ lint = [
|
||||
]
|
||||
test = [
|
||||
{ name = "httpx" },
|
||||
{ name = "langchain-core", specifier = ">=1.0.0" },
|
||||
{ name = "langchain-core", specifier = "==1.0.0a1" },
|
||||
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
|
||||
{ name = "langgraph-checkpoint-postgres", editable = "../checkpoint-postgres" },
|
||||
{ name = "langgraph-checkpoint-sqlite", editable = "../checkpoint-sqlite" },
|
||||
@@ -1710,7 +1710,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "1.0.2"
|
||||
version = "1.0.1"
|
||||
source = { editable = "../prebuilt" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -1719,7 +1719,7 @@ dependencies = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "langchain-core", specifier = ">=1.0.0" },
|
||||
{ name = "langchain-core", specifier = ">=0.3.67" },
|
||||
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
|
||||
]
|
||||
|
||||
@@ -1732,7 +1732,6 @@ dev = [
|
||||
{ name = "langgraph-checkpoint-postgres", editable = "../checkpoint-postgres" },
|
||||
{ name = "langgraph-checkpoint-sqlite", editable = "../checkpoint-sqlite" },
|
||||
{ name = "mypy" },
|
||||
{ name = "psycopg-binary" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
@@ -1751,7 +1750,6 @@ test = [
|
||||
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
|
||||
{ name = "langgraph-checkpoint-postgres", editable = "../checkpoint-postgres" },
|
||||
{ name = "langgraph-checkpoint-sqlite", editable = "../checkpoint-sqlite" },
|
||||
{ name = "psycopg-binary" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
|
||||
@@ -5,7 +5,6 @@ from langgraph.prebuilt.tool_node import (
|
||||
InjectedState,
|
||||
InjectedStore,
|
||||
ToolNode,
|
||||
ToolRuntime,
|
||||
tools_condition,
|
||||
)
|
||||
from langgraph.prebuilt.tool_validator import ValidationNode
|
||||
@@ -17,5 +16,4 @@ __all__ = [
|
||||
"ValidationNode",
|
||||
"InjectedState",
|
||||
"InjectedStore",
|
||||
"ToolRuntime",
|
||||
]
|
||||
@@ -44,7 +44,7 @@ from langgraph.warnings import LangGraphDeprecatedSinceV10
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import NotRequired, TypedDict, deprecated
|
||||
|
||||
from langgraph.prebuilt.tool_node import ToolCallWithContext, ToolNode
|
||||
from langgraph.prebuilt.tool_node import ToolNode
|
||||
|
||||
StructuredResponse = dict | BaseModel
|
||||
StructuredResponseSchema = dict | type[BaseModel]
|
||||
@@ -826,17 +826,11 @@ def create_react_agent(
|
||||
elif version == "v2":
|
||||
if post_model_hook is not None:
|
||||
return "post_model_hook"
|
||||
return [
|
||||
Send(
|
||||
"tools",
|
||||
ToolCallWithContext(
|
||||
__type="tool_call_with_context",
|
||||
tool_call=call,
|
||||
state=state,
|
||||
),
|
||||
)
|
||||
tool_calls = [
|
||||
tool_node.inject_tool_args(call, state, store) # type: ignore[arg-type]
|
||||
for call in last_message.tool_calls
|
||||
]
|
||||
return [Send("tools", [tool_call]) for tool_call in tool_calls]
|
||||
|
||||
# Define a new graph
|
||||
workflow = StateGraph(
|
||||
@@ -917,17 +911,11 @@ def create_react_agent(
|
||||
]
|
||||
|
||||
if pending_tool_calls:
|
||||
return [
|
||||
Send(
|
||||
"tools",
|
||||
ToolCallWithContext(
|
||||
__type="tool_call_with_context",
|
||||
tool_call=call,
|
||||
state=state,
|
||||
),
|
||||
)
|
||||
pending_tool_calls = [
|
||||
tool_node.inject_tool_args(call, state, store) # type: ignore[arg-type]
|
||||
for call in pending_tool_calls
|
||||
]
|
||||
return [Send("tools", [tool_call]) for tool_call in pending_tool_calls]
|
||||
elif isinstance(messages[-1], ToolMessage):
|
||||
return entrypoint
|
||||
elif response_format is not None:
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "1.0.2"
|
||||
version = "1.0.1"
|
||||
description = "Library with high-level APIs for creating and executing LangGraph agents and tools."
|
||||
authors = []
|
||||
requires-python = ">=3.10"
|
||||
@@ -25,7 +25,7 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"langgraph-checkpoint>=2.1.0,<4.0.0",
|
||||
"langchain-core>=1.0.0",
|
||||
"langchain-core>=0.3.67",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -43,7 +43,6 @@ test = [
|
||||
"langgraph-checkpoint-sqlite",
|
||||
"langgraph-checkpoint-postgres",
|
||||
"syrupy",
|
||||
"psycopg-binary",
|
||||
]
|
||||
lint = [
|
||||
"ruff",
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -8,7 +8,6 @@ from typing import (
|
||||
Literal,
|
||||
TypeVar,
|
||||
)
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
@@ -65,29 +64,6 @@ pytestmark = pytest.mark.anyio
|
||||
REACT_TOOL_CALL_VERSIONS = ["v1", "v2"]
|
||||
|
||||
|
||||
def _create_mock_runtime(store: BaseStore | None = None) -> Mock:
|
||||
"""Create a mock Runtime object for testing ToolNode outside of graph context.
|
||||
|
||||
This helper is needed because ToolNode._func expects a Runtime parameter
|
||||
which is injected by RunnableCallable from config["configurable"]["__pregel_runtime"].
|
||||
When testing ToolNode directly (outside a graph), we need to provide this manually.
|
||||
"""
|
||||
mock_runtime = Mock()
|
||||
mock_runtime.store = store
|
||||
mock_runtime.context = None
|
||||
mock_runtime.stream_writer = lambda *args, **kwargs: None
|
||||
return mock_runtime
|
||||
|
||||
|
||||
def _create_config_with_runtime(store: BaseStore | None = None) -> RunnableConfig:
|
||||
"""Create a RunnableConfig with mock Runtime for testing ToolNode.
|
||||
|
||||
Returns:
|
||||
RunnableConfig with __pregel_runtime in configurable dict.
|
||||
"""
|
||||
return {"configurable": {"__pregel_runtime": _create_mock_runtime(store)}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
|
||||
def test_no_prompt(sync_checkpointer: BaseCheckpointSaver, version: str) -> None:
|
||||
model = FakeToolCallingModel()
|
||||
@@ -351,8 +327,7 @@ def test_model_with_tools(tool_style: str, version: str, include_builtin: bool):
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
tool_messages: ToolMessage = result["messages"][-2:]
|
||||
for tool_message in tool_messages:
|
||||
@@ -753,13 +728,37 @@ def test_tool_node_inject_state(schema_: type[T]) -> None:
|
||||
"type": "tool_call",
|
||||
}
|
||||
msg = AIMessage("hi?", tool_calls=[tool_call])
|
||||
result = node.invoke(
|
||||
schema_(**{"messages": [msg], "foo": "bar"}),
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
result = node.invoke(schema_(**{"messages": [msg], "foo": "bar"}))
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "bar", f"Failed for tool={tool_name}"
|
||||
|
||||
if tool_name == "tool3":
|
||||
failure_input = None
|
||||
try:
|
||||
failure_input = schema_(**{"messages": [msg], "notfoo": "bar"})
|
||||
except Exception:
|
||||
pass
|
||||
if failure_input is not None:
|
||||
with pytest.raises(KeyError):
|
||||
node.invoke(failure_input)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
node.invoke([msg])
|
||||
else:
|
||||
failure_input = None
|
||||
try:
|
||||
failure_input = schema_(**{"messages": [msg], "notfoo": "bar"})
|
||||
except Exception:
|
||||
# We'd get a validation error from pydantic state and wouldn't make it to the node
|
||||
# anyway
|
||||
pass
|
||||
if failure_input is not None:
|
||||
messages_ = node.invoke(failure_input)
|
||||
tool_message = messages_["messages"][-1]
|
||||
assert "KeyError" in tool_message.content
|
||||
tool_message = node.invoke([msg])[-1]
|
||||
assert "KeyError" in tool_message.content
|
||||
|
||||
tool_call = {
|
||||
"name": "tool4",
|
||||
"args": {"some_val": 1},
|
||||
@@ -767,13 +766,11 @@ def test_tool_node_inject_state(schema_: type[T]) -> None:
|
||||
"type": "tool_call",
|
||||
}
|
||||
msg = AIMessage("hi?", tool_calls=[tool_call])
|
||||
result = node.invoke(
|
||||
schema_(**{"messages": [msg], "foo": ""}), config=_create_config_with_runtime()
|
||||
)
|
||||
result = node.invoke(schema_(**{"messages": [msg], "foo": ""}))
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "hi?"
|
||||
|
||||
result = node.invoke([msg], config=_create_config_with_runtime())
|
||||
result = node.invoke([msg])
|
||||
tool_message = result[-1]
|
||||
assert tool_message.content == "hi?"
|
||||
|
||||
@@ -885,9 +882,7 @@ def test_tool_node_inject_store() -> None:
|
||||
"type": "tool_call",
|
||||
}
|
||||
msg = AIMessage("hi?", tool_calls=[tool_call])
|
||||
node_result = node.invoke(
|
||||
{"messages": [msg]}, config=_create_config_with_runtime(store=store)
|
||||
)
|
||||
node_result = node.invoke({"messages": [msg]}, store=store)
|
||||
graph_result = graph.invoke({"messages": [msg]})
|
||||
for result in (node_result, graph_result):
|
||||
result["messages"][-1]
|
||||
@@ -903,10 +898,7 @@ def test_tool_node_inject_store() -> None:
|
||||
"type": "tool_call",
|
||||
}
|
||||
msg = AIMessage("hi?", tool_calls=[tool_call])
|
||||
node_result = node.invoke(
|
||||
{"messages": [msg], "bar": "baz"},
|
||||
config=_create_config_with_runtime(store=store),
|
||||
)
|
||||
node_result = node.invoke({"messages": [msg], "bar": "baz"}, store=store)
|
||||
graph_result = graph.invoke({"messages": [msg], "bar": "baz"})
|
||||
for result in (node_result, graph_result):
|
||||
result["messages"][-1]
|
||||
@@ -931,8 +923,7 @@ def test_tool_node_ensure_utf8() -> None:
|
||||
tools = [get_day_list]
|
||||
tool_calls = [ToolCall(name=get_day_list.name, args={"days": data}, id="test_id")]
|
||||
outputs: list[ToolMessage] = ToolNode(tools).invoke(
|
||||
[AIMessage(content="", tool_calls=tool_calls)],
|
||||
config=_create_config_with_runtime(),
|
||||
[AIMessage(content="", tool_calls=tool_calls)]
|
||||
)
|
||||
assert outputs[0].content == json.dumps(data, ensure_ascii=False)
|
||||
|
||||
|
||||
@@ -1,92 +1,39 @@
|
||||
import contextlib
|
||||
import dataclasses
|
||||
import json
|
||||
import sys
|
||||
from functools import partial
|
||||
from typing import (
|
||||
Annotated,
|
||||
Any,
|
||||
NoReturn,
|
||||
TypeVar,
|
||||
)
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from langchain_core.messages import (
|
||||
AIMessage,
|
||||
AnyMessage,
|
||||
HumanMessage,
|
||||
RemoveMessage,
|
||||
ToolCall,
|
||||
ToolMessage,
|
||||
)
|
||||
from langchain_core.runnables.config import RunnableConfig
|
||||
from langchain_core.tools import BaseTool, ToolException
|
||||
from langchain_core.tools import tool as dec_tool
|
||||
from langgraph.config import get_stream_writer
|
||||
from langgraph.errors import GraphBubbleUp, GraphInterrupt
|
||||
from langgraph.graph import START, MessagesState, StateGraph
|
||||
from langgraph.graph.message import REMOVE_ALL_MESSAGES, add_messages
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
from langgraph.graph.message import REMOVE_ALL_MESSAGES
|
||||
from langgraph.types import Command, Send
|
||||
from pydantic import BaseModel
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from typing_extensions import TypedDict
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic.v1 import ValidationError as ValidationErrorV1
|
||||
|
||||
from langgraph.prebuilt import (
|
||||
InjectedState,
|
||||
InjectedStore,
|
||||
ToolNode,
|
||||
)
|
||||
from langgraph.prebuilt.tool_node import (
|
||||
TOOL_CALL_ERROR_TEMPLATE,
|
||||
ToolInvocationError,
|
||||
tools_condition,
|
||||
)
|
||||
|
||||
from .messages import _AnyIdHumanMessage, _AnyIdToolMessage
|
||||
from .model import FakeToolCallingModel
|
||||
from langgraph.prebuilt import ToolNode
|
||||
from langgraph.prebuilt.tool_node import TOOL_CALL_ERROR_TEMPLATE
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
def _create_mock_runtime(store: BaseStore | None = None) -> Mock:
|
||||
"""Create a mock Runtime object for testing ToolNode outside of graph context.
|
||||
|
||||
This helper is needed because ToolNode._func expects a Runtime parameter
|
||||
which is injected by RunnableCallable from config["configurable"]["__pregel_runtime"].
|
||||
When testing ToolNode directly (outside a graph), we need to provide this manually.
|
||||
"""
|
||||
mock_runtime = Mock()
|
||||
mock_runtime.store = store
|
||||
mock_runtime.context = None
|
||||
mock_runtime.stream_writer = lambda *args, **kwargs: None
|
||||
return mock_runtime
|
||||
|
||||
|
||||
def _create_config_with_runtime(store: BaseStore | None = None) -> RunnableConfig:
|
||||
"""Create a RunnableConfig with mock Runtime for testing ToolNode.
|
||||
|
||||
Returns:
|
||||
RunnableConfig with __pregel_runtime in configurable dict.
|
||||
"""
|
||||
return {"configurable": {"__pregel_runtime": _create_mock_runtime(store)}}
|
||||
|
||||
|
||||
def tool1(some_val: int, some_other_val: str) -> str:
|
||||
"""Tool 1 docstring."""
|
||||
if some_val == 0:
|
||||
msg = "Test error"
|
||||
raise ValueError(msg)
|
||||
raise ValueError("Test error")
|
||||
return f"{some_val} - {some_other_val}"
|
||||
|
||||
|
||||
async def tool2(some_val: int, some_other_val: str) -> str:
|
||||
"""Tool 2 docstring."""
|
||||
if some_val == 0:
|
||||
msg = "Test error"
|
||||
raise ToolException(msg)
|
||||
raise ToolException("Test error")
|
||||
return f"tool2: {some_val} - {some_other_val}"
|
||||
|
||||
|
||||
@@ -106,17 +53,15 @@ async def tool4(some_val: int, some_other_val: str) -> str:
|
||||
|
||||
|
||||
@dec_tool
|
||||
def tool5(some_val: int) -> NoReturn:
|
||||
def tool5(some_val: int):
|
||||
"""Tool 5 docstring."""
|
||||
msg = "Test error"
|
||||
raise ToolException(msg)
|
||||
raise ToolException("Test error")
|
||||
|
||||
|
||||
tool5.handle_tool_error = "foo"
|
||||
|
||||
|
||||
async def test_tool_node() -> None:
|
||||
"""Test tool node."""
|
||||
async def test_tool_node():
|
||||
result = ToolNode([tool1]).invoke(
|
||||
{
|
||||
"messages": [
|
||||
@@ -131,8 +76,7 @@ async def test_tool_node() -> None:
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
|
||||
tool_message: ToolMessage = result["messages"][-1]
|
||||
@@ -154,8 +98,7 @@ async def test_tool_node() -> None:
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
|
||||
tool_message: ToolMessage = result2["messages"][-1]
|
||||
@@ -177,8 +120,7 @@ async def test_tool_node() -> None:
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
tool_message: ToolMessage = result3["messages"][-1]
|
||||
assert tool_message.type == "tool"
|
||||
@@ -203,8 +145,7 @@ async def test_tool_node() -> None:
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
tool_message: ToolMessage = result4["messages"][-1]
|
||||
assert tool_message.type == "tool"
|
||||
@@ -212,7 +153,7 @@ async def test_tool_node() -> None:
|
||||
assert tool_message.tool_call_id == "some 3"
|
||||
|
||||
|
||||
async def test_tool_node_tool_call_input() -> None:
|
||||
async def test_tool_node_tool_call_input():
|
||||
# Single tool call
|
||||
tool_call_1 = {
|
||||
"name": "tool1",
|
||||
@@ -220,9 +161,7 @@ async def test_tool_node_tool_call_input() -> None:
|
||||
"id": "some 0",
|
||||
"type": "tool_call",
|
||||
}
|
||||
result = ToolNode([tool1]).invoke(
|
||||
[tool_call_1], config=_create_config_with_runtime()
|
||||
)
|
||||
result = ToolNode([tool1]).invoke([tool_call_1])
|
||||
assert result["messages"] == [
|
||||
ToolMessage(content="1 - foo", tool_call_id="some 0", name="tool1"),
|
||||
]
|
||||
@@ -234,9 +173,7 @@ async def test_tool_node_tool_call_input() -> None:
|
||||
"id": "some 1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
result = ToolNode([tool1]).invoke(
|
||||
[tool_call_1, tool_call_2], config=_create_config_with_runtime()
|
||||
)
|
||||
result = ToolNode([tool1]).invoke([tool_call_1, tool_call_2])
|
||||
assert result["messages"] == [
|
||||
ToolMessage(content="1 - foo", tool_call_id="some 0", name="tool1"),
|
||||
ToolMessage(content="2 - bar", tool_call_id="some 1", name="tool1"),
|
||||
@@ -245,9 +182,7 @@ async def test_tool_node_tool_call_input() -> None:
|
||||
# Test with unknown tool
|
||||
tool_call_3 = tool_call_1.copy()
|
||||
tool_call_3["name"] = "tool2"
|
||||
result = ToolNode([tool1]).invoke(
|
||||
[tool_call_1, tool_call_3], config=_create_config_with_runtime()
|
||||
)
|
||||
result = ToolNode([tool1]).invoke([tool_call_1, tool_call_3])
|
||||
assert result["messages"] == [
|
||||
ToolMessage(content="1 - foo", tool_call_id="some 0", name="tool1"),
|
||||
ToolMessage(
|
||||
@@ -259,58 +194,8 @@ async def test_tool_node_tool_call_input() -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_tool_node_error_handling_default_invocation() -> None:
|
||||
tn = ToolNode([tool1])
|
||||
result = tn.invoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "tool1",
|
||||
"args": {"invalid": 0, "args": "foo"},
|
||||
"id": "some id",
|
||||
},
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
assert all(m.type == "tool" for m in result["messages"])
|
||||
assert all(m.status == "error" for m in result["messages"])
|
||||
assert (
|
||||
"Error invoking tool 'tool1' with kwargs {'invalid': 0, 'args': 'foo'} with error:\n"
|
||||
in result["messages"][0].content
|
||||
)
|
||||
|
||||
|
||||
def test_tool_node_error_handling_default_exception() -> None:
|
||||
tn = ToolNode([tool1])
|
||||
with pytest.raises(ValueError):
|
||||
tn.invoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "tool1",
|
||||
"args": {"some_val": 0, "some_other_val": "foo"},
|
||||
"id": "some id",
|
||||
},
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
|
||||
async def test_tool_node_error_handling() -> None:
|
||||
def handle_all(e: ValueError | ToolException | ToolInvocationError):
|
||||
async def test_tool_node_error_handling():
|
||||
def handle_all(e: ValueError | ToolException | ValidationError):
|
||||
return TOOL_CALL_ERROR_TEMPLATE.format(error=repr(e))
|
||||
|
||||
# test catching all exceptions, via:
|
||||
@@ -319,7 +204,7 @@ async def test_tool_node_error_handling() -> None:
|
||||
# - passing a callable with all exceptions in the signature
|
||||
for handle_tool_errors in (
|
||||
True,
|
||||
(ValueError, ToolException, ToolInvocationError),
|
||||
(ValueError, ToolException, ValidationError),
|
||||
handle_all,
|
||||
):
|
||||
result_error = await ToolNode(
|
||||
@@ -348,33 +233,34 @@ async def test_tool_node_error_handling() -> None:
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
|
||||
assert all(m.type == "tool" for m in result_error["messages"])
|
||||
assert all(m.status == "error" for m in result_error["messages"])
|
||||
assert (
|
||||
result_error["messages"][0].content
|
||||
== f"Error: {ValueError('Test error')!r}\n Please fix your mistakes."
|
||||
== f"Error: {repr(ValueError('Test error'))}\n Please fix your mistakes."
|
||||
)
|
||||
assert (
|
||||
result_error["messages"][1].content
|
||||
== f"Error: {ToolException('Test error')!r}\n Please fix your mistakes."
|
||||
== f"Error: {repr(ToolException('Test error'))}\n Please fix your mistakes."
|
||||
)
|
||||
assert (
|
||||
"ValidationError" in result_error["messages"][2].content
|
||||
or "validation error" in result_error["messages"][2].content
|
||||
)
|
||||
# Check that the validation error contains the field name
|
||||
assert "some_other_val" in result_error["messages"][2].content
|
||||
|
||||
assert result_error["messages"][0].tool_call_id == "some id"
|
||||
assert result_error["messages"][1].tool_call_id == "some other id"
|
||||
assert result_error["messages"][2].tool_call_id == "another id"
|
||||
|
||||
|
||||
async def test_tool_node_error_handling_callable() -> None:
|
||||
def handle_value_error(e: ValueError) -> str:
|
||||
async def test_tool_node_error_handling_callable():
|
||||
def handle_value_error(e: ValueError):
|
||||
return "Value error"
|
||||
|
||||
def handle_tool_exception(e: ToolException) -> str:
|
||||
def handle_tool_exception(e: ToolException):
|
||||
return "Tool exception"
|
||||
|
||||
for handle_tool_errors in ("Value error", handle_value_error):
|
||||
@@ -394,8 +280,7 @@ async def test_tool_node_error_handling_callable() -> None:
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
tool_message: ToolMessage = result_error["messages"][-1]
|
||||
assert tool_message.type == "tool"
|
||||
@@ -428,8 +313,7 @@ async def test_tool_node_error_handling_callable() -> None:
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
assert str(exc_info.value) == "Test error"
|
||||
|
||||
@@ -456,13 +340,12 @@ async def test_tool_node_error_handling_callable() -> None:
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
assert str(exc_info.value) == "Test error"
|
||||
|
||||
|
||||
async def test_tool_node_handle_tool_errors_false() -> None:
|
||||
async def test_tool_node_handle_tool_errors_false():
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
ToolNode([tool1], handle_tool_errors=False).invoke(
|
||||
{
|
||||
@@ -478,8 +361,7 @@ async def test_tool_node_handle_tool_errors_false() -> None:
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
|
||||
assert str(exc_info.value) == "Test error"
|
||||
@@ -499,14 +381,13 @@ async def test_tool_node_handle_tool_errors_false() -> None:
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
|
||||
assert str(exc_info.value) == "Test error"
|
||||
|
||||
# test validation errors get raised if handle_tool_errors is False
|
||||
with pytest.raises(ToolInvocationError):
|
||||
with pytest.raises((ValidationError, ValidationErrorV1)):
|
||||
ToolNode([tool1], handle_tool_errors=False).invoke(
|
||||
{
|
||||
"messages": [
|
||||
@@ -521,12 +402,11 @@ async def test_tool_node_handle_tool_errors_false() -> None:
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_tool_node_individual_tool_error_handling() -> None:
|
||||
def test_tool_node_individual_tool_error_handling():
|
||||
# test error handling on individual tools (and that it overrides overall error handling!)
|
||||
result_individual_tool_error_handler = ToolNode(
|
||||
[tool5], handle_tool_errors="bar"
|
||||
@@ -544,8 +424,7 @@ def test_tool_node_individual_tool_error_handling() -> None:
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
|
||||
tool_message: ToolMessage = result_individual_tool_error_handler["messages"][-1]
|
||||
@@ -555,7 +434,7 @@ def test_tool_node_individual_tool_error_handling() -> None:
|
||||
assert tool_message.tool_call_id == "some 0"
|
||||
|
||||
|
||||
def test_tool_node_incorrect_tool_name() -> None:
|
||||
def test_tool_node_incorrect_tool_name():
|
||||
result_incorrect_name = ToolNode([tool1, tool2]).invoke(
|
||||
{
|
||||
"messages": [
|
||||
@@ -570,8 +449,7 @@ def test_tool_node_incorrect_tool_name() -> None:
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
|
||||
tool_message: ToolMessage = result_incorrect_name["messages"][-1]
|
||||
@@ -584,13 +462,12 @@ def test_tool_node_incorrect_tool_name() -> None:
|
||||
assert tool_message.tool_call_id == "some 0"
|
||||
|
||||
|
||||
def test_tool_node_node_interrupt() -> None:
|
||||
def test_tool_node_node_interrupt():
|
||||
def tool_interrupt(some_val: int) -> None:
|
||||
"""Tool docstring."""
|
||||
msg = "foo"
|
||||
raise GraphBubbleUp(msg)
|
||||
raise GraphBubbleUp("foo")
|
||||
|
||||
def handle(e: GraphInterrupt) -> str:
|
||||
def handle(e: GraphInterrupt):
|
||||
return "handled"
|
||||
|
||||
for handle_tool_errors in (True, (GraphBubbleUp,), "handled", handle, False):
|
||||
@@ -610,14 +487,13 @@ def test_tool_node_node_interrupt() -> None:
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
assert exc_info.value == "foo"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("input_type", ["dict", "tool_calls"])
|
||||
async def test_tool_node_command(input_type: str) -> None:
|
||||
async def test_tool_node_command(input_type: str):
|
||||
from langchain_core.tools.base import InjectedToolCallId
|
||||
|
||||
@dec_tool
|
||||
@@ -702,9 +578,7 @@ async def test_tool_node_command(input_type: str) -> None:
|
||||
input_ = {"messages": [AIMessage("", tool_calls=tool_calls)]}
|
||||
elif input_type == "tool_calls":
|
||||
input_ = tool_calls
|
||||
result = ToolNode([add, transfer_to_bob]).invoke(
|
||||
input_, config=_create_config_with_runtime()
|
||||
)
|
||||
result = ToolNode([add, transfer_to_bob]).invoke(input_)
|
||||
|
||||
assert result == [
|
||||
{
|
||||
@@ -742,8 +616,7 @@ async def test_tool_node_command(input_type: str) -> None:
|
||||
"", tool_calls=[{"args": {}, "id": "1", "name": tool.name}]
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
assert result == [
|
||||
Command(
|
||||
@@ -770,8 +643,7 @@ async def test_tool_node_command(input_type: str) -> None:
|
||||
"", tool_calls=[{"args": {}, "id": "1", "name": tool.name}]
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
assert result == [
|
||||
Command(
|
||||
@@ -801,8 +673,7 @@ async def test_tool_node_command(input_type: str) -> None:
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
assert result == [
|
||||
Command(
|
||||
@@ -853,8 +724,7 @@ async def test_tool_node_command(input_type: str) -> None:
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
|
||||
# test validation (missing tool message in the update for current graph)
|
||||
@@ -873,8 +743,7 @@ async def test_tool_node_command(input_type: str) -> None:
|
||||
tool_calls=[{"args": {}, "id": "1", "name": "no_update_tool"}],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
|
||||
# test validation (tool message with a wrong tool call ID)
|
||||
@@ -901,8 +770,7 @@ async def test_tool_node_command(input_type: str) -> None:
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
)
|
||||
|
||||
# test validation (missing tool message in the update for parent graph is OK)
|
||||
@@ -921,12 +789,11 @@ async def test_tool_node_command(input_type: str) -> None:
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
}
|
||||
) == [Command(update={"messages": []}, graph=Command.PARENT)]
|
||||
|
||||
|
||||
async def test_tool_node_command_list_input() -> None:
|
||||
async def test_tool_node_command_list_input():
|
||||
from langchain_core.tools.base import InjectedToolCallId
|
||||
|
||||
@dec_tool
|
||||
@@ -1004,8 +871,7 @@ async def test_tool_node_command_list_input() -> None:
|
||||
{"args": {}, "id": "2", "name": "transfer_to_bob"},
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
]
|
||||
)
|
||||
|
||||
assert result == [
|
||||
@@ -1034,8 +900,7 @@ async def test_tool_node_command_list_input() -> None:
|
||||
# test sync tools
|
||||
for tool in [transfer_to_bob, custom_tool]:
|
||||
result = ToolNode([tool]).invoke(
|
||||
[AIMessage("", tool_calls=[{"args": {}, "id": "1", "name": tool.name}])],
|
||||
config=_create_config_with_runtime(),
|
||||
[AIMessage("", tool_calls=[{"args": {}, "id": "1", "name": tool.name}])]
|
||||
)
|
||||
assert result == [
|
||||
Command(
|
||||
@@ -1054,8 +919,7 @@ async def test_tool_node_command_list_input() -> None:
|
||||
# test async tools
|
||||
for tool in [async_transfer_to_bob, async_custom_tool]:
|
||||
result = await ToolNode([tool]).ainvoke(
|
||||
[AIMessage("", tool_calls=[{"args": {}, "id": "1", "name": tool.name}])],
|
||||
config=_create_config_with_runtime(),
|
||||
[AIMessage("", tool_calls=[{"args": {}, "id": "1", "name": tool.name}])]
|
||||
)
|
||||
assert result == [
|
||||
Command(
|
||||
@@ -1081,8 +945,7 @@ async def test_tool_node_command_list_input() -> None:
|
||||
{"args": {}, "id": "2", "name": "custom_transfer_to_bob"},
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
]
|
||||
)
|
||||
assert result == [
|
||||
Command(
|
||||
@@ -1127,8 +990,7 @@ async def test_tool_node_command_list_input() -> None:
|
||||
"",
|
||||
tool_calls=[{"args": {}, "id": "1", "name": "list_update_tool"}],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
]
|
||||
)
|
||||
|
||||
# test validation (missing tool message in the update for current graph)
|
||||
@@ -1145,8 +1007,7 @@ async def test_tool_node_command_list_input() -> None:
|
||||
"",
|
||||
tool_calls=[{"args": {}, "id": "1", "name": "no_update_tool"}],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
]
|
||||
)
|
||||
|
||||
# test validation (tool message with a wrong tool call ID)
|
||||
@@ -1165,8 +1026,7 @@ async def test_tool_node_command_list_input() -> None:
|
||||
{"args": {}, "id": "1", "name": "mismatching_tool_call_id_tool"}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
]
|
||||
)
|
||||
|
||||
# test validation (missing tool message in the update for parent graph is OK)
|
||||
@@ -1181,12 +1041,11 @@ async def test_tool_node_command_list_input() -> None:
|
||||
"",
|
||||
tool_calls=[{"args": {}, "id": "1", "name": "node_update_parent_tool"}],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
]
|
||||
) == [Command(update=[], graph=Command.PARENT)]
|
||||
|
||||
|
||||
def test_tool_node_parent_command_with_send() -> None:
|
||||
def test_tool_node_parent_command_with_send():
|
||||
from langchain_core.tools.base import InjectedToolCallId
|
||||
|
||||
@dec_tool
|
||||
@@ -1237,8 +1096,7 @@ def test_tool_node_parent_command_with_send() -> None:
|
||||
]
|
||||
|
||||
result = ToolNode([transfer_to_alice, transfer_to_bob]).invoke(
|
||||
[AIMessage("", tool_calls=tool_calls)],
|
||||
config=_create_config_with_runtime(),
|
||||
[AIMessage("", tool_calls=tool_calls)]
|
||||
)
|
||||
|
||||
assert result == [
|
||||
@@ -1274,7 +1132,7 @@ def test_tool_node_parent_command_with_send() -> None:
|
||||
]
|
||||
|
||||
|
||||
async def test_tool_node_command_remove_all_messages() -> None:
|
||||
async def test_tool_node_command_remove_all_messages():
|
||||
from langchain_core.tools.base import InjectedToolCallId
|
||||
|
||||
@dec_tool
|
||||
@@ -1289,8 +1147,7 @@ async def test_tool_node_command_remove_all_messages() -> None:
|
||||
"id": "tool_call_123",
|
||||
}
|
||||
result = await tool_node.ainvoke(
|
||||
{"messages": [AIMessage(content="", tool_calls=[tool_call])]},
|
||||
config=_create_config_with_runtime(),
|
||||
{"messages": [AIMessage(content="", tool_calls=[tool_call])]}
|
||||
)
|
||||
|
||||
assert isinstance(result, list)
|
||||
@@ -1298,315 +1155,3 @@ async def test_tool_node_command_remove_all_messages() -> None:
|
||||
command = result[0]
|
||||
assert isinstance(command, Command)
|
||||
assert command.update == {"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES)]}
|
||||
|
||||
|
||||
class _InjectStateSchema(TypedDict):
|
||||
messages: list
|
||||
foo: str
|
||||
|
||||
|
||||
class _InjectedStatePydanticV2Schema(BaseModel):
|
||||
messages: list
|
||||
foo: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _InjectedStateDataclassSchema:
|
||||
messages: list
|
||||
foo: str
|
||||
|
||||
|
||||
_INJECTED_STATE_SCHEMAS = [
|
||||
_InjectStateSchema,
|
||||
_InjectedStatePydanticV2Schema,
|
||||
_InjectedStateDataclassSchema,
|
||||
]
|
||||
|
||||
if sys.version_info < (3, 14):
|
||||
|
||||
class _InjectedStatePydanticSchema(BaseModelV1):
|
||||
messages: list
|
||||
foo: str
|
||||
|
||||
_INJECTED_STATE_SCHEMAS.append(_InjectedStatePydanticSchema)
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("schema_", _INJECTED_STATE_SCHEMAS)
|
||||
def test_tool_node_inject_state(schema_: type[T]) -> None:
|
||||
def tool1(some_val: int, state: Annotated[T, InjectedState]) -> str:
|
||||
"""Tool 1 docstring."""
|
||||
if isinstance(state, dict):
|
||||
return state["foo"]
|
||||
return state.foo
|
||||
|
||||
def tool2(some_val: int, state: Annotated[T, InjectedState()]) -> str:
|
||||
"""Tool 2 docstring."""
|
||||
if isinstance(state, dict):
|
||||
return state["foo"]
|
||||
return state.foo
|
||||
|
||||
def tool3(
|
||||
some_val: int,
|
||||
foo: Annotated[str, InjectedState("foo")],
|
||||
msgs: Annotated[list[AnyMessage], InjectedState("messages")],
|
||||
) -> str:
|
||||
"""Tool 1 docstring."""
|
||||
return foo
|
||||
|
||||
def tool4(
|
||||
some_val: int, msgs: Annotated[list[AnyMessage], InjectedState("messages")]
|
||||
) -> str:
|
||||
"""Tool 1 docstring."""
|
||||
return msgs[0].content
|
||||
|
||||
node = ToolNode([tool1, tool2, tool3, tool4], handle_tool_errors=True)
|
||||
for tool_name in ("tool1", "tool2", "tool3"):
|
||||
tool_call = {
|
||||
"name": tool_name,
|
||||
"args": {"some_val": 1},
|
||||
"id": "some 0",
|
||||
"type": "tool_call",
|
||||
}
|
||||
msg = AIMessage("hi?", tool_calls=[tool_call])
|
||||
result = node.invoke(
|
||||
schema_(messages=[msg], foo="bar"), config=_create_config_with_runtime()
|
||||
)
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "bar", f"Failed for tool={tool_name}"
|
||||
|
||||
if tool_name == "tool3":
|
||||
failure_input = None
|
||||
with contextlib.suppress(Exception):
|
||||
failure_input = schema_(messages=[msg], notfoo="bar")
|
||||
if failure_input is not None:
|
||||
with pytest.raises(KeyError):
|
||||
node.invoke(failure_input, config=_create_config_with_runtime())
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
node.invoke([msg], config=_create_config_with_runtime())
|
||||
else:
|
||||
failure_input = None
|
||||
try:
|
||||
failure_input = schema_(messages=[msg], notfoo="bar")
|
||||
except Exception:
|
||||
# We'd get a validation error from pydantic state and wouldn't make it to the node
|
||||
# anyway
|
||||
pass
|
||||
if failure_input is not None:
|
||||
messages_ = node.invoke(
|
||||
failure_input, config=_create_config_with_runtime()
|
||||
)
|
||||
tool_message = messages_["messages"][-1]
|
||||
assert "KeyError" in tool_message.content
|
||||
tool_message = node.invoke([msg], config=_create_config_with_runtime())[
|
||||
-1
|
||||
]
|
||||
assert "KeyError" in tool_message.content
|
||||
|
||||
tool_call = {
|
||||
"name": "tool4",
|
||||
"args": {"some_val": 1},
|
||||
"id": "some 0",
|
||||
"type": "tool_call",
|
||||
}
|
||||
msg = AIMessage("hi?", tool_calls=[tool_call])
|
||||
result = node.invoke(
|
||||
schema_(messages=[msg], foo=""), config=_create_config_with_runtime()
|
||||
)
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "hi?"
|
||||
|
||||
result = node.invoke([msg], config=_create_config_with_runtime())
|
||||
tool_message = result[-1]
|
||||
assert tool_message.content == "hi?"
|
||||
|
||||
|
||||
def test_tool_node_inject_store() -> None:
|
||||
store = InMemoryStore()
|
||||
namespace = ("test",)
|
||||
|
||||
def tool1(some_val: int, store: Annotated[BaseStore, InjectedStore()]) -> str:
|
||||
"""Tool 1 docstring."""
|
||||
store_val = store.get(namespace, "test_key").value["foo"]
|
||||
return f"Some val: {some_val}, store val: {store_val}"
|
||||
|
||||
def tool2(some_val: int, store: Annotated[BaseStore, InjectedStore()]) -> str:
|
||||
"""Tool 2 docstring."""
|
||||
store_val = store.get(namespace, "test_key").value["foo"]
|
||||
return f"Some val: {some_val}, store val: {store_val}"
|
||||
|
||||
def tool3(
|
||||
some_val: int,
|
||||
bar: Annotated[str, InjectedState("bar")],
|
||||
store: Annotated[BaseStore, InjectedStore()],
|
||||
) -> str:
|
||||
"""Tool 3 docstring."""
|
||||
store_val = store.get(namespace, "test_key").value["foo"]
|
||||
return f"Some val: {some_val}, store val: {store_val}, state val: {bar}"
|
||||
|
||||
node = ToolNode([tool1, tool2, tool3], handle_tool_errors=True)
|
||||
store.put(namespace, "test_key", {"foo": "bar"})
|
||||
|
||||
class State(MessagesState):
|
||||
bar: str
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("tools", node)
|
||||
builder.add_edge(START, "tools")
|
||||
graph = builder.compile(store=store)
|
||||
|
||||
for tool_name in ("tool1", "tool2"):
|
||||
tool_call = {
|
||||
"name": tool_name,
|
||||
"args": {"some_val": 1},
|
||||
"id": "some 0",
|
||||
"type": "tool_call",
|
||||
}
|
||||
msg = AIMessage("hi?", tool_calls=[tool_call])
|
||||
node_result = node.invoke(
|
||||
{"messages": [msg]}, config=_create_config_with_runtime(store=store)
|
||||
)
|
||||
graph_result = graph.invoke({"messages": [msg]})
|
||||
for result in (node_result, graph_result):
|
||||
result["messages"][-1]
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "Some val: 1, store val: bar", (
|
||||
f"Failed for tool={tool_name}"
|
||||
)
|
||||
|
||||
tool_call = {
|
||||
"name": "tool3",
|
||||
"args": {"some_val": 1},
|
||||
"id": "some 0",
|
||||
"type": "tool_call",
|
||||
}
|
||||
msg = AIMessage("hi?", tool_calls=[tool_call])
|
||||
node_result = node.invoke(
|
||||
{"messages": [msg], "bar": "baz"},
|
||||
config=_create_config_with_runtime(store=store),
|
||||
)
|
||||
graph_result = graph.invoke({"messages": [msg], "bar": "baz"})
|
||||
for result in (node_result, graph_result):
|
||||
result["messages"][-1]
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "Some val: 1, store val: bar, state val: baz", (
|
||||
f"Failed for tool={tool_name}"
|
||||
)
|
||||
|
||||
# test injected store without passing store to compiled graph
|
||||
failing_graph = builder.compile()
|
||||
with pytest.raises(ValueError):
|
||||
failing_graph.invoke({"messages": [msg], "bar": "baz"})
|
||||
|
||||
|
||||
def test_tool_node_ensure_utf8() -> None:
|
||||
@dec_tool
|
||||
def get_day_list(days: list[str]) -> list[str]:
|
||||
"""choose days"""
|
||||
return days
|
||||
|
||||
data = ["星期一", "水曜日", "목요일", "Friday"]
|
||||
tools = [get_day_list]
|
||||
tool_calls = [ToolCall(name=get_day_list.name, args={"days": data}, id="test_id")]
|
||||
outputs: list[ToolMessage] = ToolNode(tools).invoke(
|
||||
[AIMessage(content="", tool_calls=tool_calls)],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
assert outputs[0].content == json.dumps(data, ensure_ascii=False)
|
||||
|
||||
|
||||
def test_tool_node_messages_key() -> None:
|
||||
@dec_tool
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Adds a and b."""
|
||||
return a + b
|
||||
|
||||
model = FakeToolCallingModel(
|
||||
tool_calls=[[ToolCall(name=add.name, args={"a": 1, "b": 2}, id="test_id")]]
|
||||
)
|
||||
|
||||
class State(TypedDict):
|
||||
subgraph_messages: Annotated[list[AnyMessage], add_messages]
|
||||
|
||||
def call_model(state: State) -> dict[str, Any]:
|
||||
response = model.invoke(state["subgraph_messages"])
|
||||
model.tool_calls = []
|
||||
return {"subgraph_messages": response}
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("agent", call_model)
|
||||
builder.add_node("tools", ToolNode([add], messages_key="subgraph_messages"))
|
||||
builder.add_conditional_edges(
|
||||
"agent", partial(tools_condition, messages_key="subgraph_messages")
|
||||
)
|
||||
builder.add_edge(START, "agent")
|
||||
builder.add_edge("tools", "agent")
|
||||
|
||||
graph = builder.compile()
|
||||
result = graph.invoke({"subgraph_messages": [HumanMessage(content="hi")]})
|
||||
assert result["subgraph_messages"] == [
|
||||
_AnyIdHumanMessage(content="hi"),
|
||||
AIMessage(
|
||||
content="hi",
|
||||
id="0",
|
||||
tool_calls=[ToolCall(name=add.name, args={"a": 1, "b": 2}, id="test_id")],
|
||||
),
|
||||
_AnyIdToolMessage(content="3", name=add.name, tool_call_id="test_id"),
|
||||
AIMessage(content="hi-hi-3", id="1"),
|
||||
]
|
||||
|
||||
|
||||
def test_tool_node_stream_writer() -> None:
|
||||
@dec_tool
|
||||
def streaming_tool(x: int) -> str:
|
||||
"""Do something with writer."""
|
||||
my_writer = get_stream_writer()
|
||||
for value in ["foo", "bar", "baz"]:
|
||||
my_writer({"custom_tool_value": value})
|
||||
|
||||
return x
|
||||
|
||||
tool_node = ToolNode([streaming_tool])
|
||||
graph = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("tools", tool_node)
|
||||
.add_edge(START, "tools")
|
||||
.compile()
|
||||
)
|
||||
|
||||
tool_call = {
|
||||
"name": "streaming_tool",
|
||||
"args": {"x": 1},
|
||||
"id": "1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
inputs = {
|
||||
"messages": [AIMessage("", tool_calls=[tool_call])],
|
||||
}
|
||||
|
||||
assert list(graph.stream(inputs, stream_mode="custom")) == [
|
||||
{"custom_tool_value": "foo"},
|
||||
{"custom_tool_value": "bar"},
|
||||
{"custom_tool_value": "baz"},
|
||||
]
|
||||
assert list(graph.stream(inputs, stream_mode=["custom", "updates"])) == [
|
||||
("custom", {"custom_tool_value": "foo"}),
|
||||
("custom", {"custom_tool_value": "bar"}),
|
||||
("custom", {"custom_tool_value": "baz"}),
|
||||
(
|
||||
"updates",
|
||||
{
|
||||
"tools": {
|
||||
"messages": [
|
||||
_AnyIdToolMessage(
|
||||
content="1",
|
||||
name="streaming_tool",
|
||||
tool_call_id="1",
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,578 +0,0 @@
|
||||
"""Test tool node interceptor handling of unregistered tools."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from langchain_core.messages import AIMessage, ToolMessage
|
||||
from langchain_core.runnables.config import RunnableConfig
|
||||
from langchain_core.tools import tool as dec_tool
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import Command
|
||||
|
||||
from langgraph.prebuilt import ToolNode
|
||||
from langgraph.prebuilt.tool_node import ToolCallRequest
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
def _create_mock_runtime(store: BaseStore | None = None) -> Mock:
|
||||
"""Create a mock Runtime object for testing ToolNode outside of graph context.
|
||||
|
||||
This helper is needed because ToolNode._func expects a Runtime parameter
|
||||
which is injected by RunnableCallable from config["configurable"]["__pregel_runtime"].
|
||||
When testing ToolNode directly (outside a graph), we need to provide this manually.
|
||||
"""
|
||||
mock_runtime = Mock()
|
||||
mock_runtime.store = store
|
||||
mock_runtime.context = None
|
||||
mock_runtime.stream_writer = lambda *args, **kwargs: None
|
||||
return mock_runtime
|
||||
|
||||
|
||||
def _create_config_with_runtime(store: BaseStore | None = None) -> RunnableConfig:
|
||||
"""Create a RunnableConfig with mock Runtime for testing ToolNode.
|
||||
|
||||
Returns:
|
||||
RunnableConfig with __pregel_runtime in configurable dict.
|
||||
"""
|
||||
return {"configurable": {"__pregel_runtime": _create_mock_runtime(store)}}
|
||||
|
||||
|
||||
@dec_tool
|
||||
def registered_tool(x: int) -> str:
|
||||
"""A registered tool."""
|
||||
return f"Result: {x}"
|
||||
|
||||
|
||||
def test_interceptor_can_handle_unregistered_tool_sync() -> None:
|
||||
"""Test that interceptor can handle requests for unregistered tools (sync)."""
|
||||
|
||||
def interceptor(
|
||||
request: ToolCallRequest,
|
||||
execute: Callable[[ToolCallRequest], ToolMessage | Command],
|
||||
) -> ToolMessage | Command:
|
||||
"""Intercept and handle unregistered tools."""
|
||||
if request.tool_call["name"] == "unregistered_tool":
|
||||
# Short-circuit without calling execute for unregistered tool
|
||||
return ToolMessage(
|
||||
content="Handled by interceptor",
|
||||
tool_call_id=request.tool_call["id"],
|
||||
name="unregistered_tool",
|
||||
)
|
||||
# Pass through for registered tools
|
||||
return execute(request)
|
||||
|
||||
node = ToolNode([registered_tool], wrap_tool_call=interceptor)
|
||||
|
||||
# Test registered tool works normally
|
||||
result = node.invoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "registered_tool",
|
||||
"args": {"x": 42},
|
||||
"id": "1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
assert result[0].content == "Result: 42"
|
||||
assert result[0].tool_call_id == "1"
|
||||
|
||||
# Test unregistered tool is intercepted and handled
|
||||
result = node.invoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "unregistered_tool",
|
||||
"args": {"x": 99},
|
||||
"id": "2",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
assert result[0].content == "Handled by interceptor"
|
||||
assert result[0].tool_call_id == "2"
|
||||
assert result[0].name == "unregistered_tool"
|
||||
|
||||
|
||||
async def test_interceptor_can_handle_unregistered_tool_async() -> None:
|
||||
"""Test that interceptor can handle requests for unregistered tools (async)."""
|
||||
|
||||
async def async_interceptor(
|
||||
request: ToolCallRequest,
|
||||
execute: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
|
||||
) -> ToolMessage | Command:
|
||||
"""Intercept and handle unregistered tools."""
|
||||
if request.tool_call["name"] == "unregistered_tool":
|
||||
# Short-circuit without calling execute for unregistered tool
|
||||
return ToolMessage(
|
||||
content="Handled by async interceptor",
|
||||
tool_call_id=request.tool_call["id"],
|
||||
name="unregistered_tool",
|
||||
)
|
||||
# Pass through for registered tools
|
||||
return await execute(request)
|
||||
|
||||
node = ToolNode([registered_tool], awrap_tool_call=async_interceptor)
|
||||
|
||||
# Test registered tool works normally
|
||||
result = await node.ainvoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "registered_tool",
|
||||
"args": {"x": 42},
|
||||
"id": "1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
assert result[0].content == "Result: 42"
|
||||
assert result[0].tool_call_id == "1"
|
||||
|
||||
# Test unregistered tool is intercepted and handled
|
||||
result = await node.ainvoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "unregistered_tool",
|
||||
"args": {"x": 99},
|
||||
"id": "2",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
assert result[0].content == "Handled by async interceptor"
|
||||
assert result[0].tool_call_id == "2"
|
||||
assert result[0].name == "unregistered_tool"
|
||||
|
||||
|
||||
def test_unregistered_tool_error_when_interceptor_calls_execute() -> None:
|
||||
"""Test that unregistered tools error if interceptor tries to execute them."""
|
||||
|
||||
def bad_interceptor(
|
||||
request: ToolCallRequest,
|
||||
execute: Callable[[ToolCallRequest], ToolMessage | Command],
|
||||
) -> ToolMessage | Command:
|
||||
"""Interceptor that tries to execute unregistered tool."""
|
||||
# This should fail validation when execute is called
|
||||
return execute(request)
|
||||
|
||||
node = ToolNode([registered_tool], wrap_tool_call=bad_interceptor)
|
||||
|
||||
# Registered tool should still work
|
||||
result = node.invoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "registered_tool",
|
||||
"args": {"x": 42},
|
||||
"id": "1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
assert result[0].content == "Result: 42"
|
||||
|
||||
# Unregistered tool should error when interceptor calls execute
|
||||
result = node.invoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "unregistered_tool",
|
||||
"args": {"x": 99},
|
||||
"id": "2",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
# Should get validation error message
|
||||
assert result[0].status == "error"
|
||||
assert "is not a valid tool" in result[0].content
|
||||
assert result[0].tool_call_id == "2"
|
||||
|
||||
|
||||
def test_interceptor_handles_mix_of_registered_and_unregistered() -> None:
|
||||
"""Test interceptor handling mix of registered and unregistered tools."""
|
||||
|
||||
def selective_interceptor(
|
||||
request: ToolCallRequest,
|
||||
execute: Callable[[ToolCallRequest], ToolMessage | Command],
|
||||
) -> ToolMessage | Command:
|
||||
"""Handle unregistered tools, pass through registered ones."""
|
||||
if request.tool_call["name"] == "magic_tool":
|
||||
return ToolMessage(
|
||||
content=f"Magic result: {request.tool_call['args'].get('value', 0) * 2}",
|
||||
tool_call_id=request.tool_call["id"],
|
||||
name="magic_tool",
|
||||
)
|
||||
return execute(request)
|
||||
|
||||
node = ToolNode([registered_tool], wrap_tool_call=selective_interceptor)
|
||||
|
||||
# Test multiple tool calls - mix of registered and unregistered
|
||||
result = node.invoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "registered_tool",
|
||||
"args": {"x": 10},
|
||||
"id": "1",
|
||||
"type": "tool_call",
|
||||
},
|
||||
{
|
||||
"name": "magic_tool",
|
||||
"args": {"value": 5},
|
||||
"id": "2",
|
||||
"type": "tool_call",
|
||||
},
|
||||
{
|
||||
"name": "registered_tool",
|
||||
"args": {"x": 20},
|
||||
"id": "3",
|
||||
"type": "tool_call",
|
||||
},
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
# All tools should execute successfully
|
||||
assert len(result) == 3
|
||||
assert result[0].content == "Result: 10"
|
||||
assert result[0].tool_call_id == "1"
|
||||
assert result[1].content == "Magic result: 10"
|
||||
assert result[1].tool_call_id == "2"
|
||||
assert result[2].content == "Result: 20"
|
||||
assert result[2].tool_call_id == "3"
|
||||
|
||||
|
||||
def test_interceptor_command_for_unregistered_tool() -> None:
|
||||
"""Test interceptor returning Command for unregistered tool."""
|
||||
|
||||
def command_interceptor(
|
||||
request: ToolCallRequest,
|
||||
execute: Callable[[ToolCallRequest], ToolMessage | Command],
|
||||
) -> ToolMessage | Command:
|
||||
"""Return Command for unregistered tools."""
|
||||
if request.tool_call["name"] == "routing_tool":
|
||||
return Command(
|
||||
update=[
|
||||
ToolMessage(
|
||||
content="Routing to special handler",
|
||||
tool_call_id=request.tool_call["id"],
|
||||
name="routing_tool",
|
||||
)
|
||||
],
|
||||
goto="special_node",
|
||||
)
|
||||
return execute(request)
|
||||
|
||||
node = ToolNode([registered_tool], wrap_tool_call=command_interceptor)
|
||||
|
||||
result = node.invoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "routing_tool",
|
||||
"args": {},
|
||||
"id": "1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
# Should get Command back
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], Command)
|
||||
assert result[0].goto == "special_node"
|
||||
assert result[0].update is not None
|
||||
assert len(result[0].update) == 1
|
||||
assert result[0].update[0].content == "Routing to special handler"
|
||||
|
||||
|
||||
def test_interceptor_exception_with_unregistered_tool() -> None:
|
||||
"""Test that interceptor exceptions are caught by error handling."""
|
||||
|
||||
def failing_interceptor(
|
||||
request: ToolCallRequest,
|
||||
execute: Callable[[ToolCallRequest], ToolMessage | Command],
|
||||
) -> ToolMessage | Command:
|
||||
"""Interceptor that throws exception for unregistered tools."""
|
||||
if request.tool_call["name"] == "bad_tool":
|
||||
msg = "Interceptor failed"
|
||||
raise ValueError(msg)
|
||||
return execute(request)
|
||||
|
||||
node = ToolNode(
|
||||
[registered_tool], wrap_tool_call=failing_interceptor, handle_tool_errors=True
|
||||
)
|
||||
|
||||
# Interceptor exception should be caught and converted to error message
|
||||
result = node.invoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "bad_tool",
|
||||
"args": {},
|
||||
"id": "1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "error"
|
||||
assert "Interceptor failed" in result[0].content
|
||||
assert result[0].tool_call_id == "1"
|
||||
|
||||
# Test that exception is raised when handle_tool_errors is False
|
||||
node_no_handling = ToolNode(
|
||||
[registered_tool], wrap_tool_call=failing_interceptor, handle_tool_errors=False
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Interceptor failed"):
|
||||
node_no_handling.invoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "bad_tool",
|
||||
"args": {},
|
||||
"id": "2",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
|
||||
async def test_async_interceptor_exception_with_unregistered_tool() -> None:
|
||||
"""Test that async interceptor exceptions are caught by error handling."""
|
||||
|
||||
async def failing_async_interceptor(
|
||||
request: ToolCallRequest,
|
||||
execute: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
|
||||
) -> ToolMessage | Command:
|
||||
"""Async interceptor that throws exception for unregistered tools."""
|
||||
if request.tool_call["name"] == "bad_async_tool":
|
||||
msg = "Async interceptor failed"
|
||||
raise RuntimeError(msg)
|
||||
return await execute(request)
|
||||
|
||||
node = ToolNode(
|
||||
[registered_tool],
|
||||
awrap_tool_call=failing_async_interceptor,
|
||||
handle_tool_errors=True,
|
||||
)
|
||||
|
||||
# Interceptor exception should be caught and converted to error message
|
||||
result = await node.ainvoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "bad_async_tool",
|
||||
"args": {},
|
||||
"id": "1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "error"
|
||||
assert "Async interceptor failed" in result[0].content
|
||||
assert result[0].tool_call_id == "1"
|
||||
|
||||
# Test that exception is raised when handle_tool_errors is False
|
||||
node_no_handling = ToolNode(
|
||||
[registered_tool],
|
||||
awrap_tool_call=failing_async_interceptor,
|
||||
handle_tool_errors=False,
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="Async interceptor failed"):
|
||||
await node_no_handling.ainvoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "bad_async_tool",
|
||||
"args": {},
|
||||
"id": "2",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
|
||||
def test_interceptor_with_dict_input_format() -> None:
|
||||
"""Test that interceptor works with dict input format."""
|
||||
|
||||
def interceptor(
|
||||
request: ToolCallRequest,
|
||||
execute: Callable[[ToolCallRequest], ToolMessage | Command],
|
||||
) -> ToolMessage | Command:
|
||||
"""Intercept unregistered tools with dict input."""
|
||||
if request.tool_call["name"] == "dict_tool":
|
||||
return ToolMessage(
|
||||
content="Handled dict input",
|
||||
tool_call_id=request.tool_call["id"],
|
||||
name="dict_tool",
|
||||
)
|
||||
return execute(request)
|
||||
|
||||
node = ToolNode([registered_tool], wrap_tool_call=interceptor)
|
||||
|
||||
# Test with dict input format
|
||||
result = node.invoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "dict_tool",
|
||||
"args": {"value": 5},
|
||||
"id": "1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
# Should return dict format output
|
||||
assert isinstance(result, dict)
|
||||
assert "messages" in result
|
||||
assert len(result["messages"]) == 1
|
||||
assert result["messages"][0].content == "Handled dict input"
|
||||
assert result["messages"][0].tool_call_id == "1"
|
||||
|
||||
|
||||
def test_interceptor_verifies_tool_is_none_for_unregistered() -> None:
|
||||
"""Test that request.tool is None for unregistered tools."""
|
||||
|
||||
captured_requests: list[ToolCallRequest] = []
|
||||
|
||||
def capturing_interceptor(
|
||||
request: ToolCallRequest,
|
||||
execute: Callable[[ToolCallRequest], ToolMessage | Command],
|
||||
) -> ToolMessage | Command:
|
||||
"""Capture request to verify tool field."""
|
||||
captured_requests.append(request)
|
||||
if request.tool is None:
|
||||
# Tool is unregistered
|
||||
return ToolMessage(
|
||||
content=f"Unregistered: {request.tool_call['name']}",
|
||||
tool_call_id=request.tool_call["id"],
|
||||
name=request.tool_call["name"],
|
||||
)
|
||||
# Tool is registered
|
||||
return execute(request)
|
||||
|
||||
node = ToolNode([registered_tool], wrap_tool_call=capturing_interceptor)
|
||||
|
||||
# Test unregistered tool
|
||||
node.invoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "unknown_tool",
|
||||
"args": {},
|
||||
"id": "1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
assert len(captured_requests) == 1
|
||||
assert captured_requests[0].tool is None
|
||||
assert captured_requests[0].tool_call["name"] == "unknown_tool"
|
||||
|
||||
# Clear and test registered tool
|
||||
captured_requests.clear()
|
||||
node.invoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "registered_tool",
|
||||
"args": {"x": 10},
|
||||
"id": "2",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
assert len(captured_requests) == 1
|
||||
assert captured_requests[0].tool is not None
|
||||
assert captured_requests[0].tool.name == "registered_tool"
|
||||
@@ -1,470 +0,0 @@
|
||||
"""Unit tests for ValidationError filtering in ToolNode.
|
||||
|
||||
This module tests that validation errors are filtered to only include arguments
|
||||
that the LLM controls. Injected arguments (InjectedState, InjectedStore,
|
||||
ToolRuntime) are automatically provided by the system and should not appear in
|
||||
validation error messages. This ensures the LLM receives focused, actionable
|
||||
feedback about the parameters it can actually control, improving error correction
|
||||
and reducing confusion from irrelevant system implementation details.
|
||||
"""
|
||||
|
||||
from typing import Annotated
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from langchain_core.messages import AIMessage
|
||||
from langchain_core.runnables.config import RunnableConfig
|
||||
from langchain_core.tools import tool as dec_tool
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
|
||||
from langgraph.prebuilt import InjectedState, InjectedStore, ToolNode, ToolRuntime
|
||||
from langgraph.prebuilt.tool_node import ToolInvocationError
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
def _create_mock_runtime(store: BaseStore | None = None) -> Mock:
|
||||
"""Create a mock Runtime object for testing ToolNode outside of graph context."""
|
||||
mock_runtime = Mock()
|
||||
mock_runtime.store = store
|
||||
mock_runtime.context = None
|
||||
mock_runtime.stream_writer = lambda *args, **kwargs: None
|
||||
return mock_runtime
|
||||
|
||||
|
||||
def _create_config_with_runtime(store: BaseStore | None = None) -> RunnableConfig:
|
||||
"""Create a RunnableConfig with mock Runtime for testing ToolNode."""
|
||||
return {"configurable": {"__pregel_runtime": _create_mock_runtime(store)}}
|
||||
|
||||
|
||||
async def test_filter_injected_state_validation_errors() -> None:
|
||||
"""Test that validation errors for InjectedState arguments are filtered out.
|
||||
|
||||
InjectedState parameters are not controlled by the LLM, so any validation
|
||||
errors related to them should not appear in error messages. This ensures
|
||||
the LLM receives only actionable feedback about its own tool call arguments.
|
||||
"""
|
||||
|
||||
@dec_tool
|
||||
def my_tool(
|
||||
value: int,
|
||||
state: Annotated[dict, InjectedState],
|
||||
) -> str:
|
||||
"""Tool that uses injected state.
|
||||
|
||||
Args:
|
||||
value: An integer value.
|
||||
state: The graph state (injected).
|
||||
"""
|
||||
return f"value={value}, messages={len(state.get('messages', []))}"
|
||||
|
||||
tool_node = ToolNode([my_tool])
|
||||
|
||||
# Call with invalid 'value' argument (should be int, not str)
|
||||
result = await tool_node.ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "my_tool",
|
||||
"args": {"value": "not_an_int"}, # Invalid type
|
||||
"id": "call_1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
# Should get a ToolMessage with error
|
||||
assert len(result["messages"]) == 1
|
||||
tool_message = result["messages"][0]
|
||||
assert tool_message.status == "error"
|
||||
assert tool_message.tool_call_id == "call_1"
|
||||
|
||||
# Error should mention 'value' but NOT 'state' (which is injected)
|
||||
assert "value" in tool_message.content
|
||||
assert "state" not in tool_message.content.lower()
|
||||
|
||||
|
||||
async def test_filter_injected_store_validation_errors() -> None:
|
||||
"""Test that validation errors for InjectedStore arguments are filtered out.
|
||||
|
||||
InjectedStore parameters are not controlled by the LLM, so any validation
|
||||
errors related to them should not appear in error messages. This keeps
|
||||
error feedback focused on LLM-controllable parameters.
|
||||
"""
|
||||
|
||||
@dec_tool
|
||||
def my_tool(
|
||||
key: str,
|
||||
store: Annotated[BaseStore, InjectedStore()],
|
||||
) -> str:
|
||||
"""Tool that uses injected store.
|
||||
|
||||
Args:
|
||||
key: A key to look up.
|
||||
store: The persistent store (injected).
|
||||
"""
|
||||
return f"key={key}"
|
||||
|
||||
tool_node = ToolNode([my_tool])
|
||||
|
||||
# Call with invalid 'key' argument (missing required argument)
|
||||
result = await tool_node.ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "my_tool",
|
||||
"args": {}, # Missing 'key'
|
||||
"id": "call_1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(store=InMemoryStore()),
|
||||
)
|
||||
|
||||
# Should get a ToolMessage with error
|
||||
assert len(result["messages"]) == 1
|
||||
tool_message = result["messages"][0]
|
||||
assert tool_message.status == "error"
|
||||
|
||||
# Error should mention 'key' is required
|
||||
assert "key" in tool_message.content.lower()
|
||||
# The error should be about 'key' field specifically (not about store field)
|
||||
# Note: 'store' might appear in input_value representation, but the validation
|
||||
# error itself should only be for 'key'
|
||||
assert (
|
||||
"field required" in tool_message.content.lower()
|
||||
or "missing" in tool_message.content.lower()
|
||||
)
|
||||
|
||||
|
||||
async def test_filter_tool_runtime_validation_errors() -> None:
|
||||
"""Test that validation errors for ToolRuntime arguments are filtered out.
|
||||
|
||||
ToolRuntime parameters are not controlled by the LLM, so any validation
|
||||
errors related to them should not appear in error messages. This ensures
|
||||
the LLM only sees errors for parameters it can fix.
|
||||
"""
|
||||
|
||||
@dec_tool
|
||||
def my_tool(
|
||||
query: str,
|
||||
runtime: ToolRuntime,
|
||||
) -> str:
|
||||
"""Tool that uses ToolRuntime.
|
||||
|
||||
Args:
|
||||
query: A query string.
|
||||
runtime: The tool runtime context (injected).
|
||||
"""
|
||||
return f"query={query}"
|
||||
|
||||
tool_node = ToolNode([my_tool])
|
||||
|
||||
# Call with invalid 'query' argument (wrong type)
|
||||
result = await tool_node.ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "my_tool",
|
||||
"args": {"query": 123}, # Should be str, not int
|
||||
"id": "call_1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
# Should get a ToolMessage with error
|
||||
assert len(result["messages"]) == 1
|
||||
tool_message = result["messages"][0]
|
||||
assert tool_message.status == "error"
|
||||
|
||||
# Error should mention 'query' but NOT 'runtime' (which is injected)
|
||||
assert "query" in tool_message.content.lower()
|
||||
assert "runtime" not in tool_message.content.lower()
|
||||
|
||||
|
||||
async def test_filter_multiple_injected_args() -> None:
|
||||
"""Test filtering when a tool has multiple injected arguments.
|
||||
|
||||
When a tool uses multiple injected parameters (state, store, runtime), none of
|
||||
them should appear in validation error messages since they're all system-provided
|
||||
and not controlled by the LLM. Only LLM-controllable parameter errors should appear.
|
||||
"""
|
||||
|
||||
@dec_tool
|
||||
def my_tool(
|
||||
value: int,
|
||||
state: Annotated[dict, InjectedState],
|
||||
store: Annotated[BaseStore, InjectedStore()],
|
||||
runtime: ToolRuntime,
|
||||
) -> str:
|
||||
"""Tool with multiple injected arguments.
|
||||
|
||||
Args:
|
||||
value: An integer value.
|
||||
state: The graph state (injected).
|
||||
store: The persistent store (injected).
|
||||
runtime: The tool runtime context (injected).
|
||||
"""
|
||||
return f"value={value}"
|
||||
|
||||
tool_node = ToolNode([my_tool])
|
||||
|
||||
# Call with invalid 'value' - injected args should be filtered from error
|
||||
result = await tool_node.ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "my_tool",
|
||||
"args": {"value": "not_an_int"},
|
||||
"id": "call_1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(store=InMemoryStore()),
|
||||
)
|
||||
|
||||
tool_message = result["messages"][0]
|
||||
assert tool_message.status == "error"
|
||||
|
||||
# Only 'value' error should be reported
|
||||
assert "value" in tool_message.content
|
||||
# None of the injected args should appear in error
|
||||
assert "state" not in tool_message.content.lower()
|
||||
assert "store" not in tool_message.content.lower()
|
||||
assert "runtime" not in tool_message.content.lower()
|
||||
|
||||
|
||||
async def test_no_filtering_when_all_errors_are_model_args() -> None:
|
||||
"""Test that validation errors for LLM-controlled arguments are preserved.
|
||||
|
||||
When validation fails for arguments the LLM controls, those errors should
|
||||
be fully reported to help the LLM correct its tool calls. This ensures
|
||||
the LLM receives complete feedback about all issues it can fix.
|
||||
"""
|
||||
|
||||
@dec_tool
|
||||
def my_tool(
|
||||
value1: int,
|
||||
value2: str,
|
||||
state: Annotated[dict, InjectedState],
|
||||
) -> str:
|
||||
"""Tool with both regular and injected arguments.
|
||||
|
||||
Args:
|
||||
value1: First value.
|
||||
value2: Second value.
|
||||
state: The graph state (injected).
|
||||
"""
|
||||
return f"value1={value1}, value2={value2}"
|
||||
|
||||
tool_node = ToolNode([my_tool])
|
||||
|
||||
# Call with invalid arguments for BOTH non-injected parameters
|
||||
result = await tool_node.ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "my_tool",
|
||||
"args": {
|
||||
"value1": "not_an_int", # Invalid
|
||||
"value2": 456, # Invalid (should be str)
|
||||
},
|
||||
"id": "call_1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
tool_message = result["messages"][0]
|
||||
assert tool_message.status == "error"
|
||||
|
||||
# Both errors should be present
|
||||
assert "value1" in tool_message.content
|
||||
assert "value2" in tool_message.content
|
||||
# Injected state should not appear
|
||||
assert "state" not in tool_message.content.lower()
|
||||
|
||||
|
||||
async def test_validation_error_with_no_injected_args() -> None:
|
||||
"""Test that tools without injected arguments show all validation errors.
|
||||
|
||||
For tools that only have LLM-controlled parameters, all validation errors
|
||||
should be reported since everything is under the LLM's control and can be
|
||||
corrected by the LLM in subsequent tool calls.
|
||||
"""
|
||||
|
||||
@dec_tool
|
||||
def my_tool(value1: int, value2: str) -> str:
|
||||
"""Regular tool without injected arguments.
|
||||
|
||||
Args:
|
||||
value1: First value.
|
||||
value2: Second value.
|
||||
"""
|
||||
return f"{value1} {value2}"
|
||||
|
||||
tool_node = ToolNode([my_tool])
|
||||
|
||||
result = await tool_node.ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "my_tool",
|
||||
"args": {"value1": "invalid", "value2": 123},
|
||||
"id": "call_1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
tool_message = result["messages"][0]
|
||||
assert tool_message.status == "error"
|
||||
|
||||
# Both errors should be present since there are no injected args to filter
|
||||
assert "value1" in tool_message.content
|
||||
assert "value2" in tool_message.content
|
||||
|
||||
|
||||
async def test_tool_invocation_error_without_handle_errors() -> None:
|
||||
"""Test that ToolInvocationError contains only LLM-controlled parameter errors.
|
||||
|
||||
When handle_tool_errors is False, the raised ToolInvocationError should still
|
||||
filter out system-injected arguments from the error details, ensuring that
|
||||
error messages focus on what the LLM can control.
|
||||
"""
|
||||
|
||||
@dec_tool
|
||||
def my_tool(
|
||||
value: int,
|
||||
state: Annotated[dict, InjectedState],
|
||||
) -> str:
|
||||
"""Tool with injected state.
|
||||
|
||||
Args:
|
||||
value: An integer value.
|
||||
state: The graph state (injected).
|
||||
"""
|
||||
return f"value={value}"
|
||||
|
||||
tool_node = ToolNode([my_tool], handle_tool_errors=False)
|
||||
|
||||
# Should raise ToolInvocationError with filtered errors
|
||||
with pytest.raises(ToolInvocationError) as exc_info:
|
||||
await tool_node.ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "my_tool",
|
||||
"args": {"value": "not_an_int"},
|
||||
"id": "call_1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
error = exc_info.value
|
||||
assert error.tool_name == "my_tool"
|
||||
assert error.filtered_errors is not None
|
||||
assert len(error.filtered_errors) > 0
|
||||
|
||||
# Filtered errors should only contain 'value' error, not 'state'
|
||||
error_locs = [err["loc"] for err in error.filtered_errors]
|
||||
assert any("value" in str(loc) for loc in error_locs)
|
||||
assert not any("state" in str(loc) for loc in error_locs)
|
||||
|
||||
|
||||
async def test_sync_tool_validation_error_filtering() -> None:
|
||||
"""Test that error filtering works for sync tools.
|
||||
|
||||
Error filtering should work identically for both sync and async tool execution,
|
||||
excluding injected arguments from validation error messages.
|
||||
"""
|
||||
|
||||
@dec_tool
|
||||
def my_tool(
|
||||
value: int,
|
||||
state: Annotated[dict, InjectedState],
|
||||
) -> str:
|
||||
"""Sync tool with injected state.
|
||||
|
||||
Args:
|
||||
value: An integer value.
|
||||
state: The graph state (injected).
|
||||
"""
|
||||
return f"value={value}"
|
||||
|
||||
tool_node = ToolNode([my_tool])
|
||||
|
||||
# Test sync invocation
|
||||
result = tool_node.invoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "my_tool",
|
||||
"args": {"value": "not_an_int"},
|
||||
"id": "call_1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
tool_message = result["messages"][0]
|
||||
assert tool_message.status == "error"
|
||||
assert "value" in tool_message.content
|
||||
assert "state" not in tool_message.content.lower()
|
||||
Generated
+9
-65
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
@@ -228,7 +228,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "1.0.0"
|
||||
version = "0.3.76"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -239,14 +239,14 @@ dependencies = [
|
||||
{ name = "tenacity" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/d0/9db6d375ecf8bd498fcc87016e43c3d930ddbfbacf9a1e99018ada4e824f/langchain_core-1.0.0.tar.gz", hash = "sha256:8e81c94a22fa3a362a0f101bbd1271bf3725e50cf1e31c84e8f4a1c731279785", size = 764484, upload-time = "2025-10-17T13:48:24.408Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4f/4d/5e2ea7754ee0a1f524c412801c6ba9ad49318ecb58b0d524903c3d9efe0a/langchain_core-0.3.76.tar.gz", hash = "sha256:71136a122dd1abae2c289c5809d035cf12b5f2bb682d8a4c1078cd94feae7419", size = 573568, upload-time = "2025-09-10T14:49:39.863Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/68/6a/8dd566cb7379d6e3a921f94713babba2f71cbed65c73c784c649c1fd7d4e/langchain_core-1.0.0-py3-none-any.whl", hash = "sha256:a94561bf75dd097c7d6e3864950f28dadc963f0bd810114de4095f41f634059b", size = 467157, upload-time = "2025-10-17T13:48:23.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/b5/501c0ffcb09c734457ceaa86bc7b1dd37b6a261147bd653add03b838aacb/langchain_core-0.3.76-py3-none-any.whl", hash = "sha256:46e0eb48c7ac532432d51f8ca1ece1804c82afe9ae3dcf027b867edadf82b3ec", size = 447508, upload-time = "2025-09-10T14:49:38.179Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "1.0.2"
|
||||
version = "1.0.1"
|
||||
source = { editable = "../langgraph" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -271,7 +271,7 @@ requires-dist = [
|
||||
dev = [
|
||||
{ name = "httpx" },
|
||||
{ name = "jupyter" },
|
||||
{ name = "langchain-core", specifier = ">=1.0.0" },
|
||||
{ name = "langchain-core", specifier = "==1.0.0a1" },
|
||||
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
|
||||
{ name = "langgraph-checkpoint-postgres", editable = "../checkpoint-postgres" },
|
||||
{ name = "langgraph-checkpoint-sqlite", editable = "../checkpoint-sqlite" },
|
||||
@@ -304,7 +304,7 @@ lint = [
|
||||
]
|
||||
test = [
|
||||
{ name = "httpx" },
|
||||
{ name = "langchain-core", specifier = ">=1.0.0" },
|
||||
{ name = "langchain-core", specifier = "==1.0.0a1" },
|
||||
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
|
||||
{ name = "langgraph-checkpoint-postgres", editable = "../checkpoint-postgres" },
|
||||
{ name = "langgraph-checkpoint-sqlite", editable = "../checkpoint-sqlite" },
|
||||
@@ -467,7 +467,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "1.0.2"
|
||||
version = "1.0.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -483,7 +483,6 @@ dev = [
|
||||
{ name = "langgraph-checkpoint-postgres" },
|
||||
{ name = "langgraph-checkpoint-sqlite" },
|
||||
{ name = "mypy" },
|
||||
{ name = "psycopg-binary" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
@@ -502,7 +501,6 @@ test = [
|
||||
{ name = "langgraph-checkpoint" },
|
||||
{ name = "langgraph-checkpoint-postgres" },
|
||||
{ name = "langgraph-checkpoint-sqlite" },
|
||||
{ name = "psycopg-binary" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
@@ -512,7 +510,7 @@ test = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "langchain-core", specifier = ">=1.0.0" },
|
||||
{ name = "langchain-core", specifier = ">=0.3.67" },
|
||||
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
|
||||
]
|
||||
|
||||
@@ -525,7 +523,6 @@ dev = [
|
||||
{ name = "langgraph-checkpoint-postgres", editable = "../checkpoint-postgres" },
|
||||
{ name = "langgraph-checkpoint-sqlite", editable = "../checkpoint-sqlite" },
|
||||
{ name = "mypy" },
|
||||
{ name = "psycopg-binary" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
@@ -544,7 +541,6 @@ test = [
|
||||
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
|
||||
{ name = "langgraph-checkpoint-postgres", editable = "../checkpoint-postgres" },
|
||||
{ name = "langgraph-checkpoint-sqlite", editable = "../checkpoint-sqlite" },
|
||||
{ name = "psycopg-binary" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
@@ -832,58 +828,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/90/422ffbbeeb9418c795dae2a768db860401446af0c6768bc061ce22325f58/psycopg-3.2.10-py3-none-any.whl", hash = "sha256:ab5caf09a9ec42e314a21f5216dbcceac528e0e05142e42eea83a3b28b320ac3", size = 206586, upload-time = "2025-09-08T09:07:50.121Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg-binary"
|
||||
version = "3.2.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/96/9fe31ef61b311c697a98709a31b875d152e4f67924dd2cb94a4de0396d74/psycopg_binary-3.2.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f72146ad5b69ea177c2707578e5a4a9422b79e50d5a80992dabc5619b0929771", size = 4031016, upload-time = "2025-10-18T22:43:35.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/fe/3ae6be34bfda1ba6dfd4e3b5c1d68bc51d4593399b5a10faaff68937c9a1/psycopg_binary-3.2.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b051aa1e67f0d03ccdb4503d716f22da56229896526f0aa721e5a199baa9e5d4", size = 4090430, upload-time = "2025-10-18T22:43:41.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/ea/7aa84f6bb64f94bfbe7d494d384a0d2bc66ba66e8607f5e9b515aa6af627/psycopg_binary-3.2.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49d76391b225f72dd63fcab87937ccf307ae0f093b5a382eeacf05f19a57c176", size = 4641307, upload-time = "2025-10-18T22:43:45.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/67/ef12ff8a530230824965668b44ccd58a88dae40511f7bbd125defb7972c4/psycopg_binary-3.2.11-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:58997db1aa48a1119e26c1c2f893d1c92339bd3be5d1f25334f22eaeaeeca90e", size = 4742204, upload-time = "2025-10-18T22:43:50.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/9c/8f35345fe22a0e5997cbfba0b7e1a58f26b290400cb9a6cb67e72e503331/psycopg_binary-3.2.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e3b6328bc2f3ca233f9a5f08d266089b96a534eca9ee4e45cb92d0a8d4629d9c", size = 4425352, upload-time = "2025-10-18T22:43:55.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/29/f0c585c6b48526f0ecf179e13ea2b6d8fed0dbba8c1a0d61da8ece149b0e/psycopg_binary-3.2.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bc571786a256a2fa2d8f13b5ecf714020b753bc76c2fa6d308e46751946dc31", size = 3885019, upload-time = "2025-10-18T22:43:59.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/6d/a139e1c7e9840491d9ad3c837264a900ac95ba35e5f83fd5715b1ce7a729/psycopg_binary-3.2.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:766089fdaa8af1b5f7e2ec9fd7ad190c865e226b4fb0e7b1bd8dbcd62b5b923e", size = 3568192, upload-time = "2025-10-18T22:44:03.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/ea/43a2b6fcfa816797dc6d2ac67e9cd09b3a7e4da0a29467a8b5940e7a1312/psycopg_binary-3.2.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5fb27dd9c52ae13cb4de90244207155b694f76a75a816115ead2d573f40e1e36", size = 3609300, upload-time = "2025-10-18T22:44:09.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/d5/c9d46e626528a44b0feb881064e8018107b603ac683a658be3ee9ca00222/psycopg_binary-3.2.11-cp310-cp310-win_amd64.whl", hash = "sha256:3f32b09fba85d9e239229bdc5b6254420c02054f6954fe7fbd1ecf1ca93009ed", size = 2918105, upload-time = "2025-10-18T22:44:14.203Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/c4/350473820759d7e599e68bd79c88d32376353ceb0f764db05de8f13ff421/psycopg_binary-3.2.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6688807ed07436c18e9946d01372bc80b9d20b7732cde27de9313e0860910c84", size = 4037740, upload-time = "2025-10-18T22:44:21.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e0/00bf3e207676bbe6e9f32c0f924f0e5be1efcd1a9fb2fd84d1c3d9958a96/psycopg_binary-3.2.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:478a68d50f34f6203642d245e2046d266c719ab4e593a1bb94c3be5f82e1aee1", size = 4098558, upload-time = "2025-10-18T22:44:26.948Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/bc1d22fe57b01fa76b02943e1034cb59070bf906e982ebc507d079998b5b/psycopg_binary-3.2.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7575ca710277cc3e9257ff803a3e0e3cb7cc1b7851639cb783a7cd55ebfc815", size = 4646689, upload-time = "2025-10-18T22:44:31.055Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/6e/b1234e784af5c999ca4bd2e3a8673c58e941926dc4a53b9196d00929f7c9/psycopg_binary-3.2.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:110a2036007230416fcc2c17bfe7aaa2c1fa9b6e9d21e2cd551523e3f6489759", size = 4749164, upload-time = "2025-10-18T22:44:39.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/77/98c2e6c683941e54560ef3449fbc97b7ca31318436576e0c9d92c1dc875d/psycopg_binary-3.2.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:31f1d5630afa673c37a6327f8e3efa1f17d4e4e42972643b3478b52275233529", size = 4432473, upload-time = "2025-10-18T22:44:45.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/1d/73e72427152c03f61b75c14642a7187b16be0e03480f7329ab5cf618fdac/psycopg_binary-3.2.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9f12a34bddaeffa7840a61163595ec0d70a9db855896865dcfbb731510014484", size = 3890114, upload-time = "2025-10-18T22:44:49.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/ed/08a6b135ece52bb4024e19d03a294f002992d2f0c60fccdc35f245801d9c/psycopg_binary-3.2.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:82fe30afbdd66fbdad583b02baad5c15930a3dc8a3756d2ae15fc874e9be8ec8", size = 3571474, upload-time = "2025-10-18T22:44:52.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/5c/b0c857cd0718b1a8af86a24e61deeb9643e9e4731f732a9b7cab280b1323/psycopg_binary-3.2.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:592fb928efe0674a7400af914bcf931eb5267d36237925947aaecf63bd9a91aa", size = 3613401, upload-time = "2025-10-18T22:44:56.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/29/437255bc149b132c63ab0279f8850648cd3ae524667f475b621a2a3d0d5b/psycopg_binary-3.2.11-cp311-cp311-win_amd64.whl", hash = "sha256:20d41bcd9ac289d44ac1f6151594f7883483b4ad14680a63e04b639dc90c3349", size = 2919850, upload-time = "2025-10-18T22:45:00.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9e/58945c828b60820e5c192d04f238f1aa49de0fe5f3b9883e277f33c17c0a/psycopg_binary-3.2.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4cae9bdc482e36e825d5102a9f3010e729f33a4ca83fc8a1f439ba16eb61e1f1", size = 4019920, upload-time = "2025-10-18T22:45:05.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/c4/ac7f600ae5d8fb7a89c2712163b642d88739b3bb4c8d0fb3178c084dc521/psycopg_binary-3.2.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:749d23fbfd642a7abfef5fc0f6ca185fa82a2c0f895e6eab42c3f2a5d88f6011", size = 4092123, upload-time = "2025-10-18T22:45:09.763Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/aa/866c8b2c83490f0d55c4a27d16c0b733744faac442adf181eb59d8d48a3d/psycopg_binary-3.2.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58d8f9f80ae79ba7f2a0509424939236220d7d66a4f8256ae999b882cc58065b", size = 4626894, upload-time = "2025-10-18T22:45:13.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/a8/e7c1eba4ca230d510b76b3f8701321e0c21820953744db67ec7c8fb67537/psycopg_binary-3.2.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:eab6959fade522e586b8ec37d3fe337ce10861965edef3292f52e66e36dc375d", size = 4719913, upload-time = "2025-10-18T22:45:19.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/5f/de0dea38cef6e050ff8e9acd0f7c5d956251fcfece5360973329eb10b84b/psycopg_binary-3.2.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe5e3648e855df4fba1d70c18aef18c9880ea8d123fdfae754c18787c8cb37b3", size = 4411018, upload-time = "2025-10-18T22:45:24.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/bf/2bbefb24e491f2fa4a7c627d14680429ca33092176eadae88fab4fbce8c6/psycopg_binary-3.2.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:30e2c114d26554ae677088de5d4133cc112344d7a233200fdbf4a2ca5754c7ec", size = 3861940, upload-time = "2025-10-18T22:45:28.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/07/d68f78df7490fcd17eef7f138f96bf3398a961208262498cde7d30266481/psycopg_binary-3.2.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e3f5887019dfb094c60e7026968ca3a964ca16305807ba5e43f9a78483767d5f", size = 3534831, upload-time = "2025-10-18T22:45:32.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/18/fc5a881ca3d8b40b8e37a396bf14176b8439a7e4b1a29848af325009f955/psycopg_binary-3.2.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9b4b0fc4e774063ae64c92cc57e2b10160150de68c96d71743218159d953869d", size = 3583559, upload-time = "2025-10-18T22:45:36.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/98/c4418b609ffea80907861ddb01c043af860b179cb8fb41905ad2f0a4f400/psycopg_binary-3.2.11-cp312-cp312-win_amd64.whl", hash = "sha256:9bdc762600fcc8e4ad3224734a4e70cc226207fd8f2de47c36b115efeed01782", size = 2910294, upload-time = "2025-10-18T22:45:40.135Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/93/9cea78ed3b279909f0fd6c2badb24b2361b93c875d6a7c921e26f6254044/psycopg_binary-3.2.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47f6cf8a1d02d25238bdb8741ac641ff0ec22b1c6ff6a2acd057d0da5c712842", size = 4017939, upload-time = "2025-10-18T22:45:45.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/86/fc9925f500b2c140c0bb8c1f8fcd04f8c45c76d4852e87baf4c75182de8c/psycopg_binary-3.2.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91268f04380964a5e767f8102d05f1e23312ddbe848de1a9514b08b3fc57d354", size = 4090150, upload-time = "2025-10-18T22:45:50.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/10/752b698da1ca9e6c5f15d8798cb637c3615315fd2da17eee4a90cf20ee08/psycopg_binary-3.2.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:199f88a05dd22133eab2deb30348ef7a70c23d706c8e63fdc904234163c63517", size = 4625597, upload-time = "2025-10-18T22:45:54.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/9f/b578545c3c23484f4e234282d97ab24632a1d3cbfec64209786872e7cc8f/psycopg_binary-3.2.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7b3c5474dbad63bcccb8d14d4d4c7c19f1dc6f8e8c1914cbc771d261cf8eddca", size = 4720326, upload-time = "2025-10-18T22:45:59.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/3b/ba548d3fe65a7d4c96e568c2188e4b665802e3cba41664945ed95d16eae9/psycopg_binary-3.2.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:581358e770a4536e546841b78fd0fe318added4a82443bf22d0bbe3109cf9582", size = 4411647, upload-time = "2025-10-18T22:46:04.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/65/559ab485b198600e7ff70d70786ae5c89d63475ca01d43a7dda0d7c91386/psycopg_binary-3.2.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54a30f00a51b9043048b3e7ee806ffd31fc5fbd02a20f0e69d21306ff33dc473", size = 3863037, upload-time = "2025-10-18T22:46:08.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/29/05d0b48c8bef147e8216a36a1263a309a6240dcc09a56f5b8174fa6216d2/psycopg_binary-3.2.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2a438fad4cc081b018431fde0e791b6d50201526edf39522a85164f606c39ddb", size = 3536975, upload-time = "2025-10-18T22:46:12.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/75/304e133d3ab1a49602616192edb81f603ed574f79966449105f2e200999d/psycopg_binary-3.2.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f5e7415b5d0f58edf2708842c66605092df67f3821161d861b09695fc326c4de", size = 3586213, upload-time = "2025-10-18T22:46:19.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/10/c47cce42fa3c37d439e1400eaa5eeb2ce53dc3abc84d52c8a8a9e544d945/psycopg_binary-3.2.11-cp313-cp313-win_amd64.whl", hash = "sha256:6b9632c42f76d5349e7dd50025cff02688eb760b258e891ad2c6428e7e4917d5", size = 2912997, upload-time = "2025-10-18T22:46:24.978Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/13/728b4763ef76a688737acebfcb5ab8696b024adc49a69c86081392b0e5ba/psycopg_binary-3.2.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:260738ae222b41dbefd0d84cb2e150a112f90b41688630f57fdac487ab6d6f38", size = 4016962, upload-time = "2025-10-18T22:46:29.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/0f/6180149621a907c5b60a2fae87d6ee10cc13e8c9f58d8250c310634ced04/psycopg_binary-3.2.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c594c199869099c59c85b9f4423370b6212491fb929e7fcda0da1768761a2c2c", size = 4090614, upload-time = "2025-10-18T22:46:33.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/97/cce19bdef510b698c9036d5573b941b539ffcaa7602450da559c8a62e0c3/psycopg_binary-3.2.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5768a9e7d393b2edd3a28de5a6d5850d054a016ed711f7044a9072f19f5e50d5", size = 4629749, upload-time = "2025-10-18T22:46:37.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/9d/9bff18989fb2bf05d18c1431dd8bec4a1d90141beb11fc45d3269947ddf3/psycopg_binary-3.2.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:27eb6367350b75fef882c40cd6f748bfd976db2f8651f7511956f11efc15154f", size = 4724035, upload-time = "2025-10-18T22:46:42.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/e5/39b930323428596990367b7953197730213d3d9d07bcedcad1d026608178/psycopg_binary-3.2.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa2aa5094dc962967ca0978c035b3ef90329b802501ef12a088d3bac6a55598e", size = 4411419, upload-time = "2025-10-18T22:46:47.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/9c/97c25438d1e51ddc6a7f67990b4c59f94bc515114ada864804ccee27ef1b/psycopg_binary-3.2.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7744b4ed1f3b76fe37de7e9ef98014482fe74b6d3dfe1026cc4cfb4b4404e74f", size = 3867844, upload-time = "2025-10-18T22:46:53.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/51/8c1e291cf4aa9982666f71a886aa782d990aa16853a42de545a0a9a871ef/psycopg_binary-3.2.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5f6f948ff1cd252003ff534d7b50a2b25453b4212b283a7514ff8751bdb68c37", size = 3541539, upload-time = "2025-10-18T22:46:58.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/0a/e25edcdfa1111bfc5c95668b7469b5a957b40ce10cc81383688d65564826/psycopg_binary-3.2.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3bd2c8fb1dec6f93383fbaa561591fa3d676e079f9cb9889af17c3020a19715f", size = 3588090, upload-time = "2025-10-18T22:47:04.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/aa/f8c2f4b4c13d5680a20e5bfcd61f9e154bce26e7a2c70cb0abeade088d61/psycopg_binary-3.2.11-cp314-cp314-win_amd64.whl", hash = "sha256:c45f61202e5691090a697e599997eaffa3ec298209743caa4fd346145acabafe", size = 3006049, upload-time = "2025-10-18T22:47:07.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg-pool"
|
||||
version = "3.2.6"
|
||||
|
||||
Reference in new issue
Block a user