mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-19 22:25:44 +02:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
615c280b21 | ||
|
|
2180e0f80f | ||
|
|
4d983036a0 | ||
|
|
2923e670b9 |
@@ -3,8 +3,7 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
branches: [main, v1]
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -16,9 +16,6 @@ build-prebuilt:
|
||||
uv run python -m _scripts.third_party_page.create_third_party_page stats.yml docs/agents/prebuilt.md
|
||||
|
||||
build-docs: build-prebuilt
|
||||
TARGET_LANGUAGE=python uv run python -m mkdocs build --clean -f mkdocs.yml --strict
|
||||
|
||||
build-docs-js: build-prebuilt
|
||||
TARGET_LANGUAGE=js uv run python -m mkdocs build --clean -f mkdocs.yml --strict
|
||||
|
||||
llms-text:
|
||||
|
||||
+66
-50
@@ -4,48 +4,52 @@ This module provides link mappings for different language/framework scopes
|
||||
to resolve @[link_name] references to actual URLs.
|
||||
"""
|
||||
|
||||
# Python-specific link mappings
|
||||
# Python-specific link mappings
|
||||
PYTHON_LINK_MAP = {
|
||||
"StateGraph": "reference/graphs/#langgraph.graph.StateGraph",
|
||||
"add_conditional_edges": "reference/graphs/#langgraph.graph.state.StateGraph.add_conditional_edges",
|
||||
"add_edge": "reference/graphs/#langgraph.graph.state.StateGraph.add_edge",
|
||||
"add_node": "reference/graphs/#langgraph.graph.state.StateGraph.add_node",
|
||||
"add_messages": "reference/graphs/#langgraph.graph.message.add_messages",
|
||||
"ToolNode": "reference/agents/#langgraph.prebuilt.tool_node.ToolNode",
|
||||
"add_conditional_edges": "reference/graphs/#langgraph.graph.StateGraph.add_conditional_edges",
|
||||
"add_edge": "reference/graphs/#langgraph.graph.StateGraph.add_edge",
|
||||
"add_node": "reference/graphs/#langgraph.graph.StateGraph.add_node",
|
||||
"add_messages": "reference/messages/#langgraph.graph.message.add_messages",
|
||||
"ToolNode": "reference/prebuilt/#langgraph.prebuilt.tool_node.ToolNode",
|
||||
"CompiledStateGraph.astream": "reference/graphs/#langgraph.graph.state.CompiledStateGraph.astream",
|
||||
"Pregel.astream": "reference/pregel/#langgraph.pregel.Pregel.astream",
|
||||
"Pregel.astream": "reference/graphs/#langgraph.pregel.Pregel.astream",
|
||||
"AsyncPostgresSaver": "reference/checkpoints/#langgraph.checkpoint.postgres.aio.AsyncPostgresSaver",
|
||||
"AsyncSqliteSaver": "reference/checkpoints/#langgraph.checkpoint.sqlite.aio.AsyncSqliteSaver",
|
||||
"BaseCheckpointSaver": "reference/checkpoints/#langgraph.checkpoint.base.BaseCheckpointSaver",
|
||||
"BaseStore": "reference/store/#langgraph.store.base.BaseStore",
|
||||
"BaseStore.put": "reference/store/#langgraph.store.base.BaseStore.put",
|
||||
"BinaryOperatorAggregate": "reference/pregel/#langgraph.pregel.Pregel--advanced-channels-context-and-binaryoperatoraggregate",
|
||||
"BaseStore": "reference/stores/#langgraph.store.base.BaseStore",
|
||||
"BaseStore.put": "reference/stores/#langgraph.store.base.BaseStore.put",
|
||||
"BinaryOperatorAggregate": "reference/channels/#langgraph.channels.BinaryOperatorAggregate",
|
||||
"CipherProtocol": "reference/checkpoints/#langgraph.checkpoint.serde.base.CipherProtocol",
|
||||
"client.runs.stream": "cloud/reference/sdk/python_sdk_ref/#langgraph_sdk.client.RunsClient.stream",
|
||||
"client.runs.wait": "cloud/reference/sdk/python_sdk_ref/#langgraph_sdk.client.RunsClient.wait",
|
||||
"client.threads.get_history": "cloud/reference/sdk/python_sdk_ref/#langgraph_sdk.client.ThreadsClient.get_history",
|
||||
"client.threads.update_state": "cloud/reference/sdk/python_sdk_ref/#langgraph_sdk.client.ThreadsClient.update_state",
|
||||
"client.runs.stream": "reference/client/#langgraph_sdk.client.RunsClient.stream",
|
||||
"client.runs.wait": "reference/client/#langgraph_sdk.client.RunsClient.wait",
|
||||
"client.threads.get_history": "reference/client/#langgraph_sdk.client.ThreadsClient.get_history",
|
||||
"client.threads.update_state": "reference/client/#langgraph_sdk.client.ThreadsClient.update_state",
|
||||
"Command": "reference/types/#langgraph.types.Command",
|
||||
"CompiledStateGraph": "reference/graphs/#langgraph.graph.state.CompiledStateGraph",
|
||||
"create_react_agent": "reference/prebuilt/#langgraph.prebuilt.chat_agent_executor.create_react_agent",
|
||||
"create_supervisor": "reference/supervisor/#langgraph_supervisor.supervisor.create_supervisor",
|
||||
"EncryptedSerializer": "reference/checkpoints/#langgraph.checkpoint.serde.encrypted.EncryptedSerializer",
|
||||
"entrypoint.final": "reference/func/#langgraph.func.entrypoint.final",
|
||||
"entrypoint": "reference/func/#langgraph.func.entrypoint",
|
||||
"entrypoint.final": "reference/functions/#langgraph.func.entrypoint.final",
|
||||
"entrypoint": "reference/functions/#langgraph.func.entrypoint",
|
||||
"from_pycryptodome_aes": "reference/checkpoints/#langgraph.checkpoint.serde.encrypted.EncryptedSerializer.from_pycryptodome_aes",
|
||||
# "getContextVariable": "<insert-ref>",
|
||||
"get_state_history": "reference/graphs/#langgraph.graph.state.CompiledStateGraph.get_state_history",
|
||||
"get_stream_writer": "reference/config/#langgraph.config.get_stream_writer",
|
||||
"HumanInterrupt": "reference/prebuilt/#langgraph.prebuilt.interrupt.HumanInterrupt",
|
||||
"InjectedState": "reference/agents/#langgraph.prebuilt.tool_node.InjectedState",
|
||||
"InjectedState": "reference/prebuilt/#langgraph.prebuilt.InjectedState",
|
||||
"InMemorySaver": "reference/checkpoints/#langgraph.checkpoint.memory.InMemorySaver",
|
||||
"interrupt": "reference/types/#langgraph.types.Interrupt",
|
||||
"interrupt": "reference/graphs/#langgraph.graph.interrupt",
|
||||
"CompiledStateGraph.invoke": "reference/graphs/#langgraph.graph.state.CompiledStateGraph.invoke",
|
||||
"JsonPlusSerializer": "reference/checkpoints/#langgraph.checkpoint.serde.jsonplus.JsonPlusSerializer",
|
||||
"langgraph.json": "cloud/reference/cli/#configuration-file",
|
||||
"langgraph.json": "reference/configuration/#configuration-file",
|
||||
"LastValue": "reference/channels/#langgraph.channels.LastValue",
|
||||
# "MemorySaver": "<insert-ref>",
|
||||
# "messagesStateReducer": "<insert-ref>",
|
||||
"PostgresSaver": "reference/checkpoints/#langgraph.checkpoint.postgres.PostgresSaver",
|
||||
"Pregel": "reference/pregel/",
|
||||
"Pregel.stream": "reference/pregel/#langgraph.pregel.Pregel.stream",
|
||||
"Pregel": "reference/graphs/#langgraph.pregel.Pregel",
|
||||
"Pregel.stream": "reference/graphs/#langgraph.pregel.Pregel.stream",
|
||||
"pre_model_hook": "reference/prebuilt/#langgraph.prebuilt.chat_agent_executor.create_react_agent",
|
||||
"protocol": "reference/checkpoints/#langgraph.checkpoint.serde.base.SerializerProtocol",
|
||||
"Send": "reference/types/#langgraph.types.Send",
|
||||
@@ -53,56 +57,68 @@ PYTHON_LINK_MAP = {
|
||||
"SqliteSaver": "reference/checkpoints/#langgraph.checkpoint.sqlite.SqliteSaver",
|
||||
"START": "reference/constants/#langgraph.constants.START",
|
||||
"CompiledStateGraph.stream": "reference/graphs/#langgraph.graph.state.CompiledStateGraph.stream",
|
||||
"task": "reference/func/#langgraph.func.task",
|
||||
"task": "reference/functions/#langgraph.func.task",
|
||||
"Topic": "reference/channels/#langgraph.channels.Topic",
|
||||
"update_state": "reference/graphs/#langgraph.graph.state.CompiledStateGraph.update_state",
|
||||
}
|
||||
|
||||
|
||||
# JavaScript-specific link mappings
|
||||
JS_LINK_MAP = {
|
||||
"Auth": "reference/classes/sdk_auth.Auth.html",
|
||||
"StateGraph": "reference/classes/langgraph.StateGraph.html",
|
||||
"add_conditional_edges": "/reference/classes/langgraph.StateGraph.html#addConditionalEdges",
|
||||
"add_edge": "reference/classes/langgraph.StateGraph.html#addEdge",
|
||||
"add_node": "reference/classes/langgraph.StateGraph.html#addNode",
|
||||
"add_messages": "reference/modules/langgraph.html#addMessages",
|
||||
"add_conditional_edges": "reference/functions/langgraph_StateGraph.addConditionalEdges.html",
|
||||
"add_edge": "reference/functions/langgraph_StateGraph.addEdge.html",
|
||||
"add_node": "reference/functions/langgraph_StateGraph.addNode.html",
|
||||
"add_messages": "reference/functions/langgraph_message.addMessages.html",
|
||||
"ToolNode": "reference/classes/langgraph_prebuilt.ToolNode.html",
|
||||
"BaseCheckpointSaver": "reference/classes/checkpoint.BaseCheckpointSaver.html",
|
||||
"BaseStore": "reference/classes/checkpoint.BaseStore.html",
|
||||
"BaseStore.put": "reference/classes/checkpoint.BaseStore.html#put",
|
||||
"BinaryOperatorAggregate": "reference/classes/langgraph.BinaryOperatorAggregate.html",
|
||||
"client.runs.stream": "reference/classes/sdk_client.RunsClient.html#stream",
|
||||
"client.runs.wait": "reference/classes/sdk_client.RunsClient.html#wait",
|
||||
"client.threads.get_history": "reference/classes/sdk_client.ThreadsClient.html#getHistory",
|
||||
"client.threads.update_state": "reference/classes/sdk_client.ThreadsClient.html#updateState",
|
||||
"CompiledStateGraph.astream()": "reference/functions/langgraph_CompiledStateGraph.astream.html",
|
||||
"Pregel.astream": "reference/functions/langgraph_Pregel.astream.html",
|
||||
"AsyncPostgresSaver": "reference/classes/langgraph_checkpoint_postgres_aio.AsyncPostgresSaver.html",
|
||||
"AsyncSqliteSaver": "reference/classes/langgraph_checkpoint_sqlite_aio.AsyncSqliteSaver.html",
|
||||
"BaseCheckpointSaver": "reference/classes/langgraph_checkpoint_base.BaseCheckpointSaver.html",
|
||||
"BaseStore": "reference/classes/langgraph_store_base.BaseStore.html",
|
||||
"BaseStore.put": "reference/functions/langgraph_store_base.BaseStore.put.html",
|
||||
"BinaryOperatorAggregate": "reference/classes/langgraph_channels.BinaryOperatorAggregate.html",
|
||||
"CipherProtocol": "reference/classes/langgraph_checkpoint_serde_base.CipherProtocol.html",
|
||||
"client.runs.stream": "reference/functions/langgraph_sdk_client.RunsClient.stream.html",
|
||||
"client.runs.wait": "reference/functions/langgraph_sdk_client.RunsClient.wait.html",
|
||||
"client.threads.get_history": "reference/functions/langgraph_sdk_client.ThreadsClient.getHistory.html",
|
||||
"client.threads.update_state": "reference/functions/langgraph_sdk_client.ThreadsClient.updateState.html",
|
||||
"Command": "reference/classes/langgraph.Command.html",
|
||||
"CompiledStateGraph": "reference/classes/langgraph.CompiledStateGraph.html",
|
||||
"create_react_agent": "reference/functions/langgraph_prebuilt.createReactAgent.html",
|
||||
"create_supervisor": "reference/functions/langgraph_supervisor.createSupervisor.html",
|
||||
"entrypoint.final": "reference/functions/langgraph.entrypoint.html#final",
|
||||
"entrypoint": "reference/functions/langgraph.entrypoint.html",
|
||||
"EncryptedSerializer": "reference/classes/langgraph_checkpoint_serde_encrypted.EncryptedSerializer.html",
|
||||
"entrypoint.final": "reference/functions/langgraph_func.entrypoint.final.html",
|
||||
"entrypoint": "reference/functions/langgraph_func.entrypoint.html",
|
||||
"from_pycryptodome_aes": "reference/functions/langgraph_checkpoint_serde_encrypted.EncryptedSerializer.fromPycryptodomeAes.html",
|
||||
"getContextVariable": "https://v03.api.js.langchain.com/functions/_langchain_core.context.getContextVariable.html",
|
||||
"get_state_history": "reference/classes/langgraph.CompiledStateGraph.html#getStateHistory",
|
||||
"HumanInterrupt": "reference/interfaces/langgraph_prebuilt.HumanInterrupt.html",
|
||||
"get_state_history": "reference/functions/langgraph_CompiledStateGraph.getStateHistory.html",
|
||||
"get_stream_writer": "reference/functions/langgraph_config.getStreamWriter.html",
|
||||
"HumanInterrupt": "reference/classes/langgraph_prebuilt.HumanInterrupt.html",
|
||||
"InjectedState": "reference/classes/langgraph_prebuilt.InjectedState.html",
|
||||
"InMemorySaver": "reference/classes/langgraph_checkpoint_memory.InMemorySaver.html",
|
||||
"interrupt": "reference/functions/langgraph.interrupt-2.html",
|
||||
"CompiledStateGraph.invoke": "reference/classes/langgraph.CompiledStateGraph.html#invoke",
|
||||
"langgraph.json": "cloud/reference/cli/#configuration-file",
|
||||
"CompiledStateGraph.invoke": "reference/functions/langgraph_CompiledStateGraph.invoke.html",
|
||||
"JsonPlusSerializer": "reference/classes/langgraph_checkpoint_serde_jsonplus.JsonPlusSerializer.html",
|
||||
"langgraph.json": "reference/configuration.html",
|
||||
"LastValue": "reference/classes/langgraph_channels.LastValue.html",
|
||||
"MemorySaver": "reference/classes/checkpoint.MemorySaver.html",
|
||||
"messagesStateReducer": "reference/functions/langgraph.messagesStateReducer.html",
|
||||
"PostgresSaver": "reference/classes/checkpoint_postgres.PostgresSaver.html",
|
||||
"PostgresSaver": "reference/classes/langgraph_checkpoint_postgres.PostgresSaver.html",
|
||||
"Pregel": "reference/classes/langgraph.Pregel.html",
|
||||
"Pregel.stream": "reference/classes/langgraph.Pregel.html#stream",
|
||||
"Pregel.stream": "reference/functions/langgraph_Pregel.stream.html",
|
||||
"pre_model_hook": "reference/functions/langgraph_prebuilt.createReactAgent.html",
|
||||
"protocol": "reference/interfaces/checkpoint.SerializerProtocol.html",
|
||||
"protocol": "reference/classes/langgraph_checkpoint_serde_base.SerializerProtocol.html",
|
||||
"Send": "reference/classes/langgraph.Send.html",
|
||||
"SerializerProtocol": "reference/interfaces/checkpoint.SerializerProtocol.html",
|
||||
"SqliteSaver": "reference/classes/checkpoint_sqlite.SqliteSaver.html",
|
||||
"START": "reference/variables/langgraph.START.html",
|
||||
"CompiledStateGraph.stream": "reference/classes/langgraph.CompiledStateGraph.html#stream",
|
||||
"task": "reference/functions/langgraph.task.html",
|
||||
## TODO (hntrl): export Topic from langgraphjs
|
||||
# "Topic": "reference/classes/langgraph_channels.Topic.html",
|
||||
"update_state": "reference/classes/langgraph.CompiledStateGraph.html#updateState",
|
||||
"SerializerProtocol": "reference/classes/langgraph_checkpoint_serde_base.SerializerProtocol.html",
|
||||
"SqliteSaver": "reference/classes/langgraph_checkpoint_sqlite.SqliteSaver.html",
|
||||
"START": "reference/constants.html#START",
|
||||
"CompiledStateGraph.stream": "reference/functions/langgraph_CompiledStateGraph.stream.html",
|
||||
"task": "reference/functions/langgraph_func.task.html",
|
||||
"Topic": "reference/classes/langgraph_channels.Topic.html",
|
||||
"update_state": "reference/functions/langgraph_CompiledStateGraph.updateState.html",
|
||||
}
|
||||
|
||||
# TODO: Allow updating these to localhost for local development
|
||||
|
||||
+18
-132
@@ -88,12 +88,12 @@ REDIRECT_MAP = {
|
||||
"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/langgraph-platform/streaming",
|
||||
"cloud/how-tos/stream_updates.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||
"cloud/how-tos/stream_messages.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||
"cloud/how-tos/stream_events.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||
"cloud/how-tos/stream_debug.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||
"cloud/how-tos/stream_multiple.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||
"cloud/how-tos/stream_values.md": "cloud/how-tos/streaming.md#stream-graph-state",
|
||||
"cloud/how-tos/stream_updates.md": "cloud/how-tos/streaming.md#stream-graph-state",
|
||||
"cloud/how-tos/stream_messages.md": "cloud/how-tos/streaming.md#messages",
|
||||
"cloud/how-tos/stream_events.md": "cloud/how-tos/streaming.md#stream-events",
|
||||
"cloud/how-tos/stream_debug.md": "cloud/how-tos/streaming.md#debug",
|
||||
"cloud/how-tos/stream_multiple.md": "cloud/how-tos/streaming.md#stream-multiple-modes",
|
||||
"cloud/concepts/streaming.md": "concepts/streaming.md",
|
||||
"agents/streaming.md": "how-tos/streaming.md",
|
||||
# prebuilt redirects
|
||||
@@ -127,85 +127,6 @@ REDIRECT_MAP = {
|
||||
"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/langgraph-platform/auth",
|
||||
"tutorials/auth/resource_auth.md": "https://docs.langchain.com/langgraph-platform/resource-auth",
|
||||
"tutorials/auth/add_auth_server.md": "https://docs.langchain.com/langgraph-platform/add-auth-server",
|
||||
"how-tos/use-remote-graph.md": "https://docs.langchain.com/langgraph-platform/use-remote-graph",
|
||||
"how-tos/autogen-integration.md": "https://docs.langchain.com/langgraph-platform/autogen-integration",
|
||||
"cloud/how-tos/use_stream_react.md": "https://docs.langchain.com/langgraph-platform/use-stream-react",
|
||||
"cloud/how-tos/generative_ui_react.md": "https://docs.langchain.com/langgraph-platform/generative-ui-react",
|
||||
"concepts/langgraph_platform.md": "https://docs.langchain.com/langgraph-platform/index",
|
||||
"concepts/langgraph_components.md": "https://docs.langchain.com/langgraph-platform/components",
|
||||
"concepts/langgraph_server.md": "https://docs.langchain.com/langgraph-platform/langgraph-server",
|
||||
"concepts/langgraph_data_plane.md": "https://docs.langchain.com/langgraph-platform/data-plane",
|
||||
"concepts/langgraph_control_plane.md": "https://docs.langchain.com/langgraph-platform/control-plane",
|
||||
"concepts/langgraph_cli.md": "https://docs.langchain.com/langgraph-platform/langgraph-cli",
|
||||
"concepts/langgraph_studio.md": "https://docs.langchain.com/langgraph-platform/langgraph-studio",
|
||||
"cloud/how-tos/studio/quick_start.md": "https://docs.langchain.com/langgraph-platform/quick-start-studio",
|
||||
"cloud/how-tos/invoke_studio.md": "https://docs.langchain.com/langgraph-platform/invoke-studio",
|
||||
"cloud/how-tos/studio/manage_assistants.md": "https://docs.langchain.com/langgraph-platform/manage-assistants-studio",
|
||||
"cloud/how-tos/threads_studio.md": "https://docs.langchain.com/langgraph-platform/threads-studio",
|
||||
"cloud/how-tos/iterate_graph_studio.md": "https://docs.langchain.com/langgraph-platform/iterate-graph-studio",
|
||||
"cloud/how-tos/studio/run_evals.md": "https://docs.langchain.com/langgraph-platform/run-evals-studio",
|
||||
"cloud/how-tos/clone_traces_studio.md": "https://docs.langchain.com/langgraph-platform/clone-traces-studio",
|
||||
"cloud/how-tos/datasets_studio.md": "https://docs.langchain.com/langgraph-platform/datasets-studio",
|
||||
"concepts/sdk.md": "https://docs.langchain.com/langgraph-platform/sdk",
|
||||
"concepts/plans.md": "https://docs.langchain.com/langgraph-platform/plans",
|
||||
"concepts/application_structure.md": "https://docs.langchain.com/langgraph-platform/application-structure",
|
||||
"concepts/scalability_and_resilience.md": "https://docs.langchain.com/langgraph-platform/scalability-and-resilience",
|
||||
"concepts/auth.md": "https://docs.langchain.com/langgraph-platform/auth",
|
||||
"how-tos/auth/custom_auth.md": "https://docs.langchain.com/langgraph-platform/custom-auth",
|
||||
"how-tos/auth/openapi_security.md": "https://docs.langchain.com/langgraph-platform/openapi-security",
|
||||
"concepts/assistants.md": "https://docs.langchain.com/langgraph-platform/assistants",
|
||||
"cloud/how-tos/configuration_cloud.md": "https://docs.langchain.com/langgraph-platform/configuration-cloud",
|
||||
"cloud/how-tos/use_threads.md": "https://docs.langchain.com/langgraph-platform/use-threads",
|
||||
"cloud/how-tos/background_run.md": "https://docs.langchain.com/langgraph-platform/background-run",
|
||||
"cloud/how-tos/same-thread.md": "https://docs.langchain.com/langgraph-platform/same-thread",
|
||||
"cloud/how-tos/stateless_runs.md": "https://docs.langchain.com/langgraph-platform/stateless-runs",
|
||||
"cloud/how-tos/configurable_headers.md": "https://docs.langchain.com/langgraph-platform/configurable-headers",
|
||||
"concepts/double_texting.md": "https://docs.langchain.com/langgraph-platform/double-texting",
|
||||
"cloud/how-tos/interrupt_concurrent.md": "https://docs.langchain.com/langgraph-platform/interrupt-concurrent",
|
||||
"cloud/how-tos/rollback_concurrent.md": "https://docs.langchain.com/langgraph-platform/rollback-concurrent",
|
||||
"cloud/how-tos/reject_concurrent.md": "https://docs.langchain.com/langgraph-platform/reject-concurrent",
|
||||
"cloud/how-tos/enqueue_concurrent.md": "https://docs.langchain.com/langgraph-platform/enqueue-concurrent",
|
||||
"cloud/concepts/webhooks.md": "https://docs.langchain.com/langgraph-platform/use-webhooks",
|
||||
"cloud/how-tos/webhooks.md": "https://docs.langchain.com/langgraph-platform/use-webhooks",
|
||||
"cloud/concepts/cron_jobs.md": "https://docs.langchain.com/langgraph-platform/cron-jobs",
|
||||
"cloud/how-tos/cron_jobs.md": "https://docs.langchain.com/langgraph-platform/cron-jobs",
|
||||
"how-tos/http/custom_lifespan.md": "https://docs.langchain.com/langgraph-platform/custom-lifespan",
|
||||
"how-tos/http/custom_middleware.md": "https://docs.langchain.com/langgraph-platform/custom-middleware",
|
||||
"how-tos/http/custom_routes.md": "https://docs.langchain.com/langgraph-platform/custom-routes",
|
||||
"cloud/concepts/data_storage_and_privacy.md": "https://docs.langchain.com/langgraph-platform/data-storage-and-privacy",
|
||||
"cloud/deployment/semantic_search.md": "https://docs.langchain.com/langgraph-platform/semantic-search",
|
||||
"how-tos/ttl/configure_ttl.md": "https://docs.langchain.com/langgraph-platform/configure-ttl",
|
||||
"concepts/deployment_options.md": "https://docs.langchain.com/langgraph-platform/deployment-options",
|
||||
"cloud/quick_start.md": "https://docs.langchain.com/langgraph-platform/deployment-quickstart",
|
||||
"cloud/deployment/setup.md": "https://docs.langchain.com/langgraph-platform/setup-app-requirements-txt",
|
||||
"cloud/deployment/setup_pyproject.md": "https://docs.langchain.com/langgraph-platform/setup-pyproject",
|
||||
"cloud/deployment/setup_javascript.md": "https://docs.langchain.com/langgraph-platform/setup-javascript",
|
||||
"cloud/deployment/custom_docker.md": "https://docs.langchain.com/langgraph-platform/custom-docker",
|
||||
"cloud/deployment/graph_rebuild.md": "https://docs.langchain.com/langgraph-platform/graph-rebuild",
|
||||
"concepts/langgraph_cloud.md": "https://docs.langchain.com/langgraph-platform/cloud",
|
||||
"concepts/langgraph_self_hosted_data_plane.md": "https://docs.langchain.com/langgraph-platform/hybrid",
|
||||
"concepts/langgraph_self_hosted_control_plane.md": "https://docs.langchain.com/langgraph-platform/self-hosted",
|
||||
"concepts/langgraph_standalone_container.md": "https://docs.langchain.com/langgraph-platform/self-hosted#data-plane-only",
|
||||
"cloud/deployment/cloud.md": "https://docs.langchain.com/langgraph-platform/cloud",
|
||||
"cloud/deployment/self_hosted_data_plane.md": "https://docs.langchain.com/langgraph-platform/deploy-hybrid",
|
||||
"cloud/deployment/self_hosted_control_plane.md": "https://docs.langchain.com/langgraph-platform/deploy-self-hosted-full-platform",
|
||||
"cloud/deployment/standalone_container.md": "https://docs.langchain.com/langgraph-platform/deploy-data-plane-only",
|
||||
"concepts/server-mcp.md": "https://docs.langchain.com/langgraph-platform/server-mcp",
|
||||
"cloud/how-tos/human_in_the_loop_time_travel.md": "https://docs.langchain.com/langgraph-platform/human-in-the-loop-time-travel",
|
||||
"cloud/how-tos/add-human-in-the-loop.md": "https://docs.langchain.com/langgraph-platform/add-human-in-the-loop",
|
||||
"cloud/deployment/egress.md": "https://docs.langchain.com/langgraph-platform/env-var",
|
||||
"cloud/how-tos/streaming.md": "https://docs.langchain.com/langgraph-platform/streaming",
|
||||
"cloud/reference/api/api_ref.md": "https://docs.langchain.com/langgraph-platform/server-api-ref",
|
||||
"cloud/reference/langgraph_server_changelog.md": "https://docs.langchain.com/langgraph-platform/langgraph-server-changelog",
|
||||
"cloud/reference/api/api_ref_control_plane.md": "https://docs.langchain.com/langgraph-platform/api-ref-control-plane",
|
||||
"cloud/reference/cli.md": "https://docs.langchain.com/langgraph-platform/cli",
|
||||
"cloud/reference/env_var.md": "https://docs.langchain.com/langgraph-platform/env-var",
|
||||
"troubleshooting/studio.md": "https://docs.langchain.com/langgraph-platform/troubleshooting-studio",
|
||||
}
|
||||
|
||||
|
||||
@@ -561,51 +482,16 @@ def on_post_page(html: str, page: Page, config: MkDocsConfig) -> str:
|
||||
def on_post_build(config):
|
||||
use_directory_urls = config.get("use_directory_urls")
|
||||
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
|
||||
if page_old.endswith(".md"):
|
||||
old_html_path = page_old[:-3] + "/index.html"
|
||||
else:
|
||||
old_html_path = page_old + "/index.html"
|
||||
else:
|
||||
# Without directory URLs: /path/to/page.md becomes /path/to/page.html
|
||||
if page_old.endswith(".md"):
|
||||
old_html_path = page_old[:-3] + ".html"
|
||||
else:
|
||||
old_html_path = page_old + ".html"
|
||||
|
||||
if isinstance(page_new, str) and page_new.startswith("http"):
|
||||
# Handle external redirects
|
||||
_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
|
||||
new_html_path = (
|
||||
posixpath.relpath(new_html_path, start=posixpath.dirname(old_html_path))
|
||||
+ hash
|
||||
+ suffix
|
||||
)
|
||||
except:
|
||||
# Fallback: calculate relative path manually
|
||||
if use_directory_urls:
|
||||
if page_new_before_hash.endswith(".md"):
|
||||
new_html_path = page_new_before_hash[:-3] + "/"
|
||||
else:
|
||||
new_html_path = page_new_before_hash + "/"
|
||||
else:
|
||||
if page_new_before_hash.endswith(".md"):
|
||||
new_html_path = page_new_before_hash[:-3] + ".html"
|
||||
else:
|
||||
new_html_path = page_new_before_hash + ".html"
|
||||
new_html_path += hash + suffix
|
||||
|
||||
_write_html(config["site_dir"], old_html_path, new_html_path)
|
||||
page_new = page_new.replace(".ipynb", ".md")
|
||||
page_new_before_hash, hash, suffix = page_new.partition("#")
|
||||
old_html_path = File(page_old, "", "", use_directory_urls).dest_path.replace(
|
||||
os.sep, "/"
|
||||
)
|
||||
new_html_path = File(page_new_before_hash, "", "", True).url
|
||||
new_html_path = (
|
||||
posixpath.relpath(new_html_path, start=posixpath.dirname(old_html_path))
|
||||
+ hash
|
||||
+ suffix
|
||||
)
|
||||
_write_html(config["site_dir"], old_html_path, new_html_path)
|
||||
|
||||
@@ -29,7 +29,7 @@ pip install -U langgraph "langchain[anthropic]"
|
||||
|
||||
!!! info
|
||||
|
||||
`langchain[anthropic]` is installed so the agent can call the [model](https://python.langchain.com/docs/integrations/chat/).
|
||||
LangChain is installed so the agent can call the [model](https://python.langchain.com/docs/integrations/chat/).
|
||||
|
||||
:::
|
||||
|
||||
@@ -41,7 +41,7 @@ npm install @langchain/langgraph @langchain/core @langchain/anthropic
|
||||
|
||||
!!! info
|
||||
|
||||
`@langchain/core` `@langchain/anthropic` are installed so the agent can call the [model](https://js.langchain.com/docs/integrations/chat/).
|
||||
LangChain is installed so the agent can call the [model](https://js.langchain.com/docs/integrations/chat/).
|
||||
|
||||
:::
|
||||
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
**Context engineering** is the practice of building dynamic systems that provide the right information and tools, in the right format, so that an AI application can accomplish a task. Context can be characterized along two key dimensions:
|
||||
|
||||
|
||||
1. By **mutability**:
|
||||
- **Static context**: Immutable data that doesn't change during execution (e.g., user metadata, database connections, tools)
|
||||
- **Dynamic context**: Mutable data that evolves as the application runs (e.g., conversation history, intermediate results, tool call observations)
|
||||
- **Static context**: Immutable data that doesn't change during execution (e.g., user metadata, database connections, tools)
|
||||
- **Dynamic context**: Mutable data that evolves as the application runs (e.g., conversation history, intermediate results, tool call observations)
|
||||
2. By **lifetime**:
|
||||
- **Runtime context**: Data scoped to a single run or invocation
|
||||
- **Cross-conversation context**: Data that persists across multiple conversations or sessions
|
||||
- **Runtime context**: Data scoped to a single run or invocation
|
||||
- **Cross-conversation context**: Data that persists across multiple conversations or sessions
|
||||
|
||||
!!! tip "Runtime context vs LLM context"
|
||||
|
||||
@@ -125,7 +126,7 @@ graph.invoke( # (1)!
|
||||
|
||||
| Context type | Description | Mutability | Lifetime |
|
||||
| ------------------------------------------------------------------------------------------- | --------------------------------------------- | ---------- | ------------------ |
|
||||
| [**Config**](#config-static-context) | data passed at the start of a run | Static | Single run |
|
||||
| [**Config**](#config-static-context) | data passed at the start of a run | ❌ | per run |
|
||||
| [**Dynamic runtime context (state)**](#dynamic-runtime-context-state) | Mutable data that evolves during a single run | Dynamic | Single run |
|
||||
| [**Dynamic cross-conversation context (store)**](#dynamic-cross-conversation-context-store) | Persistent data shared across conversations | Dynamic | Cross-conversation |
|
||||
|
||||
|
||||
@@ -145,76 +145,6 @@ const agent = createReactAgent({
|
||||
|
||||
:::
|
||||
|
||||
:::python
|
||||
|
||||
### Dynamic model selection
|
||||
|
||||
Pass a callable function to `create_react_agent` to dynamically select the model at runtime. This is useful for scenarios where you want to choose a model based on user input, configuration settings, or other runtime conditions.
|
||||
|
||||
The selector function must return a chat model. If you're using tools, you must bind the tools to the model within the selector function.
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
from langchain.chat_models import init_chat_model
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
from langchain_core.tools import tool
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
from langgraph.prebuilt.chat_agent_executor import AgentState
|
||||
from langgraph.runtime import Runtime
|
||||
|
||||
@tool
|
||||
def weather() -> str:
|
||||
"""Returns the current weather conditions."""
|
||||
return "It's nice and sunny."
|
||||
|
||||
|
||||
# Define the runtime context
|
||||
@dataclass
|
||||
class CustomContext:
|
||||
provider: Literal["anthropic", "openai"]
|
||||
|
||||
# Initialize models
|
||||
openai_model = init_chat_model("openai:gpt-4o")
|
||||
anthropic_model = init_chat_model("anthropic:claude-sonnet-4-20250514")
|
||||
|
||||
|
||||
# Selector function for model choice
|
||||
def select_model(state: AgentState, runtime: Runtime[CustomContext]) -> BaseChatModel:
|
||||
if runtime.context.provider == "anthropic":
|
||||
model = anthropic_model
|
||||
elif runtime.context.provider == "openai":
|
||||
model = openai_model
|
||||
else:
|
||||
raise ValueError(f"Unsupported provider: {runtime.context.provider}")
|
||||
|
||||
# With dynamic model selection, you must bind tools explicitly
|
||||
return model.bind_tools([weather])
|
||||
|
||||
|
||||
# Create agent with dynamic model selection
|
||||
agent = create_react_agent(select_model, tools=[weather])
|
||||
|
||||
# Invoke with context to select model
|
||||
output = agent.invoke(
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Which model is handling this?",
|
||||
}
|
||||
]
|
||||
},
|
||||
context=CustomContext(provider="openai"),
|
||||
)
|
||||
|
||||
print(output["messages"][-1].text())
|
||||
```
|
||||
|
||||
!!! version-added "New in LangGraph v0.6"
|
||||
|
||||
:::
|
||||
|
||||
## Advanced model configuration
|
||||
|
||||
### Disable streaming
|
||||
|
||||
@@ -367,13 +367,13 @@ To implement handoffs with `createReactAgent`, you need to:
|
||||
|
||||
3. Define a parent graph that contains individual agents as nodes:
|
||||
|
||||
```typescript
|
||||
import { StateGraph, MessagesZodState } from "@langchain/langgraph";
|
||||
const multiAgentGraph = new StateGraph(MessagesZodState)
|
||||
.addNode("flight_assistant", flightAssistant)
|
||||
.addNode("hotel_assistant", hotelAssistant)
|
||||
// ...
|
||||
```
|
||||
```typescript
|
||||
import { StateGraph, MessagesZodState } from "@langchain/langgraph";
|
||||
const multiAgentGraph = new StateGraph(MessagesZodState)
|
||||
.addNode("flight_assistant", flightAssistant)
|
||||
.addNode("hotel_assistant", hotelAssistant)
|
||||
// ...
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
@@ -619,8 +619,7 @@ for await (const chunk of multiAgentGraph.stream({
|
||||
3. Name of the agent or node to hand off to.
|
||||
4. Take the agent's messages and **add** them to the parent's **state** as part of the handoff. The next agent will see the parent state.
|
||||
5. Indicate to LangGraph that we need to navigate to agent node in a **parent** multi-agent graph.
|
||||
|
||||
:::
|
||||
:::
|
||||
|
||||
!!! Note
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ function generateCodeSnippet({ tools, pre, post, response }) {
|
||||
if (post) lines.push(" post_model_hook=post_model_hook,");
|
||||
if (response) lines.push(" response_format=ResponseFormat,");
|
||||
|
||||
lines.push(")", "", "# Visualize the graph", "# For Jupyter or GUI environments:", "agent.get_graph().draw_mermaid_png()", "", "# To save PNG to file:", "png_data = agent.get_graph().draw_mermaid_png()", "with open(\"graph.png\", \"wb\") as f:", " f.write(png_data)", "", "# For terminal/ASCII output:", "agent.get_graph().draw_ascii()");
|
||||
lines.push(")", "", "agent.get_graph().draw_mermaid_png()");
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ Before deploying, review the [conceptual guide for the Standalone Container](../
|
||||
|
||||
`<database_name_1>` and `database_name_2` are different databases within the same instance, but `<hostname_1>` is shared. **The same database cannot be used for separate deployments**.
|
||||
|
||||
1. `LANGSMITH_API_KEY`: (if using [Lite](../../concepts/langgraph_server.md#server-versions)) LangSmith API key. This will be used to authenticate ONCE at server start up.
|
||||
1. `LANGGRAPH_CLOUD_LICENSE_KEY`: (if using [Enterprise](../../concepts/langgraph_data_plane.md#licensing)) LangGraph Platform license key. This will be used to authenticate ONCE at server start up.
|
||||
1. `LANGSMITH_ENDPOINT`: To send traces to a [self-hosted LangSmith](https://docs.smith.langchain.com/self_hosting) instance, set `LANGSMITH_ENDPOINT` to the hostname of the self-hosted LangSmith instance.
|
||||
1. Egress to `https://beacon.langchain.com` from your network. This is required for license verification and usage reporting if not running in air-gapped mode. See the [Egress documentation](../../cloud/deployment/egress.md) for more details.
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
# LangGraph Server Changelog
|
||||
|
||||
> **Note:** This changelog is no longer actively maintained. For the most up-to-date LangGraph Server changelog, please visit our new documentation site: [LangGraph Server Changelog](https://docs.langchain.com/langgraph-platform/langgraph-server-changelog#langgraph-server-changelog)
|
||||
|
||||
[LangGraph Server](../../concepts/langgraph_server.md) is an API platform for creating and managing agent-based applications. It provides built-in persistence, a task queue, and supports deploying, configuring, and running assistants (agentic workflows) at scale. This changelog documents all notable updates, features, and fixes to LangGraph Server releases.
|
||||
|
||||
---
|
||||
|
||||
## v0.2.111 (2025-07-29)
|
||||
- Started the heartbeat immediately upon connection to prevent JS graph streaming errors during long startups.
|
||||
|
||||
## v0.2.110 (2025-07-29)
|
||||
- Added interrupts as default values for all operations except streams to maintain consistent behavior.
|
||||
|
||||
## v0.2.109 (2025-07-28)
|
||||
- Fixed an issue where missing config schema occurred when `config_type` was not set.
|
||||
|
||||
|
||||
@@ -35,8 +35,7 @@ LangGraph Platform provides different security defaults:
|
||||
- Can be customized with your auth handler
|
||||
|
||||
!!! note "Custom auth"
|
||||
|
||||
Custom auth **is supported** for all plans in LangGraph Platform.
|
||||
Custom auth **is supported** for all plans in LangGraph Platform.
|
||||
|
||||
### Self-Hosted
|
||||
|
||||
@@ -44,6 +43,11 @@ LangGraph Platform provides different security defaults:
|
||||
- Complete flexibility to implement your security model
|
||||
- You control all aspects of authentication and authorization
|
||||
|
||||
!!! note "Custom auth"
|
||||
|
||||
Custom auth is supported for **Enterprise** self-hosted deployments.
|
||||
Standalone Container (Lite) deployments do not support custom auth natively.
|
||||
|
||||
## System Architecture
|
||||
|
||||
A typical authentication setup involves three main components:
|
||||
|
||||
@@ -7,7 +7,10 @@ search:
|
||||
|
||||
## Free deployment
|
||||
|
||||
[Local](../tutorials/langgraph-platform/local-server.md): Deploy for local testing and development.
|
||||
There are two free options for deploying LangGraph applications via the LangGraph Server:
|
||||
|
||||
1. [Local](../tutorials/langgraph-platform/local-server.md): Deploy for local testing and development.
|
||||
1. [Standalone Container (Lite)](../concepts/langgraph_standalone_container.md): A limited version of Standalone Container for deployments unlikely to see more than 1 million node executions per year and that do not need crons and other enterprise features. Standalone Container (Lite) deployment option is free with a LangSmith API key.
|
||||
|
||||
## Production deployment
|
||||
|
||||
@@ -30,7 +33,8 @@ A quick comparison:
|
||||
| **CI/CD** | Managed internally by platform | Managed externally by you | Managed externally by you | Managed externally by you |
|
||||
| **Data/compute residency** | LangChain's cloud | Your cloud | Your cloud | Your cloud |
|
||||
| **LangSmith compatibility** | Trace to LangSmith SaaS | Trace to LangSmith SaaS | Trace to Self-Hosted LangSmith | Optional tracing |
|
||||
| **[Pricing](https://www.langchain.com/pricing-langgraph-platform)** | Plus | Enterprise | Enterprise | Enterprise |
|
||||
| **[Server version compatibility](../concepts/langgraph_server.md#server-versions)** | Enterprise | Enterprise | Enterprise | Lite, Enterprise |
|
||||
| **[Pricing](https://www.langchain.com/pricing-langgraph-platform)** | Plus | Enterprise | Enterprise | Developer |
|
||||
|
||||
## Cloud SaaS
|
||||
|
||||
|
||||
@@ -51,51 +51,6 @@ For some examples of pitfalls to avoid, see the [Common Pitfalls](./functional_a
|
||||
how to structure your code using **tasks** to avoid these issues. The same principles apply to the @[StateGraph (Graph API)][StateGraph].
|
||||
:::
|
||||
|
||||
## Durability modes
|
||||
|
||||
LangGraph supports three durability modes that allow you to balance performance and data consistency based on your application's requirements. The durability modes, from least to most durable, are as follows:
|
||||
|
||||
- [`"exit"`](#exit)
|
||||
- [`"async"`](#async)
|
||||
- [`"sync"`](#sync)
|
||||
|
||||
A higher durability mode add more overhead to the workflow execution.
|
||||
|
||||
!!! version-added "Added in v0.6.0"
|
||||
|
||||
Use the `durability` parameter instead of `checkpoint_during` (deprecated in v0.6.0) for persistence policy management:
|
||||
|
||||
* `durability="async"` replaces `checkpoint_during=True`
|
||||
* `durability="exit"` replaces `checkpoint_during=False`
|
||||
|
||||
for persistence policy management, with the following mapping:
|
||||
|
||||
* `checkpoint_during=True` -> `durability="async"`
|
||||
* `checkpoint_during=False` -> `durability="exit"`
|
||||
|
||||
|
||||
### `"exit"`
|
||||
Changes are persisted only when graph execution completes (either successfully or with an error). This provides the best performance for long-running graphs but means intermediate state is not saved, so you cannot recover from mid-execution failures or interrupt the graph execution.
|
||||
|
||||
### `"async"`
|
||||
Changes are persisted asynchronously while the next step executes. This provides good performance and durability, but there's a small risk that checkpoints might not be written if the process crashes during execution.
|
||||
|
||||
### `"sync"`
|
||||
Changes are persisted synchronously before the next step starts. This ensures that every checkpoint is written before continuing execution, providing high durability at the cost of some performance overhead.
|
||||
|
||||
You can specify the durability mode when calling any graph execution method:
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
graph.stream(
|
||||
{"input": "test"},
|
||||
durability="sync"
|
||||
)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Using tasks in nodes
|
||||
|
||||
If a [node](./low_level.md#nodes) contains multiple operations, you may find it easier to convert each operation into a **task** rather than refactor the operations into individual nodes.
|
||||
|
||||
@@ -13,6 +13,21 @@ Use LangGraph Server to create and manage [assistants](assistants.md), [threads]
|
||||
|
||||
For detailed information on the API endpoints and data models, see [LangGraph Platform API reference docs](../cloud/reference/api/api_ref.html).
|
||||
|
||||
## Server versions
|
||||
|
||||
There are two versions of LangGraph Server:
|
||||
|
||||
- `Lite` is a limited version of the LangGraph Server that you can run locally or in a self-hosted manner (up to 1 million [nodes executed](../concepts/faq.md#what-does-nodes-executed-mean-for-langgraph-platform-usage) per year).
|
||||
- `Enterprise` is the full version of the LangGraph Server. To use the `Enterprise` version, you must acquire a license key that you will need to specify when running the Docker image. To acquire a license key, please email sales@langchain.dev.
|
||||
|
||||
Feature Differences:
|
||||
|
||||
| | Lite | Enterprise |
|
||||
|-------|------------|------------|
|
||||
| [Cron Jobs](../cloud/concepts/cron_jobs.md) |❌|✅|
|
||||
| [Custom Authentication](../concepts/auth.md) |❌|✅|
|
||||
| [Deployment options](../concepts/deployment_options.md) | Standalone container | Cloud SaaS, Self-Hosted Data Plane, Self-Hosted Control Plane, Standalone container
|
||||
|
||||
## Application structure
|
||||
|
||||
To deploy a LangGraph Server application, you need to specify the graph(s) you want to deploy, as well as any relevant configuration settings, such as dependencies and environment variables.
|
||||
|
||||
@@ -34,3 +34,12 @@ The Standalone Container deployment option supports deploying data plane infrast
|
||||
### Docker
|
||||
|
||||
The Standalone Container deployment option supports deploying data plane infrastructure to any Docker-supported compute platform.
|
||||
|
||||
## Lite vs. Enterprise
|
||||
|
||||
The Standalone Container deployment option supports both of the [server versions](../concepts/langgraph_server.md#langgraph-server):
|
||||
|
||||
- The `Lite` version is free, but has limited features.
|
||||
- The `Enterprise` version has custom pricing and is fully featured.
|
||||
|
||||
For more details on feature difference, see [LangGraph Server](../concepts/langgraph_server.md#server-versions).
|
||||
|
||||
@@ -88,6 +88,8 @@ Typically, all graph nodes communicate with a single schema. This means that the
|
||||
|
||||
It is possible to have nodes write to private state channels inside the graph for internal node communication. We can simply define a private schema, `PrivateState`.
|
||||
|
||||
See [this guide](../how-tos/graph-api.ipynb#pass-private-state-between-nodes) for more detail.
|
||||
|
||||
It is also possible to define explicit input and output schemas for a graph. In these cases, we define an "internal" schema that contains _all_ keys relevant to graph operations. But, we also define `input` and `output` schemas that are sub-sets of the "internal" schema to constrain the input and output of the graph. See [this guide](../how-tos/graph-api.md#define-input-and-output-schemas) for more detail.
|
||||
|
||||
Let's look at an example:
|
||||
@@ -471,7 +473,7 @@ const builder = new StateGraph(State);
|
||||
|
||||
:::
|
||||
|
||||
Behind the scenes, functions are converted to [RunnableLambda](https://python.langchain.com/api_reference/core/runnables/langchain_core.runnables.base.RunnableLambda.html)s, which add batch and async support to your function, along with native tracing and debugging.
|
||||
Behind the scenes, functions are converted to [RunnableLambda](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.RunnableLambda.html#langchain_core.runnables.base.RunnableLambda)s, which add batch and async support to your function, along with native tracing and debugging.
|
||||
|
||||
If you add a node to a graph without specifying a name, it will be given a default name equivalent to the function name.
|
||||
|
||||
@@ -699,8 +701,7 @@ graph.addConditionalEdges("nodeA", routingFunction, {
|
||||
:::
|
||||
|
||||
!!! tip
|
||||
|
||||
Use [`Command`](#command) instead of conditional edges if you want to combine state updates and routing in a single function.
|
||||
Use [`Command`](#command) instead of conditional edges if you want to combine state updates and routing in a single function.
|
||||
|
||||
### Entry Point
|
||||
|
||||
@@ -819,6 +820,7 @@ def my_node(state: State) -> Command[Literal["my_other_node"]]:
|
||||
return Command(update={"foo": "baz"}, goto="my_other_node")
|
||||
```
|
||||
|
||||
Check out this [how-to guide](../how-tos/graph-api.ipynb#combine-control-flow-and-state-updates-with-command) for an end-to-end example of how to use `Command`.
|
||||
:::
|
||||
|
||||
:::js
|
||||
@@ -858,6 +860,7 @@ builder.addNode("myNode", myNode, {
|
||||
});
|
||||
```
|
||||
|
||||
Check out this [how-to guide](../how-tos/graph-api.ipynb#combine-control-flow-and-state-updates-with-command) for an end-to-end example of how to use `Command`.
|
||||
:::
|
||||
|
||||
!!! important
|
||||
|
||||
@@ -6,14 +6,52 @@
|
||||
|
||||
Install the `langchain-mcp-adapters` library to use MCP tools in LangGraph:
|
||||
|
||||
:::python
|
||||
```bash
|
||||
pip install langchain-mcp-adapters
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```bash
|
||||
npm install @langchain/mcp-adapters
|
||||
## Authenticate to an MCP server
|
||||
|
||||
You can set up [custom authentication middleware](../how-tos/auth/custom_auth.md) to authenticate a user with an MCP server to get access to user-scoped tools within your LangGraph Platform deployment.
|
||||
|
||||
!!! note
|
||||
|
||||
Custom authentication is a LangGraph Platform feature.
|
||||
|
||||
An example architecture for this flow:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
%% Actors
|
||||
participant ClientApp as Client
|
||||
participant AuthProv as Auth Provider
|
||||
participant LangGraph as LangGraph Backend
|
||||
participant SecretStore as Secret Store
|
||||
participant MCPServer as MCP Server
|
||||
|
||||
%% Platform login / AuthN
|
||||
ClientApp ->> AuthProv: 1. Login (username / password)
|
||||
AuthProv -->> ClientApp: 2. Return token
|
||||
ClientApp ->> LangGraph: 3. Request with token
|
||||
|
||||
Note over LangGraph: 4. Validate token (@auth.authenticate)
|
||||
LangGraph -->> AuthProv: 5. Fetch user info
|
||||
AuthProv -->> LangGraph: 6. Confirm validity
|
||||
|
||||
%% Fetch user tokens from secret store
|
||||
LangGraph ->> SecretStore: 6a. Fetch user tokens
|
||||
SecretStore -->> LangGraph: 6b. Return tokens
|
||||
|
||||
Note over LangGraph: 7. Apply access control (@auth.on.*)
|
||||
|
||||
%% MCP round-trip
|
||||
Note over LangGraph: 8. Build MCP client with user token
|
||||
LangGraph ->> MCPServer: 9. Call MCP tool (with header)
|
||||
Note over MCPServer: 10. MCP validates header and runs tool
|
||||
MCPServer -->> LangGraph: 11. Tool response
|
||||
|
||||
%% Return to caller
|
||||
LangGraph -->> ClientApp: 12. Return resources / tool output
|
||||
```
|
||||
:::
|
||||
|
||||
For more information, see [MCP endpoint in LangGraph Server](../concepts/server-mcp.md).
|
||||
|
||||
@@ -10,7 +10,7 @@ search:
|
||||
LangGraph Platform is a solution for deploying agentic applications in production.
|
||||
There are three different plans for using it.
|
||||
|
||||
- **Developer**: All [LangSmith](https://smith.langchain.com/) users have access to this plan. You can sign up for this plan simply by creating a LangSmith account. This gives you access to the [local deployment](./deployment_options.md#free-deployment) option.
|
||||
- **Developer**: All [LangSmith](https://smith.langchain.com/) users have access to this plan. You can sign up for this plan simply by creating a LangSmith account. This gives you access to the [Standalone Container (Lite)](./deployment_options.md) deployment option.
|
||||
- **Plus**: All [LangSmith](https://smith.langchain.com/) users with a [Plus account](https://docs.smith.langchain.com/administration/pricing) have access to this plan. You can sign up for this plan simply by upgrading your LangSmith account to the Plus plan type. This gives you access to the [Cloud](./deployment_options.md#cloud-saas) deployment option.
|
||||
- **Enterprise**: This is separate from LangSmith plans. You can sign up for this plan by contacting sales@langchain.dev. This gives you access to all [deployment options](./deployment_options.md).
|
||||
|
||||
@@ -19,8 +19,8 @@ There are three different plans for using it.
|
||||
|
||||
| | Developer | Plus | Enterprise |
|
||||
|------------------------------------------------------------------|---------------------------------------------|-------------------------------------------------------|-----------------------------------------------------|
|
||||
| Deployment Options | Local | Cloud SaaS | <ul><li>Cloud SaaS</li><li>Self-Hosted Data Plane</li><li>Self-Hosted Control Plane</li><li>Standalone Container</li></ul> |
|
||||
| Usage | Free | See [Pricing](https://www.langchain.com/langgraph-platform-pricing) | Custom |
|
||||
| Deployment Options | Standalone Container (Lite) | Cloud SaaS | <ul><li>Cloud SaaS</li><li>Self-Hosted Data Plane</li><li>Self-Hosted Control Plane</li><li>Standalone Container (Enterprise)</li></ul> |
|
||||
| Usage | Free, limited to 1M [nodes executed](../concepts/faq.md#what-does-nodes-executed-mean-for-langgraph-platform-usage) per year | See [Pricing](https://www.langchain.com/langgraph-platform-pricing) | Custom |
|
||||
| APIs for retrieving and updating state and conversational history | ✅ | ✅ | ✅ |
|
||||
| APIs for retrieving and updating long-term memory | ✅ | ✅ | ✅ |
|
||||
| Horizontally scalable task queues and servers | ✅ | ✅ | ✅ |
|
||||
|
||||
@@ -9,4 +9,15 @@ The pages in this section provide end-to-end examples for the following topics:
|
||||
- [Agent Supervisor](../tutorials/multi_agent/agent_supervisor.md): Build a supervisor agent that can manage a team of agents.
|
||||
- [SQL agent](../tutorials/sql/sql-agent.md): Build a SQL agent that can execute SQL queries and return the results.
|
||||
- [Prebuilt chat UI](../agents/ui.md): Use a prebuilt chat UI to interact with any LangGraph agent.
|
||||
- [Graph runs in LangSmith](../how-tos/run-id-langsmith.md): Use LangSmith to track and analyze graph runs.
|
||||
- [Graph runs in LangSmith](../how-tos/run-id-langsmith.md): Use LangSmith to track and analyze graph runs.
|
||||
|
||||
## LangGraph Platform
|
||||
|
||||
- [Set up custom authentication](../tutorials/auth/getting_started.md): Set up custom authentication for your LangGraph application.
|
||||
- [Make conversations private](../tutorials/auth/resource_auth.md): Make conversations private by using resource-based authentication.
|
||||
- [Connect an authentication provider](../tutorials/auth/add_auth_server.md): Connect an authentication provider to your LangGraph application.
|
||||
- [Rebuild graph at runtime](../cloud/deployment/graph_rebuild.md): Rebuild a graph at runtime.
|
||||
- [Use RemoteGraph](../how-tos/use-remote-graph.md): Use RemoteGraph to deploy your LangGraph application to a remote server.
|
||||
- [Deploy CrewAI, AutoGen, and other frameworks](../how-tos/autogen-integration.md): Deploy CrewAI, AutoGen, and other frameworks with LangGraph.
|
||||
- [Integrate LangGraph into a React app](../cloud/how-tos/use_stream_react.md)
|
||||
- [Implement Generative User Interfaces with LangGraph](../cloud/how-tos/generative_ui_react.md)
|
||||
@@ -31,3 +31,15 @@ These capabilities are available in both LangGraph OSS and the LangGraph Platfor
|
||||
- [MCP](../concepts/mcp.md): Use MCP servers in a LangGraph graph.
|
||||
- [Evaluation](../agents/evals.md): Use LangSmith to evaluate your graph's performance.
|
||||
|
||||
## Platform-only capabilities
|
||||
|
||||
These capabilities are only available in [LangGraph Platform](../concepts/langgraph_platform.md).
|
||||
|
||||
- [Authentication and access control](../concepts/auth.md): Authenticate and authorize users to access a LangGraph graph.
|
||||
- [Assistants](../concepts/assistants.md): Build assistants that can be used to interact with a LangGraph graph.
|
||||
- [Double-texting](../concepts/double_texting.md): Handle double-texting (consecutive messages before a first response is returned) in a LangGraph graph.
|
||||
- [Webhooks](../cloud/concepts/webhooks.md): Send webhooks to a LangGraph graph.
|
||||
- [Cron jobs](../cloud/concepts/cron_jobs.md): Schedule jobs to run at a specific time.
|
||||
- [Server customization](../how-tos/http/custom_lifespan.md): Customize the server that runs a LangGraph graph.
|
||||
- [Data management](../cloud/concepts/data_storage_and_privacy.md): Manage data in a LangGraph graph.
|
||||
- [Deployment](../concepts/deployment_options.md): Deploy a LangGraph graph to a server.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 7.2 KiB |
@@ -11,13 +11,13 @@
|
||||
|
||||
???+ note "Support by deployment type"
|
||||
|
||||
Custom auth is supported for all deployments in the **managed LangGraph Platform**, as well as **Enterprise** self-hosted plans.
|
||||
Custom auth is supported for all deployments in the **managed LangGraph Platform**, as well as **Enterprise** self-hosted plans. It is not supported for **Lite** self-hosted plans.
|
||||
|
||||
This guide shows how to add custom authentication to your LangGraph Platform application. This guide applies to both LangGraph Platform and self-hosted deployments. It does not apply to isolated usage of the LangGraph open source library in your own custom server.
|
||||
|
||||
!!! note
|
||||
|
||||
Custom auth is supported for all **managed LangGraph Platform** deployments, as well as **Enterprise** self-hosted plans.
|
||||
Custom auth is supported for all **managed LangGraph Platform** deployments, as well as **Enterprise** self-hosted plans. It is not supported for **Lite** self-hosted plans.
|
||||
|
||||
## Add custom authentication to your deployment
|
||||
|
||||
@@ -145,7 +145,7 @@ def my_node(state, config):
|
||||
By default, if you add custom authorization on your resources, this will also apply to interactions made from the Studio. If you want, you can handle logged-in Studio users differently by checking [is_studio_user()](../../reference/functions/sdk_auth.isStudioUser.html).
|
||||
|
||||
!!! note
|
||||
`is_studio_user` was added in version 0.1.73 of the langgraph-sdk. If you're on an older version, you can still check whether `isinstance(ctx.user, StudioUser)`.
|
||||
`is_studio_user` was added in version 0.1.73 of the langgraph-sdk. If you're on an older version, you can still check whether `isinstance(ctx.user, StudioUser)`.
|
||||
|
||||
```python
|
||||
from langgraph_sdk.auth import is_studio_user, Auth
|
||||
|
||||
+28
-1452
File diff suppressed because it is too large
Load Diff
@@ -1436,7 +1436,6 @@ await agent.invoke(
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.config import get_store
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
|
||||
@@ -2798,6 +2797,4 @@ await checkpointer.deleteThread(threadId);
|
||||
## Prebuilt memory tools
|
||||
|
||||
**LangMem** is a LangChain-maintained library that offers tools for managing long-term memories in your agent. See the [LangMem documentation](https://langchain-ai.github.io/langmem/) for usage examples.
|
||||
|
||||
:::
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ To set up communication between the agents in a multi-agent system you can use [
|
||||
|
||||
To implement handoffs, you can return `Command` objects from your agent nodes or tools:
|
||||
|
||||
:::python
|
||||
```python
|
||||
from typing import Annotated
|
||||
from langchain_core.tools import tool, InjectedToolCallId
|
||||
@@ -58,7 +57,7 @@ def create_handoff_tool(*, agent_name: str, description: str | None = None):
|
||||
return handoff_tool
|
||||
```
|
||||
|
||||
1. Access the [state](../concepts/low_level.md#state) of the agent that is calling the handoff tool using the @[InjectedState] annotation.
|
||||
1. Access the [state](../concepts/low_level.md#state) of the agent that is calling the handoff tool using the @[InjectedState][InjectedState] annotation.
|
||||
2. The `Command` primitive allows specifying a state update and a node transition as a single operation, making it useful for implementing handoffs.
|
||||
3. Name of the agent or node to hand off to.
|
||||
4. Take the agent's messages and **add** them to the parent's **state** as part of the handoff. The next agent will see the parent state.
|
||||
@@ -74,109 +73,25 @@ def create_handoff_tool(*, agent_name: str, description: str | None = None):
|
||||
commands = [tools_by_name[tool_call["name"]].invoke(tool_call) for tool_call in tool_calls]
|
||||
return commands
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { Command, MessagesZodState } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
function createHandoffTool({
|
||||
agentName,
|
||||
description,
|
||||
}: {
|
||||
agentName: string;
|
||||
description?: string;
|
||||
}) {
|
||||
const name = `transfer_to_${agentName}`;
|
||||
const toolDescription = description || `Transfer to ${agentName}`;
|
||||
|
||||
return tool(
|
||||
async (_, config) => {
|
||||
// (1)!
|
||||
const state = config.state;
|
||||
const toolCallId = config.toolCall.id;
|
||||
|
||||
const toolMessage = {
|
||||
role: "tool" as const,
|
||||
content: `Successfully transferred to ${agentName}`,
|
||||
name: name,
|
||||
tool_call_id: toolCallId,
|
||||
};
|
||||
|
||||
return new Command({
|
||||
// (3)!
|
||||
goto: agentName,
|
||||
// (4)!
|
||||
update: { messages: [...state.messages, toolMessage] },
|
||||
// (5)!
|
||||
graph: Command.PARENT,
|
||||
});
|
||||
},
|
||||
{
|
||||
name,
|
||||
description: toolDescription,
|
||||
schema: z.object({}),
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
1. Access the [state](../concepts/low_level.md#state) of the agent that is calling the handoff tool through the `config` parameter.
|
||||
2. The `Command` primitive allows specifying a state update and a node transition as a single operation, making it useful for implementing handoffs.
|
||||
3. Name of the agent or node to hand off to.
|
||||
4. Take the agent's messages and **add** them to the parent's **state** as part of the handoff. The next agent will see the parent state.
|
||||
5. Indicate to LangGraph that we need to navigate to agent node in a **parent** multi-agent graph.
|
||||
|
||||
!!! tip
|
||||
|
||||
If you want to use tools that return `Command`, you can either use prebuilt @[`create_react_agent`][create_react_agent] / @[`ToolNode`][ToolNode] components, or implement your own tool-executing node that collects `Command` objects returned by the tools and returns a list of them, e.g.:
|
||||
|
||||
```typescript
|
||||
const callTools = async (state) => {
|
||||
// ...
|
||||
const commands = await Promise.all(
|
||||
toolCalls.map(toolCall => toolsByName[toolCall.name].invoke(toolCall))
|
||||
);
|
||||
return commands;
|
||||
};
|
||||
```
|
||||
:::
|
||||
|
||||
!!! Important
|
||||
|
||||
This handoff implementation assumes that:
|
||||
|
||||
- each agent receives overall message history (across all agents) in the multi-agent system as its input. If you want more control over agent inputs, see [this section](#control-agent-inputs)
|
||||
- each agent outputs its internal messages history to the overall message history of the multi-agent system. If you want more control over **how agent outputs are added**, wrap the agent in a separate node function:
|
||||
- each agent receives overall message history (across all agents) in the multi-agent system as its input. If you want more control over agent inputs, see [this section](#control-agent-inputs)
|
||||
- each agent outputs its internal messages history to the overall message history of the multi-agent system. If you want more control over **how agent outputs are added**, wrap the agent in a separate node function:
|
||||
|
||||
:::python
|
||||
```python
|
||||
def call_hotel_assistant(state):
|
||||
# return agent's final response,
|
||||
# excluding inner monologue
|
||||
response = hotel_assistant.invoke(state)
|
||||
# highlight-next-line
|
||||
return {"messages": response["messages"][-1]}
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
const callHotelAssistant = async (state) => {
|
||||
// return agent's final response,
|
||||
// excluding inner monologue
|
||||
const response = await hotelAssistant.invoke(state);
|
||||
// highlight-next-line
|
||||
return { messages: [response.messages.at(-1)] };
|
||||
};
|
||||
```
|
||||
:::
|
||||
```python
|
||||
def call_hotel_assistant(state):
|
||||
# return agent's final response,
|
||||
# excluding inner monologue
|
||||
response = hotel_assistant.invoke(state)
|
||||
# highlight-next-line
|
||||
return {"messages": response["messages"][-1]}
|
||||
```
|
||||
|
||||
### Control agent inputs
|
||||
|
||||
:::python
|
||||
You can use the @[`Send()`][Send] primitive to directly send data to the worker agents during the handoff. For example, you can request that the calling agent populate a task description for the next agent:
|
||||
|
||||
```python
|
||||
@@ -214,63 +129,6 @@ def create_task_description_handoff_tool(
|
||||
|
||||
return handoff_tool
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
You can use the @[`Send()`][Send] primitive to directly send data to the worker agents during the handoff. For example, you can request that the calling agent populate a task description for the next agent:
|
||||
|
||||
```typescript
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { Command, Send, MessagesZodState } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
function createTaskDescriptionHandoffTool({
|
||||
agentName,
|
||||
description,
|
||||
}: {
|
||||
agentName: string;
|
||||
description?: string;
|
||||
}) {
|
||||
const name = `transfer_to_${agentName}`;
|
||||
const toolDescription = description || `Ask ${agentName} for help.`;
|
||||
|
||||
return tool(
|
||||
async (
|
||||
{ taskDescription },
|
||||
config
|
||||
) => {
|
||||
const state = config.state;
|
||||
|
||||
const taskDescriptionMessage = {
|
||||
role: "user" as const,
|
||||
content: taskDescription,
|
||||
};
|
||||
const agentInput = {
|
||||
...state,
|
||||
messages: [taskDescriptionMessage],
|
||||
};
|
||||
|
||||
return new Command({
|
||||
// highlight-next-line
|
||||
goto: [new Send(agentName, agentInput)],
|
||||
graph: Command.PARENT,
|
||||
});
|
||||
},
|
||||
{
|
||||
name,
|
||||
description: toolDescription,
|
||||
schema: z.object({
|
||||
taskDescription: z
|
||||
.string()
|
||||
.describe(
|
||||
"Description of what the next agent should do, including all of the relevant context."
|
||||
),
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
See the multi-agent [supervisor](../tutorials/multi_agent/agent_supervisor.md#4-create-delegation-tasks) example for a full example of using @[`Send()`][Send] in handoffs.
|
||||
|
||||
@@ -278,7 +136,6 @@ See the multi-agent [supervisor](../tutorials/multi_agent/agent_supervisor.md#4-
|
||||
|
||||
You can use handoffs in any agents built with LangGraph. We recommend using the prebuilt [agent](../agents/overview.md) or [`ToolNode`](./tool-calling.md#toolnode), as they natively support handoffs tools returning `Command`. Below is an example of how you can implement a multi-agent system for booking travel using handoffs:
|
||||
|
||||
:::python
|
||||
```python
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
from langgraph.graph import StateGraph, START, MessagesState
|
||||
@@ -319,65 +176,9 @@ multi_agent_graph = (
|
||||
.compile()
|
||||
)
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { StateGraph, START, MessagesZodState } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
function createHandoffTool({
|
||||
agentName,
|
||||
description,
|
||||
}: {
|
||||
agentName: string;
|
||||
description?: string;
|
||||
}) {
|
||||
// same implementation as above
|
||||
// ...
|
||||
return new Command(/* ... */);
|
||||
}
|
||||
|
||||
// Handoffs
|
||||
const transferToHotelAssistant = createHandoffTool({
|
||||
agentName: "hotel_assistant",
|
||||
});
|
||||
const transferToFlightAssistant = createHandoffTool({
|
||||
agentName: "flight_assistant",
|
||||
});
|
||||
|
||||
// Define agents
|
||||
const flightAssistant = createReactAgent({
|
||||
llm: model,
|
||||
// highlight-next-line
|
||||
tools: [/* ... */, transferToHotelAssistant],
|
||||
// highlight-next-line
|
||||
name: "flight_assistant",
|
||||
});
|
||||
|
||||
const hotelAssistant = createReactAgent({
|
||||
llm: model,
|
||||
// highlight-next-line
|
||||
tools: [/* ... */, transferToFlightAssistant],
|
||||
// highlight-next-line
|
||||
name: "hotel_assistant",
|
||||
});
|
||||
|
||||
// Define multi-agent graph
|
||||
const multiAgentGraph = new StateGraph(MessagesZodState)
|
||||
// highlight-next-line
|
||||
.addNode("flight_assistant", flightAssistant)
|
||||
// highlight-next-line
|
||||
.addNode("hotel_assistant", hotelAssistant)
|
||||
.addEdge(START, "flight_assistant")
|
||||
.compile();
|
||||
```
|
||||
:::
|
||||
|
||||
??? example "Full example: Multi-agent system for booking travel"
|
||||
|
||||
:::python
|
||||
```python
|
||||
from typing import Annotated
|
||||
from langchain_core.messages import convert_to_messages
|
||||
@@ -522,183 +323,6 @@ const multiAgentGraph = new StateGraph(MessagesZodState)
|
||||
3. Name of the agent or node to hand off to.
|
||||
4. Take the agent's messages and **add** them to the parent's **state** as part of the handoff. The next agent will see the parent state.
|
||||
5. Indicate to LangGraph that we need to navigate to agent node in a **parent** multi-agent graph.
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { StateGraph, START, MessagesZodState, Command } from "@langchain/langgraph";
|
||||
import { ChatAnthropic } from "@langchain/anthropic";
|
||||
import { isBaseMessage } from "@langchain/core/messages";
|
||||
import { z } from "zod";
|
||||
|
||||
// We'll use a helper to render the streamed agent outputs nicely
|
||||
const prettyPrintMessages = (update: Record<string, any>) => {
|
||||
// Handle tuple case with namespace
|
||||
if (Array.isArray(update)) {
|
||||
const [ns, updateData] = update;
|
||||
// Skip parent graph updates in the printouts
|
||||
if (ns.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const graphId = ns[ns.length - 1].split(":")[0];
|
||||
console.log(`Update from subgraph ${graphId}:\n`);
|
||||
update = updateData;
|
||||
}
|
||||
|
||||
for (const [nodeName, updateValue] of Object.entries(update)) {
|
||||
console.log(`Update from node ${nodeName}:\n`);
|
||||
|
||||
const messages = updateValue.messages || [];
|
||||
for (const message of messages) {
|
||||
if (isBaseMessage(message)) {
|
||||
const textContent =
|
||||
typeof message.content === "string"
|
||||
? message.content
|
||||
: JSON.stringify(message.content);
|
||||
console.log(`${message.getType()}: ${textContent}`);
|
||||
}
|
||||
}
|
||||
console.log("\n");
|
||||
}
|
||||
};
|
||||
|
||||
function createHandoffTool({
|
||||
agentName,
|
||||
description,
|
||||
}: {
|
||||
agentName: string;
|
||||
description?: string;
|
||||
}) {
|
||||
const name = `transfer_to_${agentName}`;
|
||||
const toolDescription = description || `Transfer to ${agentName}`;
|
||||
|
||||
return tool(
|
||||
async (_, config) => {
|
||||
// highlight-next-line
|
||||
const state = config.state; // (1)!
|
||||
const toolCallId = config.toolCall.id;
|
||||
|
||||
const toolMessage = {
|
||||
role: "tool" as const,
|
||||
content: `Successfully transferred to ${agentName}`,
|
||||
name: name,
|
||||
tool_call_id: toolCallId,
|
||||
};
|
||||
|
||||
return new Command({
|
||||
// highlight-next-line
|
||||
goto: agentName, // (3)!
|
||||
// highlight-next-line
|
||||
update: { messages: [...state.messages, toolMessage] }, // (4)!
|
||||
// highlight-next-line
|
||||
graph: Command.PARENT, // (5)!
|
||||
});
|
||||
},
|
||||
{
|
||||
name,
|
||||
description: toolDescription,
|
||||
schema: z.object({}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Handoffs
|
||||
const transferToHotelAssistant = createHandoffTool({
|
||||
agentName: "hotel_assistant",
|
||||
description: "Transfer user to the hotel-booking assistant.",
|
||||
});
|
||||
|
||||
const transferToFlightAssistant = createHandoffTool({
|
||||
agentName: "flight_assistant",
|
||||
description: "Transfer user to the flight-booking assistant.",
|
||||
});
|
||||
|
||||
// Simple agent tools
|
||||
const bookHotel = tool(
|
||||
async ({ hotelName }) => {
|
||||
return `Successfully booked a stay at ${hotelName}.`;
|
||||
},
|
||||
{
|
||||
name: "book_hotel",
|
||||
description: "Book a hotel",
|
||||
schema: z.object({
|
||||
hotelName: z.string(),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const bookFlight = tool(
|
||||
async ({ fromAirport, toAirport }) => {
|
||||
return `Successfully booked a flight from ${fromAirport} to ${toAirport}.`;
|
||||
},
|
||||
{
|
||||
name: "book_flight",
|
||||
description: "Book a flight",
|
||||
schema: z.object({
|
||||
fromAirport: z.string(),
|
||||
toAirport: z.string(),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const model = new ChatAnthropic({
|
||||
model: "claude-3-5-sonnet-latest",
|
||||
});
|
||||
|
||||
// Define agents
|
||||
const flightAssistant = createReactAgent({
|
||||
llm: model,
|
||||
// highlight-next-line
|
||||
tools: [bookFlight, transferToHotelAssistant],
|
||||
prompt: "You are a flight booking assistant",
|
||||
// highlight-next-line
|
||||
name: "flight_assistant",
|
||||
});
|
||||
|
||||
const hotelAssistant = createReactAgent({
|
||||
llm: model,
|
||||
// highlight-next-line
|
||||
tools: [bookHotel, transferToFlightAssistant],
|
||||
prompt: "You are a hotel booking assistant",
|
||||
// highlight-next-line
|
||||
name: "hotel_assistant",
|
||||
});
|
||||
|
||||
// Define multi-agent graph
|
||||
const multiAgentGraph = new StateGraph(MessagesZodState)
|
||||
.addNode("flight_assistant", flightAssistant)
|
||||
.addNode("hotel_assistant", hotelAssistant)
|
||||
.addEdge(START, "flight_assistant")
|
||||
.compile();
|
||||
|
||||
// Run the multi-agent graph
|
||||
const stream = await multiAgentGraph.stream(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "book a flight from BOS to JFK and a stay at McKittrick Hotel",
|
||||
},
|
||||
],
|
||||
},
|
||||
// highlight-next-line
|
||||
{ subgraphs: true }
|
||||
);
|
||||
|
||||
for await (const chunk of stream) {
|
||||
prettyPrintMessages(chunk);
|
||||
}
|
||||
```
|
||||
|
||||
1. Access agent's state
|
||||
2. The `Command` primitive allows specifying a state update and a node transition as a single operation, making it useful for implementing handoffs.
|
||||
3. Name of the agent or node to hand off to.
|
||||
4. Take the agent's messages and **add** them to the parent's **state** as part of the handoff. The next agent will see the parent state.
|
||||
5. Indicate to LangGraph that we need to navigate to agent node in a **parent** multi-agent graph.
|
||||
:::
|
||||
|
||||
## Multi-turn conversation
|
||||
|
||||
@@ -709,7 +333,6 @@ The agents can then be implemented as nodes in a graph that executes agent steps
|
||||
1. **Wait for user input** to continue the conversation, or
|
||||
2. **Route to another agent** (or back to itself, such as in a loop) via a [handoff](#handoffs)
|
||||
|
||||
:::python
|
||||
```python
|
||||
def human(state) -> Command[Literal["agent", "another_agent"]]:
|
||||
"""A node for collecting user input."""
|
||||
@@ -737,44 +360,6 @@ def agent(state) -> Command[Literal["agent", "another_agent", "human"]]:
|
||||
else:
|
||||
return Command(goto="human") # Go to human node
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { interrupt, Command } from "@langchain/langgraph";
|
||||
|
||||
function human(state: MessagesState): Command {
|
||||
const userInput: string = interrupt("Ready for user input.");
|
||||
|
||||
// Determine the active agent
|
||||
const activeAgent = /* ... */;
|
||||
|
||||
return new Command({
|
||||
update: {
|
||||
messages: [{
|
||||
role: "human",
|
||||
content: userInput,
|
||||
}]
|
||||
},
|
||||
goto: activeAgent,
|
||||
});
|
||||
}
|
||||
|
||||
function agent(state: MessagesState): Command {
|
||||
// The condition for routing/halting can be anything, e.g. LLM tool call / structured output, etc.
|
||||
const goto = getNextAgent(/* ... */); // 'agent' / 'anotherAgent'
|
||||
|
||||
if (goto) {
|
||||
return new Command({
|
||||
goto,
|
||||
update: { myStateKey: "myStateValue" }
|
||||
});
|
||||
}
|
||||
|
||||
return new Command({ goto: "human" });
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
??? example "Full example: multi-agent system for travel recommendations"
|
||||
|
||||
@@ -785,7 +370,6 @@ function agent(state: MessagesState): Command {
|
||||
* travel_advisor: can help with travel destination recommendations. Can ask hotel_advisor for help.
|
||||
* hotel_advisor: can help with hotel recommendations. Can ask travel_advisor for help.
|
||||
|
||||
:::python
|
||||
```python
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langgraph.graph import MessagesState, StateGraph, START
|
||||
@@ -987,267 +571,10 @@ function agent(state: MessagesState): Command {
|
||||
|
||||
Would you like more specific information about any of these activities or would you like to know about other options in the area?
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { ChatAnthropic } from "@langchain/anthropic";
|
||||
import { StateGraph, START, MessagesZodState, Command, interrupt, MemorySaver } from "@langchain/langgraph";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
const model = new ChatAnthropic({ model: "claude-3-5-sonnet-latest" });
|
||||
|
||||
const MultiAgentState = MessagesZodState.extend({
|
||||
lastActiveAgent: z.string().optional(),
|
||||
});
|
||||
|
||||
// Define travel advisor tools
|
||||
const getTravelRecommendations = tool(
|
||||
async () => {
|
||||
// Placeholder implementation
|
||||
return "Based on current trends, I recommend visiting Japan, Portugal, or New Zealand.";
|
||||
},
|
||||
{
|
||||
name: "get_travel_recommendations",
|
||||
description: "Get current travel destination recommendations",
|
||||
schema: z.object({}),
|
||||
}
|
||||
);
|
||||
|
||||
const makeHandoffTool = (agentName: string) => {
|
||||
return tool(
|
||||
async (_, config) => {
|
||||
const state = config.state;
|
||||
const toolCallId = config.toolCall.id;
|
||||
|
||||
const toolMessage = {
|
||||
role: "tool" as const,
|
||||
content: `Successfully transferred to ${agentName}`,
|
||||
name: `transfer_to_${agentName}`,
|
||||
tool_call_id: toolCallId,
|
||||
};
|
||||
|
||||
return new Command({
|
||||
goto: agentName,
|
||||
update: { messages: [...state.messages, toolMessage] },
|
||||
graph: Command.PARENT,
|
||||
});
|
||||
},
|
||||
{
|
||||
name: `transfer_to_${agentName}`,
|
||||
description: `Transfer to ${agentName}`,
|
||||
schema: z.object({}),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const travelAdvisorTools = [
|
||||
getTravelRecommendations,
|
||||
makeHandoffTool("hotel_advisor"),
|
||||
];
|
||||
|
||||
const travelAdvisor = createReactAgent({
|
||||
llm: model,
|
||||
tools: travelAdvisorTools,
|
||||
prompt: [
|
||||
"You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc). ",
|
||||
"If you need hotel recommendations, ask 'hotel_advisor' for help. ",
|
||||
"You MUST include human-readable response before transferring to another agent."
|
||||
].join("")
|
||||
});
|
||||
|
||||
const callTravelAdvisor = async (
|
||||
state: z.infer<typeof MultiAgentState>
|
||||
): Promise<Command> => {
|
||||
const response = await travelAdvisor.invoke(state);
|
||||
const update = { ...response, lastActiveAgent: "travel_advisor" };
|
||||
return new Command({ update, goto: "human" });
|
||||
};
|
||||
|
||||
// Define hotel advisor tools
|
||||
const getHotelRecommendations = tool(
|
||||
async () => {
|
||||
// Placeholder implementation
|
||||
return "I recommend the Ritz-Carlton for luxury stays or boutique hotels for unique experiences.";
|
||||
},
|
||||
{
|
||||
name: "get_hotel_recommendations",
|
||||
description: "Get hotel recommendations for destinations",
|
||||
schema: z.object({}),
|
||||
}
|
||||
);
|
||||
|
||||
const hotelAdvisorTools = [
|
||||
getHotelRecommendations,
|
||||
makeHandoffTool("travel_advisor"),
|
||||
];
|
||||
|
||||
const hotelAdvisor = createReactAgent({
|
||||
llm: model,
|
||||
tools: hotelAdvisorTools,
|
||||
prompt: [
|
||||
"You are a hotel expert that can provide hotel recommendations for a given destination. ",
|
||||
"If you need help picking travel destinations, ask 'travel_advisor' for help.",
|
||||
"You MUST include human-readable response before transferring to another agent."
|
||||
].join("")
|
||||
});
|
||||
|
||||
const callHotelAdvisor = async (
|
||||
state: z.infer<typeof MultiAgentState>
|
||||
): Promise<Command> => {
|
||||
const response = await hotelAdvisor.invoke(state);
|
||||
const update = { ...response, lastActiveAgent: "hotel_advisor" };
|
||||
return new Command({ update, goto: "human" });
|
||||
};
|
||||
|
||||
const humanNode = async (
|
||||
state: z.infer<typeof MultiAgentState>
|
||||
): Promise<Command> => {
|
||||
const userInput: string = interrupt("Ready for user input.");
|
||||
const activeAgent = state.lastActiveAgent || "travel_advisor";
|
||||
|
||||
return new Command({
|
||||
update: {
|
||||
messages: [
|
||||
{
|
||||
role: "human",
|
||||
content: userInput,
|
||||
}
|
||||
]
|
||||
},
|
||||
goto: activeAgent,
|
||||
});
|
||||
};
|
||||
|
||||
const builder = new StateGraph(MultiAgentState)
|
||||
.addNode("travel_advisor", callTravelAdvisor)
|
||||
.addNode("hotel_advisor", callHotelAdvisor)
|
||||
.addNode("human", humanNode)
|
||||
.addEdge(START, "travel_advisor");
|
||||
|
||||
const checkpointer = new MemorySaver();
|
||||
const graph = builder.compile({ checkpointer });
|
||||
```
|
||||
|
||||
Let's test a multi turn conversation with this application.
|
||||
|
||||
```typescript
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Command } from "@langchain/langgraph";
|
||||
|
||||
const threadConfig = { configurable: { thread_id: uuidv4() } };
|
||||
|
||||
const inputs = [
|
||||
// 1st round of conversation
|
||||
{
|
||||
messages: [
|
||||
{ role: "user", content: "i wanna go somewhere warm in the caribbean" }
|
||||
]
|
||||
},
|
||||
// Since we're using `interrupt`, we'll need to resume using the Command primitive.
|
||||
// 2nd round of conversation
|
||||
new Command({
|
||||
resume: "could you recommend a nice hotel in one of the areas and tell me which area it is."
|
||||
}),
|
||||
// 3rd round of conversation
|
||||
new Command({
|
||||
resume: "i like the first one. could you recommend something to do near the hotel?"
|
||||
}),
|
||||
];
|
||||
|
||||
for (const [idx, userInput] of inputs.entries()) {
|
||||
console.log();
|
||||
console.log(`--- Conversation Turn ${idx + 1} ---`);
|
||||
console.log();
|
||||
console.log(`User: ${JSON.stringify(userInput)}`);
|
||||
console.log();
|
||||
|
||||
for await (const update of await graph.stream(
|
||||
userInput,
|
||||
{ ...threadConfig, streamMode: "updates" }
|
||||
)) {
|
||||
for (const [nodeId, value] of Object.entries(update)) {
|
||||
if (value?.messages?.length) {
|
||||
const lastMessage = value.messages.at(-1);
|
||||
if (lastMessage?.getType?.() === "ai") {
|
||||
console.log(`${nodeId}: ${lastMessage.content}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
--- Conversation Turn 1 ---
|
||||
|
||||
User: {"messages":[{"role":"user","content":"i wanna go somewhere warm in the caribbean"}]}
|
||||
|
||||
travel_advisor: Based on the recommendations, Aruba would be an excellent choice for your Caribbean getaway! Aruba is known as "One Happy Island" and offers:
|
||||
- Year-round warm weather with consistent temperatures around 82°F (28°C)
|
||||
- Beautiful white sand beaches like Eagle Beach and Palm Beach
|
||||
- Clear turquoise waters perfect for swimming and snorkeling
|
||||
- Minimal rainfall and location outside the hurricane belt
|
||||
- A blend of Caribbean and Dutch culture
|
||||
- Great dining options and nightlife
|
||||
- Various water sports and activities
|
||||
|
||||
Would you like me to get some specific hotel recommendations in Aruba for your stay? I can transfer you to our hotel advisor who can help with accommodations.
|
||||
|
||||
--- Conversation Turn 2 ---
|
||||
|
||||
User: Command { resume: 'could you recommend a nice hotel in one of the areas and tell me which area it is.' }
|
||||
|
||||
hotel_advisor: Based on the recommendations, I can suggest two excellent options:
|
||||
|
||||
1. The Ritz-Carlton, Aruba - Located in Palm Beach
|
||||
- This luxury resort is situated in the vibrant Palm Beach area
|
||||
- Known for its exceptional service and amenities
|
||||
- Perfect if you want to be close to dining, shopping, and entertainment
|
||||
- Features multiple restaurants, a casino, and a world-class spa
|
||||
- Located on a pristine stretch of Palm Beach
|
||||
|
||||
2. Bucuti & Tara Beach Resort - Located in Eagle Beach
|
||||
- An adults-only boutique resort on Eagle Beach
|
||||
- Known for being more intimate and peaceful
|
||||
- Award-winning for its sustainability practices
|
||||
- Perfect for a romantic getaway or peaceful vacation
|
||||
- Located on one of the most beautiful beaches in the Caribbean
|
||||
|
||||
Would you like more specific information about either of these properties or their locations?
|
||||
|
||||
--- Conversation Turn 3 ---
|
||||
|
||||
User: Command { resume: 'i like the first one. could you recommend something to do near the hotel?' }
|
||||
|
||||
travel_advisor: Near the Ritz-Carlton in Palm Beach, here are some highly recommended activities:
|
||||
|
||||
1. Visit the Palm Beach Plaza Mall - Just a short walk from the hotel, featuring shopping, dining, and entertainment
|
||||
2. Try your luck at the Stellaris Casino - It's right in the Ritz-Carlton
|
||||
3. Take a sunset sailing cruise - Many depart from the nearby pier
|
||||
4. Visit the California Lighthouse - A scenic landmark just north of Palm Beach
|
||||
5. Enjoy water sports at Palm Beach:
|
||||
- Jet skiing
|
||||
- Parasailing
|
||||
- Snorkeling
|
||||
- Stand-up paddleboarding
|
||||
|
||||
Would you like more specific information about any of these activities or would you like to know about other options in the area?
|
||||
```
|
||||
:::
|
||||
|
||||
## Prebuilt implementations
|
||||
|
||||
LangGraph comes with prebuilt implementations of two of the most popular multi-agent architectures:
|
||||
|
||||
:::python
|
||||
- [supervisor](../agents/multi-agent.md#supervisor) — individual agents are coordinated by a central supervisor agent. The supervisor controls all communication flow and task delegation, making decisions about which agent to invoke based on the current context and task requirements. You can use [`langgraph-supervisor`](https://github.com/langchain-ai/langgraph-supervisor-py) library to create a supervisor multi-agent systems.
|
||||
- [swarm](../agents/multi-agent.md#supervisor) — agents dynamically hand off control to one another based on their specializations. The system remembers which agent was last active, ensuring that on subsequent interactions, the conversation resumes with that agent. You can use [`langgraph-swarm`](https://github.com/langchain-ai/langgraph-swarm-py) library to create a swarm multi-agent systems.
|
||||
:::
|
||||
|
||||
:::js
|
||||
- [supervisor](../agents/multi-agent.md#supervisor) — individual agents are coordinated by a central supervisor agent. The supervisor controls all communication flow and task delegation, making decisions about which agent to invoke based on the current context and task requirements. You can use [`langgraph-supervisor`](https://github.com/langchain-ai/langgraph-supervisor-js) library to create a supervisor multi-agent systems.
|
||||
- [swarm](../agents/multi-agent.md#supervisor) — agents dynamically hand off control to one another based on their specializations. The system remembers which agent was last active, ensuring that on subsequent interactions, the conversation resumes with that agent. You can use [`langgraph-swarm`](https://github.com/langchain-ai/langgraph-swarm-js) library to create a swarm multi-agent systems.
|
||||
:::
|
||||
- [swarm](../agents/multi-agent.md#supervisor) — agents dynamically hand off control to one another based on their specializations. The system remembers which agent was last active, ensuring that on subsequent interactions, the conversation resumes with that agent. You can use [`langgraph-swarm`](https://github.com/langchain-ai/langgraph-swarm-py) library to create a swarm multi-agent systems.
|
||||
@@ -9,20 +9,11 @@ When adding subgraphs, you need to define how the parent graph and the subgraph
|
||||
|
||||
## Setup
|
||||
|
||||
:::python
|
||||
```bash
|
||||
pip install -U langgraph
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```bash
|
||||
npm install @langchain/langgraph
|
||||
```
|
||||
:::
|
||||
|
||||
!!! tip "Set up LangSmith for LangGraph development"
|
||||
|
||||
Sign up for [LangSmith](https://smith.langchain.com) to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started [here](https://docs.smith.langchain.com).
|
||||
|
||||
## Shared state schemas
|
||||
@@ -31,7 +22,6 @@ A common case is for the parent graph and subgraph to communicate over a shared
|
||||
|
||||
If your subgraph shares state keys with the parent graph, you can follow these steps to add it to your graph:
|
||||
|
||||
:::python
|
||||
1. Define the subgraph workflow (`subgraph_builder` in the example below) and compile it
|
||||
2. Pass compiled subgraph to the `.add_node` method when defining the parent graph workflow
|
||||
|
||||
@@ -59,41 +49,9 @@ builder.add_node("node_1", subgraph)
|
||||
builder.add_edge(START, "node_1")
|
||||
graph = builder.compile()
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
1. Define the subgraph workflow (`subgraphBuilder` in the example below) and compile it
|
||||
2. Pass compiled subgraph to the `.addNode` method when defining the parent graph workflow
|
||||
|
||||
```typescript
|
||||
import { StateGraph, START } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
const State = z.object({
|
||||
foo: z.string(),
|
||||
});
|
||||
|
||||
// Subgraph
|
||||
const subgraphBuilder = new StateGraph(State)
|
||||
.addNode("subgraphNode1", (state) => {
|
||||
return { foo: "hi! " + state.foo };
|
||||
})
|
||||
.addEdge(START, "subgraphNode1");
|
||||
|
||||
const subgraph = subgraphBuilder.compile();
|
||||
|
||||
// Parent graph
|
||||
const builder = new StateGraph(State)
|
||||
.addNode("node1", subgraph)
|
||||
.addEdge(START, "node1");
|
||||
|
||||
const graph = builder.compile();
|
||||
```
|
||||
:::
|
||||
|
||||
??? example "Full example: shared state schemas"
|
||||
|
||||
:::python
|
||||
```python
|
||||
from typing_extensions import TypedDict
|
||||
from langgraph.graph.state import StateGraph, START
|
||||
@@ -143,61 +101,6 @@ const graph = builder.compile();
|
||||
{'node_1': {'foo': 'hi! foo'}}
|
||||
{'node_2': {'foo': 'hi! foobar'}}
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { StateGraph, START } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
// Define subgraph
|
||||
const SubgraphState = z.object({
|
||||
foo: z.string(), // (1)!
|
||||
bar: z.string(), // (2)!
|
||||
});
|
||||
|
||||
const subgraphBuilder = new StateGraph(SubgraphState)
|
||||
.addNode("subgraphNode1", (state) => {
|
||||
return { bar: "bar" };
|
||||
})
|
||||
.addNode("subgraphNode2", (state) => {
|
||||
// note that this node is using a state key ('bar') that is only available in the subgraph
|
||||
// and is sending update on the shared state key ('foo')
|
||||
return { foo: state.foo + state.bar };
|
||||
})
|
||||
.addEdge(START, "subgraphNode1")
|
||||
.addEdge("subgraphNode1", "subgraphNode2");
|
||||
|
||||
const subgraph = subgraphBuilder.compile();
|
||||
|
||||
// Define parent graph
|
||||
const ParentState = z.object({
|
||||
foo: z.string(),
|
||||
});
|
||||
|
||||
const builder = new StateGraph(ParentState)
|
||||
.addNode("node1", (state) => {
|
||||
return { foo: "hi! " + state.foo };
|
||||
})
|
||||
.addNode("node2", subgraph)
|
||||
.addEdge(START, "node1")
|
||||
.addEdge("node1", "node2");
|
||||
|
||||
const graph = builder.compile();
|
||||
|
||||
for await (const chunk of await graph.stream({ foo: "foo" })) {
|
||||
console.log(chunk);
|
||||
}
|
||||
```
|
||||
|
||||
3. This key is shared with the parent graph state
|
||||
4. This key is private to the `SubgraphState` and is not visible to the parent graph
|
||||
|
||||
```
|
||||
{ node1: { foo: 'hi! foo' } }
|
||||
{ node2: { foo: 'hi! foobar' } }
|
||||
```
|
||||
:::
|
||||
|
||||
## Different state schemas
|
||||
|
||||
@@ -205,7 +108,6 @@ For more complex systems you might want to define subgraphs that have a **comple
|
||||
|
||||
If that's the case for your application, you need to define a node **function that invokes the subgraph**. This function needs to transform the input (parent) state to the subgraph state before invoking the subgraph, and transform the results back to the parent state before returning the state update from the node.
|
||||
|
||||
:::python
|
||||
```python
|
||||
from typing_extensions import TypedDict
|
||||
from langgraph.graph.state import StateGraph, START
|
||||
@@ -240,48 +142,9 @@ graph = builder.compile()
|
||||
|
||||
1. Transform the state to the subgraph state
|
||||
2. Transform response back to the parent state
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { StateGraph, START } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
const SubgraphState = z.object({
|
||||
bar: z.string(),
|
||||
});
|
||||
|
||||
// Subgraph
|
||||
const subgraphBuilder = new StateGraph(SubgraphState)
|
||||
.addNode("subgraphNode1", (state) => {
|
||||
return { bar: "hi! " + state.bar };
|
||||
})
|
||||
.addEdge(START, "subgraphNode1");
|
||||
|
||||
const subgraph = subgraphBuilder.compile();
|
||||
|
||||
// Parent graph
|
||||
const State = z.object({
|
||||
foo: z.string(),
|
||||
});
|
||||
|
||||
const builder = new StateGraph(State)
|
||||
.addNode("node1", async (state) => {
|
||||
const subgraphOutput = await subgraph.invoke({ bar: state.foo }); // (1)!
|
||||
return { foo: subgraphOutput.bar }; // (2)!
|
||||
})
|
||||
.addEdge(START, "node1");
|
||||
|
||||
const graph = builder.compile();
|
||||
```
|
||||
|
||||
1. Transform the state to the subgraph state
|
||||
2. Transform response back to the parent state
|
||||
:::
|
||||
|
||||
??? example "Full example: different state schemas"
|
||||
|
||||
:::python
|
||||
```python
|
||||
from typing_extensions import TypedDict
|
||||
from langgraph.graph.state import StateGraph, START
|
||||
@@ -337,74 +200,11 @@ const graph = builder.compile();
|
||||
(('node_2:9c36dd0f-151a-cb42-cbad-fa2f851f9ab7',), {'grandchild_2': {'bar': 'hi! foobaz'}})
|
||||
((), {'node_2': {'foo': 'hi! foobaz'}})
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { StateGraph, START } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
// Define subgraph
|
||||
const SubgraphState = z.object({
|
||||
// note that none of these keys are shared with the parent graph state
|
||||
bar: z.string(),
|
||||
baz: z.string(),
|
||||
});
|
||||
|
||||
const subgraphBuilder = new StateGraph(SubgraphState)
|
||||
.addNode("subgraphNode1", (state) => {
|
||||
return { baz: "baz" };
|
||||
})
|
||||
.addNode("subgraphNode2", (state) => {
|
||||
return { bar: state.bar + state.baz };
|
||||
})
|
||||
.addEdge(START, "subgraphNode1")
|
||||
.addEdge("subgraphNode1", "subgraphNode2");
|
||||
|
||||
const subgraph = subgraphBuilder.compile();
|
||||
|
||||
// Define parent graph
|
||||
const ParentState = z.object({
|
||||
foo: z.string(),
|
||||
});
|
||||
|
||||
const builder = new StateGraph(ParentState)
|
||||
.addNode("node1", (state) => {
|
||||
return { foo: "hi! " + state.foo };
|
||||
})
|
||||
.addNode("node2", async (state) => {
|
||||
const response = await subgraph.invoke({ bar: state.foo }); // (1)!
|
||||
return { foo: response.bar }; // (2)!
|
||||
})
|
||||
.addEdge(START, "node1")
|
||||
.addEdge("node1", "node2");
|
||||
|
||||
const graph = builder.compile();
|
||||
|
||||
for await (const chunk of await graph.stream(
|
||||
{ foo: "foo" },
|
||||
{ subgraphs: true }
|
||||
)) {
|
||||
console.log(chunk);
|
||||
}
|
||||
```
|
||||
|
||||
3. Transform the state to the subgraph state
|
||||
4. Transform response back to the parent state
|
||||
|
||||
```
|
||||
[[], { node1: { foo: 'hi! foo' } }]
|
||||
[['node2:9c36dd0f-151a-cb42-cbad-fa2f851f9ab7'], { subgraphNode1: { baz: 'baz' } }]
|
||||
[['node2:9c36dd0f-151a-cb42-cbad-fa2f851f9ab7'], { subgraphNode2: { bar: 'hi! foobaz' } }]
|
||||
[[], { node2: { foo: 'hi! foobaz' } }]
|
||||
```
|
||||
:::
|
||||
|
||||
??? example "Full example: different state schemas (two levels of subgraphs)"
|
||||
|
||||
This is an example with two levels of subgraphs: parent -> child -> grandchild.
|
||||
|
||||
:::python
|
||||
```python
|
||||
# Grandchild graph
|
||||
from typing_extensions import TypedDict
|
||||
@@ -488,102 +288,14 @@ const graph = builder.compile();
|
||||
((), {'child': {'my_key': 'hi Bob, how are you today?'}})
|
||||
((), {'parent_2': {'my_key': 'hi Bob, how are you today? bye!'}})
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { StateGraph, START, END } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
// Grandchild graph
|
||||
const GrandChildState = z.object({
|
||||
myGrandchildKey: z.string(),
|
||||
});
|
||||
|
||||
const grandchild = new StateGraph(GrandChildState)
|
||||
.addNode("grandchild1", (state) => {
|
||||
// NOTE: child or parent keys will not be accessible here
|
||||
return { myGrandchildKey: state.myGrandchildKey + ", how are you" };
|
||||
})
|
||||
.addEdge(START, "grandchild1")
|
||||
.addEdge("grandchild1", END);
|
||||
|
||||
const grandchildGraph = grandchild.compile();
|
||||
|
||||
// Child graph
|
||||
const ChildState = z.object({
|
||||
myChildKey: z.string(),
|
||||
});
|
||||
|
||||
const child = new StateGraph(ChildState)
|
||||
.addNode("child1", async (state) => {
|
||||
// NOTE: parent or grandchild keys won't be accessible here
|
||||
const grandchildGraphInput = { myGrandchildKey: state.myChildKey }; // (1)!
|
||||
const grandchildGraphOutput = await grandchildGraph.invoke(grandchildGraphInput);
|
||||
return { myChildKey: grandchildGraphOutput.myGrandchildKey + " today?" }; // (2)!
|
||||
}) // (3)!
|
||||
.addEdge(START, "child1")
|
||||
.addEdge("child1", END);
|
||||
|
||||
const childGraph = child.compile();
|
||||
|
||||
// Parent graph
|
||||
const ParentState = z.object({
|
||||
myKey: z.string(),
|
||||
});
|
||||
|
||||
const parent = new StateGraph(ParentState)
|
||||
.addNode("parent1", (state) => {
|
||||
// NOTE: child or grandchild keys won't be accessible here
|
||||
return { myKey: "hi " + state.myKey };
|
||||
})
|
||||
.addNode("child", async (state) => {
|
||||
const childGraphInput = { myChildKey: state.myKey }; // (4)!
|
||||
const childGraphOutput = await childGraph.invoke(childGraphInput);
|
||||
return { myKey: childGraphOutput.myChildKey }; // (5)!
|
||||
}) // (6)!
|
||||
.addNode("parent2", (state) => {
|
||||
return { myKey: state.myKey + " bye!" };
|
||||
})
|
||||
.addEdge(START, "parent1")
|
||||
.addEdge("parent1", "child")
|
||||
.addEdge("child", "parent2")
|
||||
.addEdge("parent2", END);
|
||||
|
||||
const parentGraph = parent.compile();
|
||||
|
||||
for await (const chunk of await parentGraph.stream(
|
||||
{ myKey: "Bob" },
|
||||
{ subgraphs: true }
|
||||
)) {
|
||||
console.log(chunk);
|
||||
}
|
||||
```
|
||||
|
||||
7. We're transforming the state from the child state channels (`myChildKey`) to the grandchild state channels (`myGrandchildKey`)
|
||||
8. We're transforming the state from the grandchild state channels (`myGrandchildKey`) back to the child state channels (`myChildKey`)
|
||||
9. We're passing a function here instead of just compiled graph (`grandchildGraph`)
|
||||
10. We're transforming the state from the parent state channels (`myKey`) to the child state channels (`myChildKey`)
|
||||
11. We're transforming the state from the child state channels (`myChildKey`) back to the parent state channels (`myKey`)
|
||||
12. We're passing a function here instead of just a compiled graph (`childGraph`)
|
||||
|
||||
```
|
||||
[[], { parent1: { myKey: 'hi Bob' } }]
|
||||
[['child:2e26e9ce-602f-862c-aa66-1ea5a4655e3b', 'child1:781bb3b1-3971-84ce-810b-acf819a03f9c'], { grandchild1: { myGrandchildKey: 'hi Bob, how are you' } }]
|
||||
[['child:2e26e9ce-602f-862c-aa66-1ea5a4655e3b'], { child1: { myChildKey: 'hi Bob, how are you today?' } }]
|
||||
[[], { child: { myKey: 'hi Bob, how are you today?' } }]
|
||||
[[], { parent2: { myKey: 'hi Bob, how are you today? bye!' } }]
|
||||
```
|
||||
:::
|
||||
|
||||
## Add persistence
|
||||
|
||||
You only need to **provide the checkpointer when compiling the parent graph**. LangGraph will automatically propagate the checkpointer to the child subgraphs.
|
||||
|
||||
:::python
|
||||
```python
|
||||
from langgraph.graph import START, StateGraph
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
class State(TypedDict):
|
||||
@@ -605,66 +317,20 @@ builder = StateGraph(State)
|
||||
builder.add_node("node_1", subgraph)
|
||||
builder.add_edge(START, "node_1")
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { StateGraph, START, MemorySaver } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
If you want the subgraph to **have its own memory**, you can compile it `with checkpointer=True`. This is useful in [multi-agent](../concepts/multi_agent.md) systems, if you want agents to keep track of their internal message histories:
|
||||
|
||||
const State = z.object({
|
||||
foo: z.string(),
|
||||
});
|
||||
|
||||
// Subgraph
|
||||
const subgraphBuilder = new StateGraph(State)
|
||||
.addNode("subgraphNode1", (state) => {
|
||||
return { foo: state.foo + "bar" };
|
||||
})
|
||||
.addEdge(START, "subgraphNode1");
|
||||
|
||||
const subgraph = subgraphBuilder.compile();
|
||||
|
||||
// Parent graph
|
||||
const builder = new StateGraph(State)
|
||||
.addNode("node1", subgraph)
|
||||
.addEdge(START, "node1");
|
||||
|
||||
const checkpointer = new MemorySaver();
|
||||
const graph = builder.compile({ checkpointer });
|
||||
```
|
||||
:::
|
||||
|
||||
If you want the subgraph to **have its own memory**, you can compile it with the appropriate checkpointer option. This is useful in [multi-agent](../concepts/multi_agent.md) systems, if you want agents to keep track of their internal message histories:
|
||||
|
||||
:::python
|
||||
```python
|
||||
subgraph_builder = StateGraph(...)
|
||||
subgraph = subgraph_builder.compile(checkpointer=True)
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
const subgraphBuilder = new StateGraph(...)
|
||||
const subgraph = subgraphBuilder.compile({ checkpointer: true });
|
||||
```
|
||||
:::
|
||||
|
||||
## View subgraph state
|
||||
|
||||
When you enable [persistence](../concepts/persistence.md), you can [inspect the graph state](../concepts/persistence.md#checkpoints) (checkpoint) via the appropriate method. To view the subgraph state, you can use the subgraphs option.
|
||||
|
||||
:::python
|
||||
You can inspect the graph state via `graph.get_state(config)`. To view the subgraph state, you can use `graph.get_state(config, subgraphs=True)`.
|
||||
:::
|
||||
|
||||
:::js
|
||||
You can inspect the graph state via `graph.getState(config)`. To view the subgraph state, you can use `graph.getState(config, { subgraphs: true })`.
|
||||
:::
|
||||
When you enable [persistence](../concepts/persistence.md), you can [inspect the graph state](../concepts/persistence.md#checkpoints) (checkpoint) via `graph.get_state(config)`. To view the subgraph state, you can use `graph.get_state(config, subgraphs=True)`.
|
||||
|
||||
!!! important "Available **only** when interrupted"
|
||||
|
||||
@@ -672,10 +338,9 @@ You can inspect the graph state via `graph.getState(config)`. To view the subgra
|
||||
|
||||
??? example "View interrupted subgraph state"
|
||||
|
||||
:::python
|
||||
```python
|
||||
from langgraph.graph import START, StateGraph
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.types import interrupt, Command
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
@@ -700,7 +365,7 @@ You can inspect the graph state via `graph.getState(config)`. To view the subgra
|
||||
builder.add_node("node_1", subgraph)
|
||||
builder.add_edge(START, "node_1")
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
@@ -714,53 +379,11 @@ You can inspect the graph state via `graph.getState(config)`. To view the subgra
|
||||
```
|
||||
|
||||
1. This will be available only when the subgraph is interrupted. Once you resume the graph, you won't be able to access the subgraph state.
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { StateGraph, START, MemorySaver, interrupt, Command } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
const State = z.object({
|
||||
foo: z.string(),
|
||||
});
|
||||
|
||||
// Subgraph
|
||||
const subgraphBuilder = new StateGraph(State)
|
||||
.addNode("subgraphNode1", (state) => {
|
||||
const value = interrupt("Provide value:");
|
||||
return { foo: state.foo + value };
|
||||
})
|
||||
.addEdge(START, "subgraphNode1");
|
||||
|
||||
const subgraph = subgraphBuilder.compile();
|
||||
|
||||
// Parent graph
|
||||
const builder = new StateGraph(State)
|
||||
.addNode("node1", subgraph)
|
||||
.addEdge(START, "node1");
|
||||
|
||||
const checkpointer = new MemorySaver();
|
||||
const graph = builder.compile({ checkpointer });
|
||||
|
||||
const config = { configurable: { thread_id: "1" } };
|
||||
|
||||
await graph.invoke({ foo: "" }, config);
|
||||
const parentState = await graph.getState(config);
|
||||
const subgraphState = (await graph.getState(config, { subgraphs: true })).tasks[0].state; // (1)!
|
||||
|
||||
// resume the subgraph
|
||||
await graph.invoke(new Command({ resume: "bar" }), config);
|
||||
```
|
||||
|
||||
2. This will be available only when the subgraph is interrupted. Once you resume the graph, you won't be able to access the subgraph state.
|
||||
:::
|
||||
|
||||
## Stream subgraph outputs
|
||||
|
||||
To include outputs from subgraphs in the streamed outputs, you can set the subgraphs option in the stream method of the parent graph. This will stream outputs from both the parent graph and any subgraphs.
|
||||
To include outputs from subgraphs in the streamed outputs, you can set `subgraphs=True` in the `.stream()` method of the parent graph. This will stream outputs from both the parent graph and any subgraphs.
|
||||
|
||||
:::python
|
||||
```python
|
||||
for chunk in graph.stream(
|
||||
{"foo": "foo"},
|
||||
@@ -771,27 +394,9 @@ for chunk in graph.stream(
|
||||
```
|
||||
|
||||
1. Set `subgraphs=True` to stream outputs from subgraphs.
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
for await (const chunk of await graph.stream(
|
||||
{ foo: "foo" },
|
||||
{
|
||||
subgraphs: true, // (1)!
|
||||
streamMode: "updates",
|
||||
}
|
||||
)) {
|
||||
console.log(chunk);
|
||||
}
|
||||
```
|
||||
|
||||
1. Set `subgraphs: true` to stream outputs from subgraphs.
|
||||
:::
|
||||
|
||||
??? example "Stream from subgraphs"
|
||||
|
||||
:::python
|
||||
```python
|
||||
from typing_extensions import TypedDict
|
||||
from langgraph.graph.state import StateGraph, START
|
||||
@@ -845,66 +450,4 @@ for await (const chunk of await graph.stream(
|
||||
(('node_2:e58e5673-a661-ebb0-70d4-e298a7fc28b7',), {'subgraph_node_1': {'bar': 'bar'}})
|
||||
(('node_2:e58e5673-a661-ebb0-70d4-e298a7fc28b7',), {'subgraph_node_2': {'foo': 'hi! foobar'}})
|
||||
((), {'node_2': {'foo': 'hi! foobar'}})
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { StateGraph, START } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
// Define subgraph
|
||||
const SubgraphState = z.object({
|
||||
foo: z.string(),
|
||||
bar: z.string(),
|
||||
});
|
||||
|
||||
const subgraphBuilder = new StateGraph(SubgraphState)
|
||||
.addNode("subgraphNode1", (state) => {
|
||||
return { bar: "bar" };
|
||||
})
|
||||
.addNode("subgraphNode2", (state) => {
|
||||
// note that this node is using a state key ('bar') that is only available in the subgraph
|
||||
// and is sending update on the shared state key ('foo')
|
||||
return { foo: state.foo + state.bar };
|
||||
})
|
||||
.addEdge(START, "subgraphNode1")
|
||||
.addEdge("subgraphNode1", "subgraphNode2");
|
||||
|
||||
const subgraph = subgraphBuilder.compile();
|
||||
|
||||
// Define parent graph
|
||||
const ParentState = z.object({
|
||||
foo: z.string(),
|
||||
});
|
||||
|
||||
const builder = new StateGraph(ParentState)
|
||||
.addNode("node1", (state) => {
|
||||
return { foo: "hi! " + state.foo };
|
||||
})
|
||||
.addNode("node2", subgraph)
|
||||
.addEdge(START, "node1")
|
||||
.addEdge("node1", "node2");
|
||||
|
||||
const graph = builder.compile();
|
||||
|
||||
for await (const chunk of await graph.stream(
|
||||
{ foo: "foo" },
|
||||
{
|
||||
streamMode: "updates",
|
||||
subgraphs: true, // (1)!
|
||||
}
|
||||
)) {
|
||||
console.log(chunk);
|
||||
}
|
||||
```
|
||||
|
||||
2. Set `subgraphs: true` to stream outputs from subgraphs.
|
||||
|
||||
```
|
||||
[[], { node1: { foo: 'hi! foo' } }]
|
||||
[['node2:e58e5673-a661-ebb0-70d4-e298a7fc28b7'], { subgraphNode1: { bar: 'bar' } }]
|
||||
[['node2:e58e5673-a661-ebb0-70d4-e298a7fc28b7'], { subgraphNode2: { foo: 'hi! foobar' } }]
|
||||
[[], { node2: { foo: 'hi! foobar' } }]
|
||||
```
|
||||
:::
|
||||
|
||||
@@ -172,82 +172,6 @@ await agent.invoke({
|
||||
|
||||
:::
|
||||
|
||||
:::python
|
||||
|
||||
### Dynamically select tools
|
||||
|
||||
Configure tool availability at runtime based on context:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
|
||||
from langchain.chat_models import init_chat_model
|
||||
from langchain_core.tools import tool
|
||||
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
from langgraph.prebuilt.chat_agent_executor import AgentState
|
||||
from langgraph.runtime import Runtime
|
||||
|
||||
|
||||
@dataclass
|
||||
class CustomContext:
|
||||
tools: list[Literal["weather", "compass"]]
|
||||
|
||||
|
||||
@tool
|
||||
def weather() -> str:
|
||||
"""Returns the current weather conditions."""
|
||||
return "It's nice and sunny."
|
||||
|
||||
|
||||
@tool
|
||||
def compass() -> str:
|
||||
"""Returns the direction the user is facing."""
|
||||
return "North"
|
||||
|
||||
model = init_chat_model("anthropic:claude-sonnet-4-20250514")
|
||||
|
||||
# highlight-next-line
|
||||
def configure_model(state: AgentState, runtime: Runtime[CustomContext]):
|
||||
"""Configure the model with tools based on runtime context."""
|
||||
selected_tools = [
|
||||
tool
|
||||
for tool in [weather, compass]
|
||||
if tool.name in runtime.context.tools
|
||||
]
|
||||
return model.bind_tools(selected_tools)
|
||||
|
||||
|
||||
agent = create_react_agent(
|
||||
# Dynamically configure the model with tools based on runtime context
|
||||
# highlight-next-line
|
||||
configure_model,
|
||||
# Initialize with all tools available
|
||||
# highlight-next-line
|
||||
tools=[weather, compass]
|
||||
)
|
||||
|
||||
output = agent.invoke(
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Who are you and what tools do you have access to?",
|
||||
}
|
||||
]
|
||||
},
|
||||
# highlight-next-line
|
||||
context=CustomContext(tools=["weather"]), # Only enable the weather tool
|
||||
)
|
||||
|
||||
print(output["messages"][-1].text())
|
||||
```
|
||||
|
||||
!!! version-added "New in langgraph>=0.6"
|
||||
|
||||
:::
|
||||
|
||||
## Use in a workflow
|
||||
|
||||
If you are writing a custom workflow, you will need to:
|
||||
@@ -1571,7 +1495,6 @@ const saveUserInfo = tool(
|
||||
|
||||
from langchain_core.tools import tool
|
||||
from langgraph.config import get_store
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
|
||||
|
||||
+3
-3
@@ -10,7 +10,7 @@
|
||||
- [Implementing Human-in-the-Loop Controls in LangGraph](https://langchain-ai.github.io/langgraph/tutorials/get-started/4-human-in-the-loop/): This page provides a comprehensive guide on adding human-in-the-loop controls to LangGraph workflows, enabling agents to pause execution for human input. It details the use of the `interrupt` function to facilitate user feedback and outlines the steps to integrate a `human_assistance` tool into a chatbot. Additionally, the tutorial covers graph compilation, visualization, and resuming execution with human input.
|
||||
- [Customizing State in LangGraph for Enhanced Chatbot Functionality](https://langchain-ai.github.io/langgraph/tutorials/get-started/5-customize-state/): This tutorial guides you through the process of adding custom fields to the state in LangGraph, enabling complex behaviors in your chatbot without relying solely on message lists. You will learn how to implement human-in-the-loop controls to verify information before it is stored in the state. By the end of this tutorial, you will have a deeper understanding of state management and how to enhance your chatbot's capabilities.
|
||||
- [Implementing Time Travel in LangGraph Chatbots](https://langchain-ai.github.io/langgraph/tutorials/get-started/6-time-travel/): This page provides a comprehensive guide on utilizing the time travel functionality in LangGraph to enhance chatbot interactions. It covers how to rewind, add steps, and replay the state history of a chatbot, allowing users to explore different outcomes and fix mistakes. Additionally, it includes code snippets and practical examples to help developers implement these features effectively.
|
||||
- [LangGraph Deployment Options](https://langchain-ai.github.io/langgraph/tutorials/deployment/): This page outlines the various options available for deploying LangGraph applications, including local testing and different cloud-based solutions. It details free deployment methods such as Local, as well as production options like Cloud SaaS and self-hosted solutions. Each deployment method is linked to further documentation for in-depth guidance.
|
||||
- [LangGraph Deployment Options](https://langchain-ai.github.io/langgraph/tutorials/deployment/): This page outlines the various options available for deploying LangGraph applications, including local testing and different cloud-based solutions. It details free deployment methods such as Local and Standalone Container (Lite), as well as production options like Cloud SaaS and self-hosted solutions. Each deployment method is linked to further documentation for in-depth guidance.
|
||||
- [Agent Development with LangGraph](https://langchain-ai.github.io/langgraph/agents/overview/): This page provides an overview of agent development using LangGraph, highlighting its prebuilt components and capabilities for building agent-based applications. It explains the structure of an agent, key features such as memory integration and human-in-the-loop control, and outlines the package ecosystem available for developers. With LangGraph, users can focus on application logic while leveraging robust infrastructure for state management and feedback.
|
||||
- [Guide to Running Agents in LangGraph](https://langchain-ai.github.io/langgraph/agents/run_agents/): This page provides a comprehensive overview of how to execute agents in LangGraph, detailing both synchronous and asynchronous methods. It covers input and output formats, streaming capabilities, and how to manage execution limits to prevent infinite loops. Additionally, it includes code examples and links to further resources for deeper understanding.
|
||||
- [Streaming Data in LangGraph](https://langchain-ai.github.io/langgraph/agents/streaming/): This page provides an overview of streaming data types in LangGraph, including agent progress, LLM tokens, and custom updates. It includes code examples for both synchronous and asynchronous streaming methods. Additionally, it covers how to stream multiple modes and disable streaming when necessary.
|
||||
@@ -73,7 +73,7 @@
|
||||
- [Integrating Semantic Search in LangGraph](https://langchain-ai.github.io/langgraph/cloud/deployment/semantic_search/): This guide provides step-by-step instructions on how to implement semantic search in your LangGraph deployment. It covers prerequisites, configuration of the store, and usage examples for searching memories and documents by semantic similarity. Additionally, it includes information on using custom embeddings and querying via the LangGraph SDK.
|
||||
- [Configuring Time-to-Live (TTL) in LangGraph Applications](https://langchain-ai.github.io/langgraph/how-tos/ttl/configure_ttl/): This guide provides detailed instructions on how to configure Time-to-Live (TTL) settings for checkpoints and store items in LangGraph applications. It covers the necessary configurations in the `langgraph.json` file, including strategies for managing data lifecycle and memory. Additionally, it explains how to combine TTL configurations and override them at runtime.
|
||||
- [LangGraph Authentication & Access Control Overview](https://langchain-ai.github.io/langgraph/concepts/auth/): This page provides a comprehensive guide to the authentication and authorization mechanisms within the LangGraph Platform. It explains the core concepts of authentication versus authorization, outlines default security models, and details the system architecture involved in user identity management. Additionally, it covers implementation examples for authentication and authorization handlers, along with common access patterns and supported resources.
|
||||
- [Custom Authentication Setup for LangGraph Platform](https://langchain-ai.github.io/langgraph/how-tos/auth/custom_auth/): This guide provides step-by-step instructions on how to implement custom authentication in your LangGraph Platform application. It covers the necessary prerequisites, implementation details, configuration updates, and client connection methods. The guide is applicable to both managed and Enterprise self-hosted deployments.
|
||||
- [Custom Authentication Setup for LangGraph Platform](https://langchain-ai.github.io/langgraph/how-tos/auth/custom_auth/): This guide provides step-by-step instructions on how to implement custom authentication in your LangGraph Platform application. It covers the necessary prerequisites, implementation details, configuration updates, and client connection methods. The guide is applicable to both managed and Enterprise self-hosted deployments, but not to Lite self-hosted plans.
|
||||
- [Documenting API Authentication in OpenAPI for LangGraph](https://langchain-ai.github.io/langgraph/how-tos/auth/openapi_security/): This guide provides instructions on how to customize the security schema for your LangGraph Platform API documentation using OpenAPI. It covers default security schemes for both LangGraph Platform and self-hosted deployments, as well as how to implement custom authentication. Additionally, it includes examples for OAuth2 and API key authentication, along with testing procedures.
|
||||
- [Managing Assistants in LangGraph](https://langchain-ai.github.io/langgraph/concepts/assistants/): This page provides an overview of how to create and manage assistants within the LangGraph Platform, which allows for separate configuration of agents without altering the core graph logic. It covers the prerequisites, configuration options, and versioning of assistants, highlighting their role in optimizing agent performance for different tasks. Additionally, it includes links to relevant API references and how-to guides for further assistance.
|
||||
- [Managing Assistants in LangGraph](https://langchain-ai.github.io/langgraph/cloud/how-tos/configuration_cloud/): This documentation page provides a comprehensive guide on how to create, configure, and manage assistants using the LangGraph SDK and Platform UI. It includes code examples in Python and JavaScript, as well as instructions for creating new versions and using previous versions of assistants. Additionally, it covers the process of utilizing assistants in various environments.
|
||||
@@ -112,7 +112,7 @@
|
||||
- [Deploying a Self-Hosted Data Plane](https://langchain-ai.github.io/langgraph/cloud/deployment/self_hosted_data_plane/): This page provides a comprehensive guide on deploying a Self-Hosted Data Plane using Kubernetes and Amazon ECS. It outlines the prerequisites, setup steps, and configuration details necessary for a successful deployment. Additionally, it highlights the current beta status of this deployment option.
|
||||
- [Self-Hosted Control Plane Deployment Guide](https://langchain-ai.github.io/langgraph/concepts/langgraph_self_hosted_control_plane/): This page provides an overview of the Self-Hosted Control Plane deployment option, currently in beta. It outlines the requirements, architecture, and compute platforms supported for deploying the control and data planes in your cloud environment. Additionally, it includes important links and resources for managing your self-hosted infrastructure.
|
||||
- [Deploying a Self-Hosted Control Plane](https://langchain-ai.github.io/langgraph/cloud/deployment/self_hosted_control_plane/): This page provides a comprehensive guide on deploying a Self-Hosted Control Plane using Kubernetes. It outlines the prerequisites, setup steps, and configuration details necessary for a successful deployment. Additionally, it highlights the beta status of this deployment option and includes links to relevant resources for further assistance.
|
||||
- [Deploying LangGraph Server with Standalone Container](https://langchain-ai.github.io/langgraph/concepts/langgraph_standalone_container/): This page provides a comprehensive guide on deploying a LangGraph Server using the Standalone Container option. It outlines the architecture, supported compute platforms, and Enterprise server version features. Users will find essential information on managing the data plane infrastructure without a control plane.
|
||||
- [Deploying LangGraph Server with Standalone Container](https://langchain-ai.github.io/langgraph/concepts/langgraph_standalone_container/): This page provides a comprehensive guide on deploying a LangGraph Server using the Standalone Container option. It outlines the architecture, supported compute platforms, and differences between Lite and Enterprise server versions. Users will find essential information on managing the data plane infrastructure without a control plane.
|
||||
- [Deploying a Standalone Container with LangGraph](https://langchain-ai.github.io/langgraph/cloud/deployment/standalone_container/): This documentation provides a comprehensive guide on deploying a standalone container for the LangGraph application. It covers prerequisites, environment variable configurations, and deployment methods using Docker and Docker Compose. Additionally, it includes instructions for deploying on Kubernetes using Helm.
|
||||
- [Scalability and Resilience of LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/scalability_and_resilience/): This page provides an overview of the scalability and resilience features of the LangGraph Platform. It details how the platform handles server and queue scalability, as well as the mechanisms in place for ensuring resilience during both graceful and hard shutdowns. Additionally, it covers the resilience strategies employed for Postgres and Redis to maintain service availability.
|
||||
- [LangGraph Platform Plans Overview](https://langchain-ai.github.io/langgraph/concepts/plans/): This page provides an overview of the different plans available for the LangGraph Platform, including Developer, Plus, and Enterprise options. Each plan offers varying deployment options, usage limits, and features tailored to different user needs. For detailed pricing and related resources, links to additional documentation are also included.
|
||||
|
||||
@@ -49,8 +49,9 @@ Higher-level abstractions for common workflows, agents, and other patterns.
|
||||
|
||||
Tools for deploying and connecting to the LangGraph Platform.
|
||||
|
||||
- [CLI](../cloud/reference/cli.md): Command-line interface for building and deploying LangGraph Platform applications.
|
||||
- [Server API](../cloud/reference/api/api_ref.md): REST API for the LangGraph Server.
|
||||
- [SDK (Python)](../cloud/reference/sdk/python_sdk_ref.md): Python SDK for interacting with instances of the LangGraph Server.
|
||||
- [SDK (JS/TS)](../cloud/reference/sdk/js_ts_sdk_ref.md): JavaScript/TypeScript SDK for interacting with instances of the LangGraph Server.
|
||||
- [RemoteGraph](remote_graph.md): `Pregel` abstraction for connecting to LangGraph Server instances.
|
||||
|
||||
See the [LangGraph Platform reference](https://docs.langchain.com/langgraph-platform/reference-overview) for more reference documentation.
|
||||
- [Environment variables](../cloud/reference/env_var.md): Supported configuration variables when deploying with the LangGraph Platform.
|
||||
@@ -21,9 +21,15 @@ See the [local server](../../tutorials/langgraph-platform/local-server.md) docs
|
||||
|
||||
If you would like a fast managed environment, consider the [Cloud SaaS](../../concepts/langgraph_cloud.md) deployment option. This requires no additional license key.
|
||||
|
||||
#### For Standalone Container
|
||||
#### For Standalone Container (Lite)
|
||||
|
||||
For self-hosting, set the `LANGGRAPH_CLOUD_LICENSE_KEY` environment variable. If you are interested in an enterprise license key, please contact the LangChain support team.
|
||||
If your deployment is unlikely to see more than 1 million node executions per year and don't need Crons and other enterprise features, consider the [Standalone Container](../../concepts/deployment_options.md) deployment option.
|
||||
|
||||
You can deploy with Standalone Container by setting a valid `LANGSMITH_API_KEY` in your environment (e.g., in the `.env` file referenced by `langgraph.json`) and building a Docker image. The API key must be associated with an account on a **Plus** plan or greater.
|
||||
|
||||
#### For Standalone Container (Enterprise)
|
||||
|
||||
For full self-hosting, set the `LANGGRAPH_CLOUD_LICENSE_KEY` environment variable. If you are interested in an enterprise license key, please contact the LangChain support team.
|
||||
|
||||
For more information on deployment options and their features, see the [Deployment Options](../../concepts/deployment_options.md) documentation.
|
||||
|
||||
@@ -32,7 +38,12 @@ For more information on deployment options and their features, see the [Deployme
|
||||
|
||||
If you have confirmed that you would like to self-host LangGraph Platform, please verify your credentials.
|
||||
|
||||
#### For Standalone Container
|
||||
#### For Standalone Container (Lite)
|
||||
|
||||
1. Confirm that you have provided a working `LANGSMITH_API_KEY` environment variable in your deployment environment or `.env` file
|
||||
2. Confirm the provided API key is associated with an account on a **Plus** or **Enterprise** plan (or equivalent)
|
||||
|
||||
#### For Standalone Container (Enterprise)
|
||||
|
||||
1. Confirm that you have provided a working `LANGGRAPH_CLOUD_LICENSE_KEY` environment variable in your deployment environment or `.env` file
|
||||
2. Confirm the key is still valid and has not surpassed its expiration date
|
||||
@@ -11,4 +11,11 @@ Errors referenced below will have an `lc_error_code` property corresponding to o
|
||||
- [INVALID_CONCURRENT_GRAPH_UPDATE](./INVALID_CONCURRENT_GRAPH_UPDATE.md)
|
||||
- [INVALID_GRAPH_NODE_RETURN_VALUE](./INVALID_GRAPH_NODE_RETURN_VALUE.md)
|
||||
- [MULTIPLE_SUBGRAPHS](./MULTIPLE_SUBGRAPHS.md)
|
||||
- [INVALID_CHAT_HISTORY](./INVALID_CHAT_HISTORY.md)
|
||||
- [INVALID_CHAT_HISTORY](./INVALID_CHAT_HISTORY.md)
|
||||
|
||||
## LangGraph Platform
|
||||
|
||||
These guides provide troubleshooting information for errors that are specific to the LangGraph Platform.
|
||||
|
||||
- [INVALID_LICENSE](./INVALID_LICENSE.md)
|
||||
- [Studio Errors](../studio.md)
|
||||
|
||||
@@ -17,7 +17,7 @@ Create a `MemorySaver` checkpointer:
|
||||
:::python
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
memory = InMemorySaver()
|
||||
```
|
||||
@@ -447,4 +447,3 @@ const graph = new StateGraph(State)
|
||||
## Next steps
|
||||
|
||||
In the next tutorial, you will [add human-in-the-loop to the chatbot](./4-human-in-the-loop.md) to handle situations where it may need guidance or verification before proceeding.
|
||||
|
||||
|
||||
@@ -85,80 +85,124 @@ Let's [run the agent](../../agents/run_agents.md) to verify that it behaves as e
|
||||
|
||||
!!! note "We'll use `pretty_print_messages` helper to render the streamed agent outputs nicely"
|
||||
|
||||
```python
|
||||
from langchain_core.messages import convert_to_messages
|
||||
```python
|
||||
from langchain_core.messages import convert_to_messages
|
||||
|
||||
|
||||
def pretty_print_message(message, indent=False):
|
||||
pretty_message = message.pretty_repr(html=True)
|
||||
if not indent:
|
||||
print(pretty_message)
|
||||
return
|
||||
|
||||
indented = "\n".join("\t" + c for c in pretty_message.split("\n"))
|
||||
print(indented)
|
||||
|
||||
|
||||
def pretty_print_messages(update, last_message=False):
|
||||
is_subgraph = False
|
||||
if isinstance(update, tuple):
|
||||
ns, update = update
|
||||
# skip parent graph updates in the printouts
|
||||
if len(ns) == 0:
|
||||
return
|
||||
|
||||
graph_id = ns[-1].split(":")[0]
|
||||
print(f"Update from subgraph {graph_id}:")
|
||||
print("\n")
|
||||
is_subgraph = True
|
||||
|
||||
for node_name, node_update in update.items():
|
||||
update_label = f"Update from node {node_name}:"
|
||||
if is_subgraph:
|
||||
update_label = "\t" + update_label
|
||||
|
||||
print(update_label)
|
||||
print("\n")
|
||||
|
||||
messages = convert_to_messages(node_update["messages"])
|
||||
if last_message:
|
||||
messages = messages[-1:]
|
||||
|
||||
for m in messages:
|
||||
pretty_print_message(m, indent=is_subgraph)
|
||||
print("\n")
|
||||
```
|
||||
|
||||
```python
|
||||
from langchain_core.messages import convert_to_messages
|
||||
|
||||
|
||||
def pretty_print_message(message, indent=False):
|
||||
pretty_message = message.pretty_repr(html=True)
|
||||
if not indent:
|
||||
print(pretty_message)
|
||||
return
|
||||
def pretty_print_message(message, indent=False):
|
||||
pretty_message = message.pretty_repr(html=True)
|
||||
if not indent:
|
||||
print(pretty_message)
|
||||
return
|
||||
|
||||
indented = "\n".join("\t" + c for c in pretty_message.split("\n"))
|
||||
print(indented)
|
||||
indented = "\n".join("\t" + c for c in pretty_message.split("\n"))
|
||||
print(indented)
|
||||
|
||||
|
||||
def pretty_print_messages(update, last_message=False):
|
||||
is_subgraph = False
|
||||
if isinstance(update, tuple):
|
||||
ns, update = update
|
||||
# skip parent graph updates in the printouts
|
||||
if len(ns) == 0:
|
||||
return
|
||||
def pretty_print_messages(update, last_message=False):
|
||||
is_subgraph = False
|
||||
if isinstance(update, tuple):
|
||||
ns, update = update
|
||||
# skip parent graph updates in the printouts
|
||||
if len(ns) == 0:
|
||||
return
|
||||
|
||||
graph_id = ns[-1].split(":")[0]
|
||||
print(f"Update from subgraph {graph_id}:")
|
||||
print("\n")
|
||||
is_subgraph = True
|
||||
graph_id = ns[-1].split(":")[0]
|
||||
print(f"Update from subgraph {graph_id}:")
|
||||
print("\n")
|
||||
is_subgraph = True
|
||||
|
||||
for node_name, node_update in update.items():
|
||||
update_label = f"Update from node {node_name}:"
|
||||
if is_subgraph:
|
||||
update_label = "\t" + update_label
|
||||
for node_name, node_update in update.items():
|
||||
update_label = f"Update from node {node_name}:"
|
||||
if is_subgraph:
|
||||
update_label = "\t" + update_label
|
||||
|
||||
print(update_label)
|
||||
print("\n")
|
||||
print(update_label)
|
||||
print("\n")
|
||||
|
||||
messages = convert_to_messages(node_update["messages"])
|
||||
if last_message:
|
||||
messages = messages[-1:]
|
||||
messages = convert_to_messages(node_update["messages"])
|
||||
if last_message:
|
||||
messages = messages[-1:]
|
||||
|
||||
for m in messages:
|
||||
pretty_print_message(m, indent=is_subgraph)
|
||||
print("\n")
|
||||
```
|
||||
for m in messages:
|
||||
pretty_print_message(m, indent=is_subgraph)
|
||||
print("\n")
|
||||
```
|
||||
|
||||
```python
|
||||
for chunk in research_agent.stream(
|
||||
{"messages": [{"role": "user", "content": "who is the mayor of NYC?"}]}
|
||||
):
|
||||
pretty_print_messages(chunk)
|
||||
```
|
||||
```python
|
||||
for chunk in research_agent.stream(
|
||||
{"messages": [{"role": "user", "content": "who is the mayor of NYC?"}]}
|
||||
):
|
||||
pretty_print_messages(chunk)
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
Update from node agent:
|
||||
**Output:**
|
||||
```
|
||||
Update from node agent:
|
||||
|
||||
|
||||
================================== Ai Message ==================================
|
||||
Name: research_agent
|
||||
Tool Calls:
|
||||
tavily_search (call_U748rQhQXT36sjhbkYLSXQtJ)
|
||||
Call ID: call_U748rQhQXT36sjhbkYLSXQtJ
|
||||
Args:
|
||||
query: current mayor of New York City
|
||||
search_depth: basic
|
||||
================================== Ai Message ==================================
|
||||
Name: research_agent
|
||||
Tool Calls:
|
||||
tavily_search (call_U748rQhQXT36sjhbkYLSXQtJ)
|
||||
Call ID: call_U748rQhQXT36sjhbkYLSXQtJ
|
||||
Args:
|
||||
query: current mayor of New York City
|
||||
search_depth: basic
|
||||
|
||||
|
||||
Update from node tools:
|
||||
Update from node tools:
|
||||
|
||||
|
||||
================================= Tool Message ==================================
|
||||
Name: tavily_search
|
||||
================================= Tool Message ==================================
|
||||
Name: tavily_search
|
||||
|
||||
{"query": "current mayor of New York City", "follow_up_questions": null, "answer": null, "images": [], "results": [{"title": "List of mayors of New York City - Wikipedia", "url": "https://en.wikipedia.org/wiki/List_of_mayors_of_New_York_City", "content": "The mayor of New York City is the chief executive of the Government of New York City, as stipulated by New York City's charter.The current officeholder, the 110th in the sequence of regular mayors, is Eric Adams, a member of the Democratic Party.. During the Dutch colonial period from 1624 to 1664, New Amsterdam was governed by the Director of Netherland.", "score": 0.9039154, "raw_content": null}, {"title": "Office of the Mayor | Mayor's Bio | City of New York - NYC.gov", "url": "https://www.nyc.gov/office-of-the-mayor/bio.page", "content": "Mayor Eric Adams has served the people of New York City as an NYPD officer, State Senator, Brooklyn Borough President, and now as the 110th Mayor of the City of New York. He gave voice to a diverse coalition of working families in all five boroughs and is leading the fight to bring back New York City's economy, reduce inequality, improve", "score": 0.8405867, "raw_content": null}, {"title": "Eric Adams - Wikipedia", "url": "https://en.wikipedia.org/wiki/Eric_Adams", "content": "Eric Leroy Adams (born September 1, 1960) is an American politician and former police officer who has served as the 110th mayor of New York City since 2022. Adams was an officer in the New York City Transit Police and then the New York City Police Department (```
|
||||
```
|
||||
{"query": "current mayor of New York City", "follow_up_questions": null, "answer": null, "images": [], "results": [{"title": "List of mayors of New York City - Wikipedia", "url": "https://en.wikipedia.org/wiki/List_of_mayors_of_New_York_City", "content": "The mayor of New York City is the chief executive of the Government of New York City, as stipulated by New York City's charter.The current officeholder, the 110th in the sequence of regular mayors, is Eric Adams, a member of the Democratic Party.. During the Dutch colonial period from 1624 to 1664, New Amsterdam was governed by the Director of Netherland.", "score": 0.9039154, "raw_content": null}, {"title": "Office of the Mayor | Mayor's Bio | City of New York - NYC.gov", "url": "https://www.nyc.gov/office-of-the-mayor/bio.page", "content": "Mayor Eric Adams has served the people of New York City as an NYPD officer, State Senator, Brooklyn Borough President, and now as the 110th Mayor of the City of New York. He gave voice to a diverse coalition of working families in all five boroughs and is leading the fight to bring back New York City's economy, reduce inequality, improve", "score": 0.8405867, "raw_content": null}, {"title": "Eric Adams - Wikipedia", "url": "https://en.wikipedia.org/wiki/Eric_Adams", "content": "Eric Leroy Adams (born September 1, 1960) is an American politician and former police officer who has served as the 110th mayor of New York City since 2022. Adams was an officer in the New York City Transit Police and then the New York City Police Department (```
|
||||
```
|
||||
|
||||
### Math agent
|
||||
|
||||
@@ -765,4 +809,4 @@ Update from subgraph research_agent:
|
||||
Name: tavily_search
|
||||
|
||||
{"query": "2024 United States GDP value from a reputable source", "follow_up_questions": null, "answer": null, "images": [], "results": [{"url": "https://www.focus-economics.com/countries/united-states/", "title": "United States Economy Overview - Focus Economics", "content": "The United States' Macroeconomic Analysis:\n------------------------------------------\n\n**Nominal GDP of USD 29,185 billion in 2024.**\n\n**Nominal GDP of USD 29,179 billion in 2024.**\n\n**GDP per capita of USD 86,635 compared to the global average of USD 10,589.**\n\n**GDP per capita of USD 86,652 compared to the global average of USD 10,589.**\n\n**Average real GDP growth of 2.5% over the last decade.**\n\n**Average real GDP growth of ```
|
||||
```
|
||||
```
|
||||
+99
-98
@@ -52,103 +52,6 @@ theme:
|
||||
plugins:
|
||||
- search:
|
||||
separator: '[\s\u200b\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;'
|
||||
- exclude-search:
|
||||
exclude:
|
||||
- additional-resources/index.md
|
||||
- agents/prebuilt.md
|
||||
- cloud/concepts/cron_jobs.md
|
||||
- cloud/concepts/data_storage_and_privacy.md
|
||||
- cloud/concepts/webhooks.md
|
||||
- cloud/deployment/cloud.md
|
||||
- cloud/deployment/custom_docker.md
|
||||
- cloud/deployment/egress.md
|
||||
- cloud/deployment/graph_rebuild.md
|
||||
- cloud/deployment/self_hosted_control_plane.md
|
||||
- cloud/deployment/self_hosted_data_plane.md
|
||||
- cloud/deployment/semantic_search.md
|
||||
- cloud/deployment/setup_javascript.md
|
||||
- cloud/deployment/setup_pyproject.md
|
||||
- cloud/deployment/setup.md
|
||||
- cloud/deployment/standalone_container.md
|
||||
- cloud/how-tos/add-human-in-the-loop.md
|
||||
- cloud/how-tos/background_run.md
|
||||
- cloud/how-tos/clone_traces_studio.md
|
||||
- cloud/how-tos/configurable_headers.md
|
||||
- cloud/how-tos/configuration_cloud.md
|
||||
- cloud/how-tos/cron_jobs.md
|
||||
- cloud/how-tos/datasets_studio.md
|
||||
- cloud/how-tos/enqueue_concurrent.md
|
||||
- cloud/how-tos/generative_ui_react.md
|
||||
- cloud/how-tos/human_in_the_loop_time_travel.md
|
||||
- cloud/how-tos/interrupt_concurrent.md
|
||||
- cloud/how-tos/invoke_studio.md
|
||||
- cloud/how-tos/iterate_graph_studio.md
|
||||
- cloud/how-tos/reject_concurrent.md
|
||||
- cloud/how-tos/rollback_concurrent.md
|
||||
- cloud/how-tos/same-thread.md
|
||||
- cloud/how-tos/stateless_runs.md
|
||||
- cloud/how-tos/streaming.md
|
||||
- cloud/how-tos/studio/manage_assistants.md
|
||||
- cloud/how-tos/studio/quick_start.md
|
||||
- cloud/how-tos/studio/run_evals.md
|
||||
- cloud/how-tos/threads_studio.md
|
||||
- cloud/how-tos/use_stream_react.md
|
||||
- cloud/how-tos/use_threads.md
|
||||
- cloud/how-tos/webhooks.md
|
||||
- cloud/quick_start.md
|
||||
- cloud/reference/api/api_ref_control_plane.md
|
||||
- cloud/reference/api/api_ref.md
|
||||
- cloud/reference/cli.md
|
||||
- cloud/reference/env_var.md
|
||||
- cloud/reference/langgraph_server_changelog.md
|
||||
- cloud/reference/sdk/js_ts_sdk_ref.md
|
||||
- concepts/application_structure.md
|
||||
- concepts/assistants.md
|
||||
- concepts/auth.md
|
||||
- concepts/deployment_options.md
|
||||
- concepts/double_texting.md
|
||||
- concepts/faq.md
|
||||
- concepts/langgraph_cli.md
|
||||
- concepts/langgraph_cloud.md
|
||||
- concepts/langgraph_components.md
|
||||
- concepts/langgraph_control_plane.md
|
||||
- concepts/langgraph_data_plane.md
|
||||
- concepts/langgraph_platform.md
|
||||
- concepts/langgraph_self_hosted_control_plane.md
|
||||
- concepts/langgraph_self_hosted_data_plane.md
|
||||
- concepts/langgraph_server.md
|
||||
- concepts/langgraph_standalone_container.md
|
||||
- concepts/langgraph_studio.md
|
||||
- concepts/plans.md
|
||||
- concepts/scalability_and_resilience.md
|
||||
- concepts/sdk.md
|
||||
- concepts/server-mcp.md
|
||||
- concepts/template_applications.md
|
||||
- concepts/why-langgraph.md
|
||||
- examples/index.md
|
||||
- guides/index.md
|
||||
- how-tos/auth/custom_auth.md
|
||||
- how-tos/auth/openapi_security.md
|
||||
- how-tos/autogen-integration.md
|
||||
- how-tos/http/custom_lifespan.md
|
||||
- how-tos/http/custom_middleware.md
|
||||
- how-tos/http/custom_routes.md
|
||||
- how-tos/ttl/configure_ttl.md
|
||||
- how-tos/use-remote-graph.md
|
||||
- index.md
|
||||
- reference/index.md
|
||||
- snippets/chat_model_tabs.md
|
||||
- troubleshooting/errors/GRAPH_RECURSION_LIMIT.md
|
||||
- troubleshooting/errors/index.md
|
||||
- troubleshooting/errors/INVALID_CHAT_HISTORY.md
|
||||
- troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md
|
||||
- troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md
|
||||
- troubleshooting/errors/INVALID_LICENSE.md
|
||||
- troubleshooting/errors/MULTIPLE_SUBGRAPHS.md
|
||||
- troubleshooting/studio.md
|
||||
- tutorials/auth/add_auth_server.md
|
||||
- tutorials/auth/getting_started.md
|
||||
- tutorials/auth/resource_auth.md
|
||||
- tags
|
||||
- include-markdown
|
||||
- mkdocstrings:
|
||||
@@ -220,6 +123,7 @@ nav:
|
||||
- Streaming:
|
||||
- Overview: concepts/streaming.md
|
||||
- Stream outputs: how-tos/streaming.md
|
||||
- Use Server API: cloud/how-tos/streaming.md
|
||||
- Persistence:
|
||||
- Overview: concepts/persistence.md
|
||||
- Durable execution:
|
||||
@@ -237,9 +141,11 @@ nav:
|
||||
- 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
|
||||
- Use Server API: cloud/how-tos/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
|
||||
- Use Server API: cloud/how-tos/human_in_the_loop_time_travel.md
|
||||
- Subgraphs:
|
||||
- Overview: concepts/subgraphs.md
|
||||
- Use subgraphs: how-tos/subgraph.md
|
||||
@@ -250,10 +156,88 @@ nav:
|
||||
- MCP:
|
||||
- Overview: concepts/mcp.md
|
||||
- Use MCP: agents/mcp.md
|
||||
- Server API: concepts/server-mcp.md
|
||||
- Tracing:
|
||||
- Overview: concepts/tracing.md
|
||||
- Enable tracing: how-tos/enable-tracing.md
|
||||
- Evaluate performance: agents/evals.md
|
||||
- Platform-only capabilities:
|
||||
- LangGraph Platform:
|
||||
- Overview: concepts/langgraph_platform.md
|
||||
- Components:
|
||||
- Overview: concepts/langgraph_components.md
|
||||
- LangGraph Server:
|
||||
- Overview: concepts/langgraph_server.md
|
||||
- Data plane: concepts/langgraph_data_plane.md
|
||||
- Control plane: concepts/langgraph_control_plane.md
|
||||
- LangGraph CLI: concepts/langgraph_cli.md
|
||||
- LangGraph Studio:
|
||||
- Overview: concepts/langgraph_studio.md
|
||||
- Quickstart: cloud/how-tos/studio/quick_start.md
|
||||
- cloud/how-tos/invoke_studio.md
|
||||
- cloud/how-tos/studio/manage_assistants.md
|
||||
- cloud/how-tos/threads_studio.md
|
||||
- cloud/how-tos/iterate_graph_studio.md
|
||||
- cloud/how-tos/studio/run_evals.md
|
||||
- cloud/how-tos/clone_traces_studio.md
|
||||
- cloud/how-tos/datasets_studio.md
|
||||
- LangGraph SDK: concepts/sdk.md
|
||||
- Plans & pricing: concepts/plans.md
|
||||
- Application structure: concepts/application_structure.md
|
||||
- Scalability & resilience: concepts/scalability_and_resilience.md
|
||||
- Authentication & access control:
|
||||
- Overview: concepts/auth.md
|
||||
- how-tos/auth/custom_auth.md
|
||||
- how-tos/auth/openapi_security.md
|
||||
- Assistants:
|
||||
- Overview: concepts/assistants.md
|
||||
- cloud/how-tos/configuration_cloud.md
|
||||
- Threads: cloud/how-tos/use_threads.md
|
||||
- Runs:
|
||||
- cloud/how-tos/background_run.md
|
||||
- cloud/how-tos/same-thread.md
|
||||
- cloud/how-tos/cron_jobs.md
|
||||
- cloud/how-tos/stateless_runs.md
|
||||
- cloud/how-tos/configurable_headers.md
|
||||
- Double-texting:
|
||||
- Overview: concepts/double_texting.md
|
||||
- cloud/how-tos/interrupt_concurrent.md
|
||||
- cloud/how-tos/rollback_concurrent.md
|
||||
- cloud/how-tos/reject_concurrent.md
|
||||
- cloud/how-tos/enqueue_concurrent.md
|
||||
- Webhooks:
|
||||
- Overview: cloud/concepts/webhooks.md
|
||||
- Use webhooks: cloud/how-tos/webhooks.md
|
||||
- Cron jobs:
|
||||
- Overview: cloud/concepts/cron_jobs.md
|
||||
- cloud/how-tos/cron_jobs.md
|
||||
- Server customization:
|
||||
- how-tos/http/custom_lifespan.md
|
||||
- how-tos/http/custom_middleware.md
|
||||
- how-tos/http/custom_routes.md
|
||||
- Data management:
|
||||
- cloud/concepts/data_storage_and_privacy.md
|
||||
- Add semantic search: cloud/deployment/semantic_search.md
|
||||
- Add TTLs: how-tos/ttl/configure_ttl.md
|
||||
- Deployment:
|
||||
- Overview: concepts/deployment_options.md
|
||||
- Quickstart: cloud/quick_start.md
|
||||
- Set up your application:
|
||||
- Use requirements.txt: cloud/deployment/setup.md
|
||||
- Use pyproject.toml: cloud/deployment/setup_pyproject.md
|
||||
- Use JavaScript: cloud/deployment/setup_javascript.md
|
||||
- Use custom Docker: cloud/deployment/custom_docker.md
|
||||
- Rebuild graph at runtime: cloud/deployment/graph_rebuild.md
|
||||
- Deployment options:
|
||||
- Cloud SaaS: concepts/langgraph_cloud.md
|
||||
- Self-Hosted Data Plane: concepts/langgraph_self_hosted_data_plane.md
|
||||
- Self-Hosted Control Plane: concepts/langgraph_self_hosted_control_plane.md
|
||||
- Standalone Container: concepts/langgraph_standalone_container.md
|
||||
- Deploy to production:
|
||||
- Cloud SaaS: cloud/deployment/cloud.md
|
||||
- Self-Hosted Data Plane: cloud/deployment/self_hosted_data_plane.md
|
||||
- Self-Hosted Control Plane: cloud/deployment/self_hosted_control_plane.md
|
||||
- Standalone Container: cloud/deployment/standalone_container.md
|
||||
|
||||
- Reference:
|
||||
- reference/index.md
|
||||
@@ -276,9 +260,14 @@ nav:
|
||||
- Swarm: reference/swarm.md
|
||||
- MCP Adapters: reference/mcp.md
|
||||
- LangGraph Platform:
|
||||
- Server API: cloud/reference/api/api_ref.md
|
||||
- Server changelog: cloud/reference/langgraph_server_changelog.md
|
||||
- Control Plane API: cloud/reference/api/api_ref_control_plane.md
|
||||
- CLI: cloud/reference/cli.md
|
||||
- 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
|
||||
- Environment variables: cloud/reference/env_var.md
|
||||
|
||||
- Examples:
|
||||
- examples/index.md
|
||||
@@ -288,6 +277,16 @@ nav:
|
||||
- SQL agent: tutorials/sql/sql-agent.md
|
||||
- Prebuilt chat UI: agents/ui.md
|
||||
- Graph runs in LangSmith: how-tos/run-id-langsmith.md
|
||||
- LangGraph Platform:
|
||||
- Authentication:
|
||||
- tutorials/auth/getting_started.md
|
||||
- tutorials/auth/resource_auth.md
|
||||
- tutorials/auth/add_auth_server.md
|
||||
- Use RemoteGraph: how-tos/use-remote-graph.md
|
||||
- Deploy CrewAI, AutoGen, and other frameworks: how-tos/autogen-integration.md
|
||||
- Front-end and generative UI:
|
||||
- Integrate LangGraph into a React app: cloud/how-tos/use_stream_react.md
|
||||
- Implement generative UI with LangGraph: cloud/how-tos/generative_ui_react.md
|
||||
|
||||
- Additional resources:
|
||||
- additional-resources/index.md
|
||||
@@ -306,6 +305,7 @@ nav:
|
||||
- troubleshooting/errors/MULTIPLE_SUBGRAPHS.md
|
||||
- troubleshooting/errors/INVALID_CHAT_HISTORY.md
|
||||
- troubleshooting/errors/INVALID_LICENSE.md
|
||||
- LangGraph Studio: troubleshooting/studio.md
|
||||
|
||||
|
||||
markdown_extensions:
|
||||
@@ -383,4 +383,5 @@ extra_css:
|
||||
- stylesheets/version_admonitions.css
|
||||
- stylesheets/logos.css
|
||||
- stylesheets/sticky_navigation.css
|
||||
- stylesheets/agent_graph_widget.css
|
||||
- stylesheets/agent_graph_widget.css
|
||||
|
||||
|
||||
@@ -360,5 +360,5 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
{% endblock %}
|
||||
|
||||
{% block announce %}
|
||||
<strong>LangGraph Platform docs have moved!</strong> Find the LangGraph Platform docs at the new <a href="https://docs.langchain.com/langgraph-platform" target="_blank">LangChain Docs</a> site.
|
||||
Our <a href="https://academy.langchain.com/courses/ambient-agents/?utm_medium=internal&utm_source=docs&utm_campaign=q2-2025_ambient-agents_co" target="_blank">Building Ambient Agents with LangGraph</a> course is now available on LangChain Academy!
|
||||
{% endblock %}
|
||||
|
||||
@@ -39,7 +39,6 @@ docs = [
|
||||
"markdown-callouts",
|
||||
"markdown-include",
|
||||
"mkdocs-exclude",
|
||||
"mkdocs-exclude-search",
|
||||
"psycopg[binary]",
|
||||
"psycopg-pool",
|
||||
"pygments-ansi-color",
|
||||
|
||||
Generated
+2
-16
@@ -2337,7 +2337,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "0.6.2"
|
||||
version = "0.6.1"
|
||||
source = { editable = "../libs/langgraph" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -2524,7 +2524,6 @@ docs = [
|
||||
{ name = "markdown-include" },
|
||||
{ name = "mkdocs" },
|
||||
{ name = "mkdocs-exclude" },
|
||||
{ name = "mkdocs-exclude-search" },
|
||||
{ name = "mkdocs-git-committers-plugin-2" },
|
||||
{ name = "mkdocs-include-markdown-plugin" },
|
||||
{ name = "mkdocs-material", extra = ["imaging"] },
|
||||
@@ -2596,7 +2595,6 @@ docs = [
|
||||
{ name = "markdown-include" },
|
||||
{ name = "mkdocs" },
|
||||
{ name = "mkdocs-exclude" },
|
||||
{ name = "mkdocs-exclude-search" },
|
||||
{ name = "mkdocs-git-committers-plugin-2" },
|
||||
{ name = "mkdocs-include-markdown-plugin", specifier = ">=7.1.6" },
|
||||
{ name = "mkdocs-material", extras = ["imaging"] },
|
||||
@@ -2643,7 +2641,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "0.6.2"
|
||||
version = "0.6.1"
|
||||
source = { editable = "../libs/prebuilt" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -3032,18 +3030,6 @@ dependencies = [
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/b5/3a8e289282c9e8d7003f8a2f53d673d4fdaa81d493dc6966092d9985b6fc/mkdocs-exclude-1.0.2.tar.gz", hash = "sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51", size = 6751, upload-time = "2019-02-20T23:34:12.81Z" }
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-exclude-search"
|
||||
version = "0.6.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mkdocs" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/52/8243589d294cf6091c1145896915fe50feea0e91d64d843942d0175770c2/mkdocs-exclude-search-0.6.6.tar.gz", hash = "sha256:3cdff1b9afdc1b227019cd1e124f401453235b92153d60c0e5e651a76be4f044", size = 9501, upload-time = "2023-12-03T22:58:21.259Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ef/9af45ffb1bdba684a0694922abae0bb771e9777aba005933f838b7f1bcea/mkdocs_exclude_search-0.6.6-py3-none-any.whl", hash = "sha256:2b4b941d1689808db533fe4a6afba75ce76c9bab8b21d4e31efc05fd8c4e0a4f", size = 7821, upload-time = "2023-12-03T22:58:19.355Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-get-deps"
|
||||
version = "0.2.0"
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "b3cec425",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/human_in_the_loop/dynamic_breakpoints.ipynb"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "4876215f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/human_in_the_loop/edit-graph-state.ipynb"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.8"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "b162f1bd",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/human_in_the_loop/review-tool-calls.ipynb"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "84c5f6f1",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/human_in_the_loop/time-travel.ipynb"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "5eb637a4",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/multi_agent/agent_supervisor.ipynb"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
Generated
-1
@@ -329,7 +329,6 @@ dev = [
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
{ name = "pytest-watcher" },
|
||||
{ name = "redis" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
|
||||
Generated
-1
@@ -341,7 +341,6 @@ dev = [
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
{ name = "pytest-watcher" },
|
||||
{ name = "redis" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
|
||||
-144
@@ -1,144 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any
|
||||
|
||||
from langgraph.cache.base import BaseCache, FullKey, Namespace, ValueT
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
|
||||
|
||||
class RedisCache(BaseCache[ValueT]):
|
||||
"""Redis-based cache implementation with TTL support."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
redis: Any,
|
||||
*,
|
||||
serde: SerializerProtocol | None = None,
|
||||
prefix: str = "langgraph:cache:",
|
||||
) -> None:
|
||||
"""Initialize the cache with a Redis client.
|
||||
|
||||
Args:
|
||||
redis: Redis client instance (sync or async)
|
||||
serde: Serializer to use for values
|
||||
prefix: Key prefix for all cached values
|
||||
"""
|
||||
super().__init__(serde=serde)
|
||||
self.redis = redis
|
||||
self.prefix = prefix
|
||||
|
||||
def _make_key(self, ns: Namespace, key: str) -> str:
|
||||
"""Create a Redis key from namespace and key."""
|
||||
ns_str = ":".join(ns) if ns else ""
|
||||
return f"{self.prefix}{ns_str}:{key}" if ns_str else f"{self.prefix}{key}"
|
||||
|
||||
def _parse_key(self, redis_key: str) -> tuple[Namespace, str]:
|
||||
"""Parse a Redis key back to namespace and key."""
|
||||
if not redis_key.startswith(self.prefix):
|
||||
raise ValueError(
|
||||
f"Key {redis_key} does not start with prefix {self.prefix}"
|
||||
)
|
||||
|
||||
remaining = redis_key[len(self.prefix) :]
|
||||
if ":" in remaining:
|
||||
parts = remaining.split(":")
|
||||
key = parts[-1]
|
||||
ns_parts = parts[:-1]
|
||||
return (tuple(ns_parts), key)
|
||||
else:
|
||||
return (tuple(), remaining)
|
||||
|
||||
def get(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
|
||||
"""Get the cached values for the given keys."""
|
||||
if not keys:
|
||||
return {}
|
||||
|
||||
# Build Redis keys
|
||||
redis_keys = [self._make_key(ns, key) for ns, key in keys]
|
||||
|
||||
# Get values from Redis using MGET
|
||||
try:
|
||||
raw_values = self.redis.mget(redis_keys)
|
||||
except Exception:
|
||||
# If Redis is unavailable, return empty dict
|
||||
return {}
|
||||
|
||||
values: dict[FullKey, ValueT] = {}
|
||||
for i, raw_value in enumerate(raw_values):
|
||||
if raw_value is not None:
|
||||
try:
|
||||
# Deserialize the value
|
||||
encoding, data = raw_value.split(b":", 1)
|
||||
values[keys[i]] = self.serde.loads_typed((encoding.decode(), data))
|
||||
except Exception:
|
||||
# Skip corrupted entries
|
||||
continue
|
||||
|
||||
return values
|
||||
|
||||
async def aget(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
|
||||
"""Asynchronously get the cached values for the given keys."""
|
||||
return self.get(keys)
|
||||
|
||||
def set(self, mapping: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
|
||||
"""Set the cached values for the given keys and TTLs."""
|
||||
if not mapping:
|
||||
return
|
||||
|
||||
# Use pipeline for efficient batch operations
|
||||
pipe = self.redis.pipeline()
|
||||
|
||||
for (ns, key), (value, ttl) in mapping.items():
|
||||
redis_key = self._make_key(ns, key)
|
||||
encoding, data = self.serde.dumps_typed(value)
|
||||
|
||||
# Store as "encoding:data" format
|
||||
serialized_value = f"{encoding}:".encode() + data
|
||||
|
||||
if ttl is not None:
|
||||
pipe.setex(redis_key, ttl, serialized_value)
|
||||
else:
|
||||
pipe.set(redis_key, serialized_value)
|
||||
|
||||
try:
|
||||
pipe.execute()
|
||||
except Exception:
|
||||
# Silently fail if Redis is unavailable
|
||||
pass
|
||||
|
||||
async def aset(self, mapping: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
|
||||
"""Asynchronously set the cached values for the given keys and TTLs."""
|
||||
self.set(mapping)
|
||||
|
||||
def clear(self, namespaces: Sequence[Namespace] | None = None) -> None:
|
||||
"""Delete the cached values for the given namespaces.
|
||||
If no namespaces are provided, clear all cached values."""
|
||||
try:
|
||||
if namespaces is None:
|
||||
# Clear all keys with our prefix
|
||||
pattern = f"{self.prefix}*"
|
||||
keys = self.redis.keys(pattern)
|
||||
if keys:
|
||||
self.redis.delete(*keys)
|
||||
else:
|
||||
# Clear specific namespaces
|
||||
keys_to_delete = []
|
||||
for ns in namespaces:
|
||||
ns_str = ":".join(ns) if ns else ""
|
||||
pattern = (
|
||||
f"{self.prefix}{ns_str}:*" if ns_str else f"{self.prefix}*"
|
||||
)
|
||||
keys = self.redis.keys(pattern)
|
||||
keys_to_delete.extend(keys)
|
||||
|
||||
if keys_to_delete:
|
||||
self.redis.delete(*keys_to_delete)
|
||||
except Exception:
|
||||
# Silently fail if Redis is unavailable
|
||||
pass
|
||||
|
||||
async def aclear(self, namespaces: Sequence[Namespace] | None = None) -> None:
|
||||
"""Asynchronously delete the cached values for the given namespaces.
|
||||
If no namespaces are provided, clear all cached values."""
|
||||
self.clear(namespaces)
|
||||
@@ -81,9 +81,6 @@ class Checkpoint(TypedDict):
|
||||
This keeps track of the versions of the channels that each node has seen.
|
||||
Used to determine which nodes to execute next.
|
||||
"""
|
||||
updated_channels: list[str] | None
|
||||
"""The channels that were updated in this checkpoint.
|
||||
"""
|
||||
|
||||
|
||||
def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint:
|
||||
@@ -95,7 +92,6 @@ def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint:
|
||||
channel_versions=checkpoint["channel_versions"].copy(),
|
||||
versions_seen={k: v.copy() for k, v in checkpoint["versions_seen"].items()},
|
||||
pending_sends=checkpoint.get("pending_sends", []).copy(),
|
||||
updated_channels=checkpoint.get("updated_channels", None),
|
||||
)
|
||||
|
||||
|
||||
@@ -441,7 +437,6 @@ def empty_checkpoint() -> Checkpoint:
|
||||
channel_versions={},
|
||||
versions_seen={},
|
||||
pending_sends=[],
|
||||
updated_channels=None,
|
||||
)
|
||||
|
||||
|
||||
@@ -475,5 +470,4 @@ def create_checkpoint(
|
||||
channel_versions=checkpoint["channel_versions"],
|
||||
versions_seen=checkpoint["versions_seen"],
|
||||
pending_sends=checkpoint.get("pending_sends", []),
|
||||
updated_channels=None,
|
||||
)
|
||||
|
||||
@@ -64,21 +64,14 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
super().__init__()
|
||||
self._loop = asyncio.get_running_loop()
|
||||
self._aqueue: asyncio.Queue[tuple[asyncio.Future, Op]] = asyncio.Queue()
|
||||
self._task: asyncio.Task | None = None
|
||||
self._ensure_task()
|
||||
self._task = self._loop.create_task(_run(self._aqueue, weakref.ref(self)))
|
||||
|
||||
def __del__(self) -> None:
|
||||
try:
|
||||
if self._task:
|
||||
self._task.cancel()
|
||||
self._task.cancel()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def _ensure_task(self) -> None:
|
||||
"""Ensure the background processing loop is running."""
|
||||
if self._task is None or self._task.done():
|
||||
self._task = self._loop.create_task(_run(self._aqueue, weakref.ref(self)))
|
||||
|
||||
async def aget(
|
||||
self,
|
||||
namespace: tuple[str, ...],
|
||||
@@ -86,7 +79,7 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
*,
|
||||
refresh_ttl: bool | None = None,
|
||||
) -> Item | None:
|
||||
self._ensure_task()
|
||||
assert not self._task.done()
|
||||
fut = self._loop.create_future()
|
||||
self._aqueue.put_nowait(
|
||||
(
|
||||
@@ -111,7 +104,7 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
offset: int = 0,
|
||||
refresh_ttl: bool | None = None,
|
||||
) -> list[SearchItem]:
|
||||
self._ensure_task()
|
||||
assert not self._task.done()
|
||||
fut = self._loop.create_future()
|
||||
self._aqueue.put_nowait(
|
||||
(
|
||||
@@ -137,7 +130,7 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
*,
|
||||
ttl: float | None | NotProvided = NOT_PROVIDED,
|
||||
) -> None:
|
||||
self._ensure_task()
|
||||
assert not self._task.done()
|
||||
_validate_namespace(namespace)
|
||||
fut = self._loop.create_future()
|
||||
self._aqueue.put_nowait(
|
||||
@@ -155,7 +148,7 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
namespace: tuple[str, ...],
|
||||
key: str,
|
||||
) -> None:
|
||||
self._ensure_task()
|
||||
assert not self._task.done()
|
||||
fut = self._loop.create_future()
|
||||
self._aqueue.put_nowait((fut, PutOp(namespace, key, None)))
|
||||
return await fut
|
||||
@@ -169,7 +162,7 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
) -> list[tuple[str, ...]]:
|
||||
self._ensure_task()
|
||||
assert not self._task.done()
|
||||
fut = self._loop.create_future()
|
||||
match_conditions = []
|
||||
if prefix:
|
||||
|
||||
@@ -32,7 +32,6 @@ dev = [
|
||||
"numpy",
|
||||
"pandas",
|
||||
"pandas-stubs>=2.2.2.240807",
|
||||
"redis",
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
|
||||
@@ -1,313 +0,0 @@
|
||||
"""Unit tests for Redis cache implementation."""
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
import redis
|
||||
|
||||
from langgraph.cache.redis import RedisCache
|
||||
|
||||
|
||||
class TestRedisCache:
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
"""Set up test Redis client and cache."""
|
||||
self.client = redis.Redis(
|
||||
host="localhost", port=6379, db=0, decode_responses=False
|
||||
)
|
||||
try:
|
||||
self.client.ping()
|
||||
except redis.ConnectionError:
|
||||
pytest.skip("Redis server not available")
|
||||
|
||||
self.cache = RedisCache(self.client, prefix="test:cache:")
|
||||
|
||||
# Clean up before each test
|
||||
self.client.flushdb()
|
||||
|
||||
def teardown_method(self):
|
||||
"""Clean up after each test."""
|
||||
try:
|
||||
self.client.flushdb()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def test_basic_set_and_get(self):
|
||||
"""Test basic set and get operations."""
|
||||
keys = [(("graph", "node"), "key1")]
|
||||
values = {keys[0]: ({"result": 42}, None)}
|
||||
|
||||
# Set value
|
||||
self.cache.set(values)
|
||||
|
||||
# Get value
|
||||
result = self.cache.get(keys)
|
||||
assert len(result) == 1
|
||||
assert result[keys[0]] == {"result": 42}
|
||||
|
||||
def test_batch_operations(self):
|
||||
"""Test batch set and get operations."""
|
||||
keys = [
|
||||
(("graph", "node1"), "key1"),
|
||||
(("graph", "node2"), "key2"),
|
||||
(("other", "node"), "key3"),
|
||||
]
|
||||
values = {
|
||||
keys[0]: ({"result": 1}, None),
|
||||
keys[1]: ({"result": 2}, 60), # With TTL
|
||||
keys[2]: ({"result": 3}, None),
|
||||
}
|
||||
|
||||
# Set values
|
||||
self.cache.set(values)
|
||||
|
||||
# Get all values
|
||||
result = self.cache.get(keys)
|
||||
assert len(result) == 3
|
||||
assert result[keys[0]] == {"result": 1}
|
||||
assert result[keys[1]] == {"result": 2}
|
||||
assert result[keys[2]] == {"result": 3}
|
||||
|
||||
def test_ttl_behavior(self):
|
||||
"""Test TTL (time-to-live) functionality."""
|
||||
key = (("graph", "node"), "ttl_key")
|
||||
values = {key: ({"data": "expires_soon"}, 1)} # 1 second TTL
|
||||
|
||||
# Set with TTL
|
||||
self.cache.set(values)
|
||||
|
||||
# Should be available immediately
|
||||
result = self.cache.get([key])
|
||||
assert len(result) == 1
|
||||
assert result[key] == {"data": "expires_soon"}
|
||||
|
||||
# Wait for expiration
|
||||
time.sleep(1.1)
|
||||
|
||||
# Should be expired
|
||||
result = self.cache.get([key])
|
||||
assert len(result) == 0
|
||||
|
||||
def test_namespace_isolation(self):
|
||||
"""Test that different namespaces are isolated."""
|
||||
key1 = (("graph1", "node"), "same_key")
|
||||
key2 = (("graph2", "node"), "same_key")
|
||||
|
||||
values = {key1: ({"graph": 1}, None), key2: ({"graph": 2}, None)}
|
||||
|
||||
self.cache.set(values)
|
||||
|
||||
result = self.cache.get([key1, key2])
|
||||
assert result[key1] == {"graph": 1}
|
||||
assert result[key2] == {"graph": 2}
|
||||
|
||||
def test_clear_all(self):
|
||||
"""Test clearing all cached values."""
|
||||
keys = [(("graph", "node1"), "key1"), (("graph", "node2"), "key2")]
|
||||
values = {keys[0]: ({"result": 1}, None), keys[1]: ({"result": 2}, None)}
|
||||
|
||||
self.cache.set(values)
|
||||
|
||||
# Verify data exists
|
||||
result = self.cache.get(keys)
|
||||
assert len(result) == 2
|
||||
|
||||
# Clear all
|
||||
self.cache.clear()
|
||||
|
||||
# Verify data is gone
|
||||
result = self.cache.get(keys)
|
||||
assert len(result) == 0
|
||||
|
||||
def test_clear_by_namespace(self):
|
||||
"""Test clearing cached values by namespace."""
|
||||
keys = [
|
||||
(("graph1", "node"), "key1"),
|
||||
(("graph2", "node"), "key2"),
|
||||
(("graph1", "other"), "key3"),
|
||||
]
|
||||
values = {
|
||||
keys[0]: ({"result": 1}, None),
|
||||
keys[1]: ({"result": 2}, None),
|
||||
keys[2]: ({"result": 3}, None),
|
||||
}
|
||||
|
||||
self.cache.set(values)
|
||||
|
||||
# Clear only graph1 namespace
|
||||
self.cache.clear([("graph1", "node"), ("graph1", "other")])
|
||||
|
||||
# graph1 should be cleared, graph2 should remain
|
||||
result = self.cache.get(keys)
|
||||
assert len(result) == 1
|
||||
assert result[keys[1]] == {"result": 2}
|
||||
|
||||
def test_empty_operations(self):
|
||||
"""Test behavior with empty keys/values."""
|
||||
# Empty get
|
||||
result = self.cache.get([])
|
||||
assert result == {}
|
||||
|
||||
# Empty set
|
||||
self.cache.set({}) # Should not raise error
|
||||
|
||||
def test_nonexistent_keys(self):
|
||||
"""Test getting keys that don't exist."""
|
||||
keys = [(("graph", "node"), "nonexistent")]
|
||||
result = self.cache.get(keys)
|
||||
assert len(result) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_operations(self):
|
||||
"""Test async set and get operations with sync Redis client."""
|
||||
# Create sync Redis client and cache (like main integration tests)
|
||||
client = redis.Redis(
|
||||
host="localhost", port=6379, db=1, decode_responses=False
|
||||
)
|
||||
try:
|
||||
client.ping()
|
||||
except Exception:
|
||||
pytest.skip("Redis not available")
|
||||
|
||||
cache = RedisCache(client, prefix="test:async:")
|
||||
|
||||
keys = [(("graph", "node"), "async_key")]
|
||||
values = {keys[0]: ({"async": True}, None)}
|
||||
|
||||
# Async set (delegates to sync)
|
||||
await cache.aset(values)
|
||||
|
||||
# Async get (delegates to sync)
|
||||
result = await cache.aget(keys)
|
||||
assert len(result) == 1
|
||||
assert result[keys[0]] == {"async": True}
|
||||
|
||||
# Cleanup
|
||||
client.flushdb()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_clear(self):
|
||||
"""Test async clear operations with sync Redis client."""
|
||||
# Create sync Redis client and cache (like main integration tests)
|
||||
client = redis.Redis(
|
||||
host="localhost", port=6379, db=1, decode_responses=False
|
||||
)
|
||||
try:
|
||||
client.ping()
|
||||
except Exception:
|
||||
pytest.skip("Redis not available")
|
||||
|
||||
cache = RedisCache(client, prefix="test:async:")
|
||||
|
||||
keys = [(("graph", "node"), "key")]
|
||||
values = {keys[0]: ({"data": "test"}, None)}
|
||||
|
||||
await cache.aset(values)
|
||||
|
||||
# Verify data exists
|
||||
result = await cache.aget(keys)
|
||||
assert len(result) == 1
|
||||
|
||||
# Clear all (delegates to sync)
|
||||
await cache.aclear()
|
||||
|
||||
# Verify data is gone
|
||||
result = await cache.aget(keys)
|
||||
assert len(result) == 0
|
||||
|
||||
# Cleanup
|
||||
client.flushdb()
|
||||
|
||||
def test_redis_unavailable_get(self):
|
||||
"""Test behavior when Redis is unavailable during get operations."""
|
||||
# Create cache with non-existent Redis server
|
||||
bad_client = redis.Redis(
|
||||
host="nonexistent", port=9999, socket_connect_timeout=0.1
|
||||
)
|
||||
cache = RedisCache(bad_client, prefix="test:cache:")
|
||||
|
||||
keys = [(("graph", "node"), "key")]
|
||||
result = cache.get(keys)
|
||||
|
||||
# Should return empty dict when Redis unavailable
|
||||
assert result == {}
|
||||
|
||||
def test_redis_unavailable_set(self):
|
||||
"""Test behavior when Redis is unavailable during set operations."""
|
||||
# Create cache with non-existent Redis server
|
||||
bad_client = redis.Redis(
|
||||
host="nonexistent", port=9999, socket_connect_timeout=0.1
|
||||
)
|
||||
cache = RedisCache(bad_client, prefix="test:cache:")
|
||||
|
||||
keys = [(("graph", "node"), "key")]
|
||||
values = {keys[0]: ({"data": "test"}, None)}
|
||||
|
||||
# Should not raise exception when Redis unavailable
|
||||
cache.set(values) # Should silently fail
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redis_unavailable_async(self):
|
||||
"""Test async behavior when Redis is unavailable."""
|
||||
# Create sync cache with non-existent Redis server (like main integration tests)
|
||||
bad_client = redis.Redis(
|
||||
host="nonexistent", port=9999, socket_connect_timeout=0.1
|
||||
)
|
||||
cache = RedisCache(bad_client, prefix="test:cache:")
|
||||
|
||||
keys = [(("graph", "node"), "key")]
|
||||
values = {keys[0]: ({"data": "test"}, None)}
|
||||
|
||||
# Should return empty dict for get (delegates to sync)
|
||||
result = await cache.aget(keys)
|
||||
assert result == {}
|
||||
|
||||
# Should not raise exception for set (delegates to sync)
|
||||
await cache.aset(values) # Should silently fail
|
||||
|
||||
def test_corrupted_data_handling(self):
|
||||
"""Test handling of corrupted data in Redis."""
|
||||
# Set some valid data first
|
||||
keys = [(("graph", "node"), "valid_key")]
|
||||
values = {keys[0]: ({"data": "valid"}, None)}
|
||||
self.cache.set(values)
|
||||
|
||||
# Manually insert corrupted data
|
||||
corrupted_key = self.cache._make_key(("graph", "node"), "corrupted_key")
|
||||
self.client.set(corrupted_key, b"invalid:data:format:too:many:colons")
|
||||
|
||||
# Should skip corrupted entry and return only valid ones
|
||||
all_keys = [keys[0], (("graph", "node"), "corrupted_key")]
|
||||
result = self.cache.get(all_keys)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[keys[0]] == {"data": "valid"}
|
||||
|
||||
def test_key_parsing_edge_cases(self):
|
||||
"""Test key parsing with edge cases."""
|
||||
# Test empty namespace
|
||||
key1 = ((), "empty_ns")
|
||||
values = {key1: ({"data": "empty_ns"}, None)}
|
||||
self.cache.set(values)
|
||||
result = self.cache.get([key1])
|
||||
assert result[key1] == {"data": "empty_ns"}
|
||||
|
||||
# Test namespace with special characters
|
||||
key2 = (("graph:with:colons", "node-with-dashes"), "key_with_underscores")
|
||||
values = {key2: ({"data": "special_chars"}, None)}
|
||||
self.cache.set(values)
|
||||
result = self.cache.get([key2])
|
||||
assert result[key2] == {"data": "special_chars"}
|
||||
|
||||
def test_large_data_serialization(self):
|
||||
"""Test handling of large data objects."""
|
||||
# Create a large data structure
|
||||
large_data = {"large_list": list(range(1000)), "nested": {"data": "x" * 1000}}
|
||||
key = (("graph", "node"), "large_key")
|
||||
values = {key: (large_data, None)}
|
||||
|
||||
self.cache.set(values)
|
||||
result = self.cache.get([key])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[key] == large_data
|
||||
@@ -34,42 +34,6 @@ class MockAsyncBatchedStore(AsyncBatchedBaseStore):
|
||||
return self._store.batch(ops)
|
||||
|
||||
|
||||
async def test_async_batch_store_resilience() -> None:
|
||||
"""Test that AsyncBatchedBaseStore recovers gracefully from task cancellation."""
|
||||
doc = {"foo": "bar"}
|
||||
async_store = MockAsyncBatchedStore()
|
||||
|
||||
await async_store.aput(("foo", "langgraph", "foo"), "bar", doc)
|
||||
|
||||
# Store the original task reference
|
||||
original_task = async_store._task
|
||||
assert original_task is not None
|
||||
assert not original_task.done()
|
||||
|
||||
# Cancel the background task
|
||||
original_task.cancel()
|
||||
await asyncio.sleep(0.01)
|
||||
assert original_task.cancelled()
|
||||
|
||||
# Perform a new operation - this should trigger _ensure_task() to create a new task
|
||||
result = await async_store.asearch(("foo", "langgraph", "foo"))
|
||||
assert len(result) > 0
|
||||
assert result[0].value == doc
|
||||
|
||||
# Verify a new task was created
|
||||
new_task = async_store._task
|
||||
assert new_task is not None
|
||||
assert new_task is not original_task
|
||||
assert not new_task.done()
|
||||
|
||||
# Test that operations continue to work with the new task
|
||||
doc2 = {"baz": "qux"}
|
||||
await async_store.aput(("test", "namespace"), "key", doc2)
|
||||
result2 = await async_store.aget(("test", "namespace"), "key")
|
||||
assert result2 is not None
|
||||
assert result2.value == doc2
|
||||
|
||||
|
||||
def test_get_text_at_path() -> None:
|
||||
nested_data = {
|
||||
"name": "test",
|
||||
|
||||
Generated
-23
@@ -32,15 +32,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-timeout"
|
||||
version = "5.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.7.9"
|
||||
@@ -354,7 +345,6 @@ dev = [
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
{ name = "pytest-watcher" },
|
||||
{ name = "redis" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
@@ -376,7 +366,6 @@ dev = [
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
{ name = "pytest-watcher" },
|
||||
{ name = "redis" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
@@ -1164,18 +1153,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "6.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "async-timeout", marker = "python_full_version < '3.11.3'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/cd/030274634a1a052b708756016283ea3d84e91ae45f74d7f5dcf55d753a0f/redis-6.3.0.tar.gz", hash = "sha256:3000dbe532babfb0999cdab7b3e5744bcb23e51923febcfaeb52c8cfb29632ef", size = 4647275, upload-time = "2025-08-05T08:12:31.648Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/a7/2fe45801534a187543fc45d28b3844d84559c1589255bc2ece30d92dc205/redis-6.3.0-py3-none-any.whl", hash = "sha256:92f079d656ded871535e099080f70fab8e75273c0236797126ac60242d638e9b", size = 280018, upload-time = "2025-08-05T08:12:30.093Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.4"
|
||||
|
||||
+10
-10
@@ -37,11 +37,11 @@ coverage:
|
||||
--cov-report xml \
|
||||
--cov-report term-missing:skip-covered
|
||||
|
||||
start-services:
|
||||
docker compose -f tests/compose-postgres.yml -f tests/compose-redis.yml up -V --force-recreate --wait --remove-orphans
|
||||
start-postgres:
|
||||
docker compose -f tests/compose-postgres.yml up -V --force-recreate --wait --remove-orphans
|
||||
|
||||
stop-services:
|
||||
docker compose -f tests/compose-postgres.yml -f tests/compose-redis.yml down -v
|
||||
stop-postgres:
|
||||
docker compose -f tests/compose-postgres.yml down -v
|
||||
|
||||
start-dev-server:
|
||||
LOG_LEVEL=warning uv run langgraph dev --config tests/example_app/langgraph.json --no-browser & echo "$$!" > .devserver.pid
|
||||
@@ -60,11 +60,11 @@ NO_DOCKER ?= $(sh command -v docker >/dev/null 2>&1 && echo "false" || echo "tru
|
||||
|
||||
test:
|
||||
if [ "$(NO_DOCKER)" = "false" ]; then \
|
||||
make start-services &&\
|
||||
make start-postgres &&\
|
||||
make start-dev-server &&\
|
||||
uv run pytest $(TEST); \
|
||||
EXIT_CODE=$$?; \
|
||||
make stop-services; \
|
||||
make stop-postgres; \
|
||||
make stop-dev-server; \
|
||||
exit $$EXIT_CODE; \
|
||||
else \
|
||||
@@ -74,11 +74,11 @@ test:
|
||||
fi
|
||||
|
||||
test_parallel:
|
||||
make start-services &&\
|
||||
make start-postgres &&\
|
||||
make start-dev-server &&\
|
||||
uv run pytest -n auto --dist worksteal $(TEST); \
|
||||
EXIT_CODE=$$?; \
|
||||
make stop-services; \
|
||||
make stop-postgres; \
|
||||
make stop-dev-server; \
|
||||
exit $$EXIT_CODE
|
||||
|
||||
@@ -93,11 +93,11 @@ MAXFAIL_ARGS := $(if $(MAXFAIL),--maxfail $(MAXFAIL),)
|
||||
XDIST_ARGS := $(if $(WORKERS),-x $(XDIST_ARGS),)
|
||||
|
||||
test_watch:
|
||||
make start-services &&\
|
||||
make start-postgres &&\
|
||||
make start-dev-server &&\
|
||||
uv run ptw . -- --ff -vv $(XDIST_ARGS) $(MAXFAIL_ARGS) $(TEST); \
|
||||
EXIT_CODE=$$?; \
|
||||
make stop-services; \
|
||||
make stop-postgres; \
|
||||
make stop-dev-server; \
|
||||
exit $$EXIT_CODE
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import asyncio
|
||||
import enum
|
||||
import inspect
|
||||
import sys
|
||||
import warnings
|
||||
from collections.abc import (
|
||||
AsyncIterator,
|
||||
Awaitable,
|
||||
@@ -304,16 +303,6 @@ class RunnableCallable(Runnable):
|
||||
if typ != (ANY_TYPE,) and p.annotation not in typ:
|
||||
# A specific type is required, but the function annotation does
|
||||
# not match the expected type.
|
||||
|
||||
# If this is a config parameter with incorrect typing, emit a warning
|
||||
# because we used to support any type but are moving towards more correct typing
|
||||
if kw == "config" and p.annotation != inspect.Parameter.empty:
|
||||
warnings.warn(
|
||||
f"The 'config' parameter should be typed as 'RunnableConfig' or "
|
||||
f"'RunnableConfig | None', not '{p.annotation}'. ",
|
||||
UserWarning,
|
||||
stacklevel=4,
|
||||
)
|
||||
continue
|
||||
|
||||
# If the kwarg is accepted by the function, store the key / runtime attribute to inject
|
||||
|
||||
@@ -91,7 +91,7 @@ class GraphInterrupt(GraphBubbleUp):
|
||||
|
||||
@deprecated(
|
||||
"NodeInterrupt is deprecated. Please use `langgraph.types.interrupt` instead.",
|
||||
category=None,
|
||||
stacklevel=2,
|
||||
)
|
||||
class NodeInterrupt(GraphInterrupt):
|
||||
"""Raised by a node to interrupt execution.
|
||||
|
||||
@@ -335,10 +335,7 @@ class entrypoint(Generic[ContextT]):
|
||||
of the previous invocation on the same thread id.
|
||||
|
||||
```python
|
||||
from typing import Optional
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.func import entrypoint
|
||||
|
||||
@entrypoint(checkpointer=InMemorySaver())
|
||||
@@ -350,7 +347,7 @@ class entrypoint(Generic[ContextT]):
|
||||
"thread_id": "some_thread"
|
||||
}
|
||||
}
|
||||
my_workflow.invoke("hello", config)
|
||||
my_workflow.invoke("hello")
|
||||
```
|
||||
|
||||
Example: Using entrypoint.final to save a value
|
||||
@@ -360,10 +357,7 @@ class entrypoint(Generic[ContextT]):
|
||||
long as the same thread id is used.
|
||||
|
||||
```python
|
||||
from typing import Any
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.func import entrypoint
|
||||
|
||||
@entrypoint(checkpointer=InMemorySaver())
|
||||
|
||||
@@ -41,16 +41,16 @@ _Writer = Callable[
|
||||
|
||||
|
||||
def _get_branch_path_input_schema(
|
||||
path: Callable[..., Hashable | Sequence[Hashable]]
|
||||
| Callable[..., Awaitable[Hashable | Sequence[Hashable]]]
|
||||
| Runnable[Any, Hashable | Sequence[Hashable]],
|
||||
path: Callable[..., Hashable | list[Hashable]]
|
||||
| Callable[..., Awaitable[Hashable | list[Hashable]]]
|
||||
| Runnable[Any, Hashable | list[Hashable]],
|
||||
) -> type[Any] | None:
|
||||
input = None
|
||||
# detect input schema annotation in the branch callable
|
||||
try:
|
||||
callable_: (
|
||||
Callable[..., Hashable | Sequence[Hashable]]
|
||||
| Callable[..., Awaitable[Hashable | Sequence[Hashable]]]
|
||||
Callable[..., Hashable | list[Hashable]]
|
||||
| Callable[..., Awaitable[Hashable | list[Hashable]]]
|
||||
| None
|
||||
) = None
|
||||
if isinstance(path, (RunnableCallable, RunnableLambda)):
|
||||
|
||||
@@ -22,11 +22,10 @@ from langchain_core.messages import (
|
||||
convert_to_messages,
|
||||
message_chunk_to_message,
|
||||
)
|
||||
from typing_extensions import TypedDict, deprecated
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph._internal._constants import CONF, CONFIG_KEY_SEND, NS_SEP
|
||||
from langgraph.graph.state import StateGraph
|
||||
from langgraph.warnings import LangGraphDeprecatedSinceV10
|
||||
|
||||
__all__ = (
|
||||
"add_messages",
|
||||
@@ -234,16 +233,9 @@ def add_messages(
|
||||
return merged
|
||||
|
||||
|
||||
@deprecated(
|
||||
"MessageGraph is deprecated in LangGraph v1.0.0, to be removed in v2.0.0. Please use StateGraph with a `messages` key instead.",
|
||||
category=None,
|
||||
)
|
||||
class MessageGraph(StateGraph):
|
||||
"""A StateGraph where every node receives a list of messages as input and returns one or more messages as output.
|
||||
|
||||
!!! warning "Deprecation"
|
||||
MessageGraph is deprecated in LangGraph v1.0.0, to be removed in v2.0.0. Please use StateGraph with a `messages` key instead.
|
||||
|
||||
MessageGraph is a subclass of StateGraph whose entire state is a single, append-only* list of messages.
|
||||
Each node in a MessageGraph takes a list of messages as input and returns zero or more
|
||||
messages as output. The `add_messages` function is used to merge the output messages from each node
|
||||
@@ -289,11 +281,6 @@ class MessageGraph(StateGraph):
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
warnings.warn(
|
||||
"MessageGraph is deprecated in LangGraph v1.0.0, to be removed in v2.0.0. Please use StateGraph with a `messages` key instead.",
|
||||
category=LangGraphDeprecatedSinceV10,
|
||||
stacklevel=2,
|
||||
)
|
||||
super().__init__(Annotated[list[AnyMessage], add_messages]) # type: ignore[arg-type]
|
||||
|
||||
|
||||
|
||||
@@ -607,9 +607,9 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
||||
def add_conditional_edges(
|
||||
self,
|
||||
source: str,
|
||||
path: Callable[..., Hashable | Sequence[Hashable]]
|
||||
| Callable[..., Awaitable[Hashable | Sequence[Hashable]]]
|
||||
| Runnable[Any, Hashable | Sequence[Hashable]],
|
||||
path: Callable[..., Hashable | list[Hashable]]
|
||||
| Callable[..., Awaitable[Hashable | list[Hashable]]]
|
||||
| Runnable[Any, Hashable | list[Hashable]],
|
||||
path_map: dict[Hashable, str] | list[str] | None = None,
|
||||
) -> Self:
|
||||
"""Add a conditional edge from the starting node to any number of destination nodes.
|
||||
@@ -710,9 +710,9 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
||||
|
||||
def set_conditional_entry_point(
|
||||
self,
|
||||
path: Callable[..., Hashable | Sequence[Hashable]]
|
||||
| Callable[..., Awaitable[Hashable | Sequence[Hashable]]]
|
||||
| Runnable[Any, Hashable | Sequence[Hashable]],
|
||||
path: Callable[..., Hashable | list[Hashable]]
|
||||
| Callable[..., Awaitable[Hashable | list[Hashable]]]
|
||||
| Runnable[Any, Hashable | list[Hashable]],
|
||||
path_map: dict[Hashable, str] | list[str] | None = None,
|
||||
) -> Self:
|
||||
"""Sets a conditional entry point in the graph.
|
||||
@@ -1390,14 +1390,6 @@ def _is_field_managed_value(name: str, typ: type[Any]) -> ManagedValueSpec | Non
|
||||
if is_managed_value(decoration):
|
||||
return decoration
|
||||
|
||||
# Handle Required, NotRequired, etc wrapped types by extracting the inner type
|
||||
if (
|
||||
get_origin(typ) is not None
|
||||
and (args := get_args(typ))
|
||||
and (inner_type := args[0])
|
||||
):
|
||||
return _is_field_managed_value(name, inner_type)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import (
|
||||
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
from langgraph._internal._scratchpad import PregelScratchpad
|
||||
from langgraph.pregel._scratchpad import PregelScratchpad
|
||||
|
||||
V = TypeVar("V")
|
||||
U = TypeVar("U")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Annotated
|
||||
|
||||
from langgraph._internal._scratchpad import PregelScratchpad
|
||||
from langgraph.managed.base import ManagedValue
|
||||
from langgraph.pregel._scratchpad import PregelScratchpad
|
||||
|
||||
__all__ = ("IsLastStep", "RemainingStepsManager")
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ from langgraph._internal._constants import (
|
||||
RETURN,
|
||||
TASKS,
|
||||
)
|
||||
from langgraph._internal._scratchpad import PregelScratchpad
|
||||
from langgraph._internal._typing import EMPTY_SEQ, MISSING
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.channels.topic import Topic
|
||||
@@ -70,6 +69,7 @@ from langgraph.pregel._call import get_runnable_for_task, identifier
|
||||
from langgraph.pregel._io import read_channels
|
||||
from langgraph.pregel._log import logger
|
||||
from langgraph.pregel._read import INPUT_CACHE_KEY_TYPE, PregelNode
|
||||
from langgraph.pregel._scratchpad import PregelScratchpad
|
||||
from langgraph.runtime import DEFAULT_RUNTIME, Runtime
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import (
|
||||
|
||||
@@ -29,7 +29,6 @@ def create_checkpoint(
|
||||
step: int,
|
||||
*,
|
||||
id: str | None = None,
|
||||
updated_channels: set[str] | None = None,
|
||||
) -> Checkpoint:
|
||||
"""Create a checkpoint for the given channels."""
|
||||
ts = datetime.now(timezone.utc).isoformat()
|
||||
@@ -50,7 +49,6 @@ def create_checkpoint(
|
||||
channel_values=values,
|
||||
channel_versions=checkpoint["channel_versions"],
|
||||
versions_seen=checkpoint["versions_seen"],
|
||||
updated_channels=None if updated_channels is None else sorted(updated_channels),
|
||||
)
|
||||
|
||||
|
||||
@@ -83,5 +81,4 @@ def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint:
|
||||
channel_values=checkpoint["channel_values"].copy(),
|
||||
channel_versions=checkpoint["channel_versions"].copy(),
|
||||
versions_seen={k: v.copy() for k, v in checkpoint["versions_seen"].items()},
|
||||
updated_channels=checkpoint.get("updated_channels", None),
|
||||
)
|
||||
|
||||
@@ -48,7 +48,6 @@ from langgraph._internal._constants import (
|
||||
PUSH,
|
||||
RESUME,
|
||||
)
|
||||
from langgraph._internal._scratchpad import PregelScratchpad
|
||||
from langgraph._internal._typing import EMPTY_SEQ, MISSING
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.channels.base import BaseChannel
|
||||
@@ -101,6 +100,7 @@ from langgraph.pregel._io import (
|
||||
read_channels,
|
||||
)
|
||||
from langgraph.pregel._read import PregelNode
|
||||
from langgraph.pregel._scratchpad import PregelScratchpad
|
||||
from langgraph.pregel._utils import get_new_channel_versions, is_xxh3_128_hexdigest
|
||||
from langgraph.pregel.debug import (
|
||||
map_debug_checkpoint,
|
||||
@@ -568,9 +568,7 @@ class PregelLoop:
|
||||
if task := tasks.get(tid):
|
||||
task.writes.append((k, v))
|
||||
|
||||
def _first(
|
||||
self, *, input_keys: str | Sequence[str], updated_channels: set[str] | None
|
||||
) -> set[str] | None:
|
||||
def _first(self, *, input_keys: str | Sequence[str]) -> set[str] | None:
|
||||
# resuming from previous checkpoint requires
|
||||
# - finding a previous checkpoint
|
||||
# - receiving None input (outer graph) or RESUMING flag (subgraph)
|
||||
@@ -587,6 +585,8 @@ class PregelLoop:
|
||||
),
|
||||
)
|
||||
)
|
||||
# this can be set only when there are input_writes
|
||||
updated_channels: set[str] | None = None
|
||||
|
||||
# map command to writes
|
||||
if isinstance(self.input, Command):
|
||||
@@ -614,15 +614,13 @@ class PregelLoop:
|
||||
if null_writes := [
|
||||
w[1:] for w in self.checkpoint_pending_writes if w[0] == NULL_TASK_ID
|
||||
]:
|
||||
null_updated_channels = apply_writes(
|
||||
apply_writes(
|
||||
self.checkpoint,
|
||||
self.channels,
|
||||
[PregelTaskWrites((), INPUT, null_writes, [])],
|
||||
self.checkpointer_get_next_version,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
if updated_channels is not None:
|
||||
updated_channels.update(null_updated_channels)
|
||||
# proceed past previous checkpoint
|
||||
if is_resuming:
|
||||
self.checkpoint["versions_seen"].setdefault(INTERRUPT, {})
|
||||
@@ -650,7 +648,6 @@ class PregelLoop:
|
||||
store=None,
|
||||
checkpointer=None,
|
||||
manager=None,
|
||||
updated_channels=updated_channels,
|
||||
)
|
||||
# apply input writes
|
||||
updated_channels = apply_writes(
|
||||
@@ -664,7 +661,6 @@ class PregelLoop:
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
# save input checkpoint
|
||||
self.updated_channels = updated_channels
|
||||
self._put_checkpoint({"source": "input"})
|
||||
elif CONFIG_KEY_RESUMING not in configurable:
|
||||
raise EmptyInputError(f"Received no input for {input_keys}")
|
||||
@@ -697,7 +693,6 @@ class PregelLoop:
|
||||
self.channels if do_checkpoint else None,
|
||||
self.step,
|
||||
id=self.checkpoint["id"] if exiting else None,
|
||||
updated_channels=self.updated_channels,
|
||||
)
|
||||
# bail if no checkpointer
|
||||
if do_checkpoint and self._checkpointer_put_after_previous is not None:
|
||||
@@ -1041,12 +1036,7 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
||||
self.step = self.checkpoint_metadata["step"] + 1
|
||||
self.stop = self.step + self.config["recursion_limit"] + 1
|
||||
self.checkpoint_previous_versions = self.checkpoint["channel_versions"].copy()
|
||||
self.updated_channels = self._first(
|
||||
input_keys=self.input_keys,
|
||||
updated_channels=set(self.checkpoint.get("updated_channels")) # type: ignore[arg-type]
|
||||
if self.checkpoint.get("updated_channels")
|
||||
else None,
|
||||
)
|
||||
self.updated_channels = self._first(input_keys=self.input_keys)
|
||||
|
||||
return self
|
||||
|
||||
@@ -1222,12 +1212,7 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
||||
self.step = self.checkpoint_metadata["step"] + 1
|
||||
self.stop = self.step + self.config["recursion_limit"] + 1
|
||||
self.checkpoint_previous_versions = self.checkpoint["channel_versions"].copy()
|
||||
self.updated_channels = self._first(
|
||||
input_keys=self.input_keys,
|
||||
updated_channels=set(self.checkpoint.get("updated_channels")) # type: ignore[arg-type]
|
||||
if self.checkpoint.get("updated_channels")
|
||||
else None,
|
||||
)
|
||||
self.updated_channels = self._first(input_keys=self.input_keys)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
@@ -29,51 +29,16 @@ Meta = tuple[tuple[str, ...], dict[str, Any]]
|
||||
|
||||
class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
|
||||
"""A callback handler that implements stream_mode=messages.
|
||||
|
||||
Collects messages from:
|
||||
(1) chat model stream events; and
|
||||
(2) node outputs.
|
||||
"""
|
||||
Collects messages from (1) chat model stream events and (2) node outputs."""
|
||||
|
||||
run_inline = True
|
||||
"""We want this callback to run in the main thread to avoid order/locking issues."""
|
||||
"""We want this callback to run in the main thread, to avoid order/locking issues."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
stream: Callable[[StreamChunk], None],
|
||||
subgraphs: bool,
|
||||
*,
|
||||
parent_ns: tuple[str, ...] | None = None,
|
||||
) -> None:
|
||||
"""Configure the handler to stream messages from LLMs and nodes.
|
||||
|
||||
Args:
|
||||
stream: A callable that takes a StreamChunk and emits it.
|
||||
subgraphs: Whether to emit messages from subgraphs.
|
||||
parent_ns: The namespace where the handler was created.
|
||||
We keep track of this namespace to allow calls to subgraphs that
|
||||
were explicitly requested as a stream with `messages` mode
|
||||
configured.
|
||||
|
||||
Example:
|
||||
parent_ns is used to handle scenarios where the subgraph is explicitly
|
||||
streamed with `stream_mode="messages"`.
|
||||
|
||||
```python
|
||||
def parent_graph_node():
|
||||
# This node is in the parent graph.
|
||||
async for event in some_subgraph(..., stream_mode="messages"):
|
||||
do something with event # <-- these events will be emitted
|
||||
return ...
|
||||
|
||||
parent_graph.invoke(subgraphs=False)
|
||||
```
|
||||
"""
|
||||
def __init__(self, stream: Callable[[StreamChunk], None], subgraphs: bool):
|
||||
self.stream = stream
|
||||
self.subgraphs = subgraphs
|
||||
self.metadata: dict[UUID, Meta] = {}
|
||||
self.seen: set[int | str] = set()
|
||||
self.parent_ns = parent_ns
|
||||
|
||||
def _emit(self, meta: Meta, message: BaseMessage, *, dedupe: bool = False) -> None:
|
||||
if dedupe and message.id in self.seen:
|
||||
@@ -135,7 +100,7 @@ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
|
||||
ns = tuple(cast(str, metadata["langgraph_checkpoint_ns"]).split(NS_SEP))[
|
||||
:-1
|
||||
]
|
||||
if not self.subgraphs and len(ns) > 0 and ns != self.parent_ns:
|
||||
if not self.subgraphs and len(ns) > 0:
|
||||
return
|
||||
if tags:
|
||||
if filtered_tags := [t for t in tags if not t.startswith("seq:step")]:
|
||||
|
||||
@@ -30,13 +30,13 @@ from langgraph._internal._constants import (
|
||||
RETURN,
|
||||
)
|
||||
from langgraph._internal._future import chain_future, run_coroutine_threadsafe
|
||||
from langgraph._internal._scratchpad import PregelScratchpad
|
||||
from langgraph._internal._typing import MISSING
|
||||
from langgraph.constants import TAG_HIDDEN
|
||||
from langgraph.errors import GraphBubbleUp, GraphInterrupt
|
||||
from langgraph.pregel._algo import Call
|
||||
from langgraph.pregel._executor import Submit
|
||||
from langgraph.pregel._retry import arun_with_retry, run_with_retry
|
||||
from langgraph.pregel._scratchpad import PregelScratchpad
|
||||
from langgraph.types import (
|
||||
CachePolicy,
|
||||
PregelExecutableTask,
|
||||
|
||||
@@ -72,7 +72,7 @@ from langgraph._internal._runnable import (
|
||||
RunnableSeq,
|
||||
coerce_to_runnable,
|
||||
)
|
||||
from langgraph._internal._typing import MISSING, DeprecatedKwargs
|
||||
from langgraph._internal._typing import DeprecatedKwargs
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.channels.topic import Topic
|
||||
@@ -636,9 +636,7 @@ class Pregel(
|
||||
name: str = "LangGraph",
|
||||
**deprecated_kwargs: Unpack[DeprecatedKwargs],
|
||||
) -> None:
|
||||
if (
|
||||
config_type := deprecated_kwargs.get("config_type", MISSING)
|
||||
) is not MISSING:
|
||||
if config_type := deprecated_kwargs.get("config_type"):
|
||||
warnings.warn(
|
||||
"`config_type` is deprecated and will be removed. Please use `context_schema` instead.",
|
||||
category=LangGraphDeprecatedSinceV10,
|
||||
@@ -784,8 +782,7 @@ class Pregel(
|
||||
return self
|
||||
|
||||
@deprecated(
|
||||
"`config_schema` is deprecated. Use `get_context_jsonschema` for the relevant schema instead.",
|
||||
category=None,
|
||||
"`config_schema` is deprecated. Use `get_context_jsonschema` for the relevant schema instead."
|
||||
)
|
||||
def config_schema(self, *, include: Sequence[str] | None = None) -> type[BaseModel]:
|
||||
warnings.warn(
|
||||
@@ -810,8 +807,7 @@ class Pregel(
|
||||
return create_model(self.get_name("Config"), field_definitions=fields)
|
||||
|
||||
@deprecated(
|
||||
"`get_config_jsonschema` is deprecated. Use `get_context_jsonschema` instead.",
|
||||
category=None,
|
||||
"`get_config_jsonschema` is deprecated. Use `get_context_jsonschema` instead."
|
||||
)
|
||||
def get_config_jsonschema(
|
||||
self, *, include: Sequence[str] | None = None
|
||||
@@ -1306,7 +1302,7 @@ class Pregel(
|
||||
) -> Iterator[StateSnapshot]:
|
||||
"""Get the history of the state of the graph."""
|
||||
config = ensure_config(config)
|
||||
checkpointer: BaseCheckpointSaver | None = config[CONF].get(
|
||||
checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get(
|
||||
CONFIG_KEY_CHECKPOINTER, self.checkpointer
|
||||
)
|
||||
if not checkpointer:
|
||||
@@ -2352,6 +2348,7 @@ class Pregel(
|
||||
interrupt_before: All | Sequence[str] | None,
|
||||
interrupt_after: All | Sequence[str] | None,
|
||||
durability: Durability | None = None,
|
||||
checkpoint_during: bool | None = None,
|
||||
) -> tuple[
|
||||
set[StreamMode],
|
||||
str | Sequence[str],
|
||||
@@ -2399,6 +2396,15 @@ class Pregel(
|
||||
cache: BaseCache | None = config[CONF][CONFIG_KEY_CACHE]
|
||||
else:
|
||||
cache = self.cache
|
||||
if checkpoint_during is not None:
|
||||
if durability is not None:
|
||||
raise ValueError(
|
||||
"Cannot use both `checkpoint_during` and `durability` parameters."
|
||||
)
|
||||
elif checkpoint_during:
|
||||
durability = "async"
|
||||
else:
|
||||
durability = "exit"
|
||||
if durability is None:
|
||||
durability = config.get(CONF, {}).get(CONFIG_KEY_DURABILITY, "async")
|
||||
return (
|
||||
@@ -2471,17 +2477,6 @@ class Pregel(
|
||||
Yields:
|
||||
The output of each step in the graph. The output shape depends on the stream_mode.
|
||||
"""
|
||||
if (checkpoint_during := kwargs.get("checkpoint_during")) is not None:
|
||||
warnings.warn(
|
||||
"`checkpoint_during` is deprecated and will be removed. Please use `durability` instead.",
|
||||
category=LangGraphDeprecatedSinceV10,
|
||||
stacklevel=2,
|
||||
)
|
||||
if durability is not None:
|
||||
raise ValueError(
|
||||
"Cannot use both `checkpoint_during` and `durability` parameters. Please use `durability` instead."
|
||||
)
|
||||
durability = "async" if checkpoint_during else "exit"
|
||||
|
||||
if stream_mode is None:
|
||||
# if being called as a node in another graph, default to values mode
|
||||
@@ -2505,6 +2500,14 @@ class Pregel(
|
||||
run_id=config.get("run_id"),
|
||||
)
|
||||
try:
|
||||
deprecated_checkpoint_during = cast(
|
||||
Optional[bool], kwargs.get("checkpoint_during")
|
||||
)
|
||||
if deprecated_checkpoint_during is not None:
|
||||
warnings.warn(
|
||||
"`checkpoint_during` is deprecated and will be removed. Please use `durability` instead.",
|
||||
category=LangGraphDeprecatedSinceV10,
|
||||
)
|
||||
# assign defaults
|
||||
(
|
||||
stream_modes,
|
||||
@@ -2523,8 +2526,11 @@ class Pregel(
|
||||
interrupt_before=interrupt_before,
|
||||
interrupt_after=interrupt_after,
|
||||
durability=durability,
|
||||
checkpoint_during=deprecated_checkpoint_during,
|
||||
)
|
||||
if checkpointer is None and durability is not None:
|
||||
if checkpointer is None and (
|
||||
durability is not None or deprecated_checkpoint_during is not None
|
||||
):
|
||||
warnings.warn(
|
||||
"`durability` has no effect when no checkpointer is present.",
|
||||
)
|
||||
@@ -2534,13 +2540,8 @@ class Pregel(
|
||||
config[CONF][CONFIG_KEY_CHECKPOINT_NS] = recast_checkpoint_ns(ns)
|
||||
# set up messages stream mode
|
||||
if "messages" in stream_modes:
|
||||
ns_ = cast(Optional[str], config[CONF].get(CONFIG_KEY_CHECKPOINT_NS))
|
||||
run_manager.inheritable_handlers.append(
|
||||
StreamMessagesHandler(
|
||||
stream.put,
|
||||
subgraphs,
|
||||
parent_ns=tuple(ns_.split(NS_SEP)) if ns_ else None,
|
||||
)
|
||||
StreamMessagesHandler(stream.put, subgraphs)
|
||||
)
|
||||
|
||||
# set up custom stream mode
|
||||
@@ -2566,11 +2567,11 @@ class Pregel(
|
||||
pass
|
||||
|
||||
# set durability mode for subgraphs
|
||||
if durability is not None:
|
||||
if durability is not None or deprecated_checkpoint_during is not None:
|
||||
config[CONF][CONFIG_KEY_DURABILITY] = durability_
|
||||
|
||||
runtime = Runtime(
|
||||
context=_coerce_context(self.context_schema, context),
|
||||
context=context,
|
||||
store=store,
|
||||
stream_writer=stream_writer,
|
||||
previous=None,
|
||||
@@ -2737,17 +2738,6 @@ class Pregel(
|
||||
Yields:
|
||||
The output of each step in the graph. The output shape depends on the stream_mode.
|
||||
"""
|
||||
if (checkpoint_during := kwargs.get("checkpoint_during")) is not None:
|
||||
warnings.warn(
|
||||
"`checkpoint_during` is deprecated and will be removed. Please use `durability` instead.",
|
||||
category=LangGraphDeprecatedSinceV10,
|
||||
stacklevel=2,
|
||||
)
|
||||
if durability is not None:
|
||||
raise ValueError(
|
||||
"Cannot use both `checkpoint_during` and `durability` parameters. Please use `durability` instead."
|
||||
)
|
||||
durability = "async" if checkpoint_during else "exit"
|
||||
|
||||
if stream_mode is None:
|
||||
# if being called as a node in another graph, default to values mode
|
||||
@@ -2790,6 +2780,14 @@ class Pregel(
|
||||
else False
|
||||
)
|
||||
try:
|
||||
deprecated_checkpoint_during = cast(
|
||||
Optional[bool], kwargs.get("checkpoint_during")
|
||||
)
|
||||
if deprecated_checkpoint_during is not None:
|
||||
warnings.warn(
|
||||
"`checkpoint_during` is deprecated and will be removed. Please use `durability` instead.",
|
||||
category=LangGraphDeprecatedSinceV10,
|
||||
)
|
||||
# assign defaults
|
||||
(
|
||||
stream_modes,
|
||||
@@ -2808,8 +2806,11 @@ class Pregel(
|
||||
interrupt_before=interrupt_before,
|
||||
interrupt_after=interrupt_after,
|
||||
durability=durability,
|
||||
checkpoint_during=deprecated_checkpoint_during,
|
||||
)
|
||||
if checkpointer is None and durability is not None:
|
||||
if checkpointer is None and (
|
||||
durability is not None or deprecated_checkpoint_during is not None
|
||||
):
|
||||
warnings.warn(
|
||||
"`durability` has no effect when no checkpointer is present.",
|
||||
)
|
||||
@@ -2819,14 +2820,8 @@ class Pregel(
|
||||
config[CONF][CONFIG_KEY_CHECKPOINT_NS] = recast_checkpoint_ns(ns)
|
||||
# set up messages stream mode
|
||||
if "messages" in stream_modes:
|
||||
# namespace can be None in a root level graph?
|
||||
ns_ = cast(Optional[str], config[CONF].get(CONFIG_KEY_CHECKPOINT_NS))
|
||||
run_manager.inheritable_handlers.append(
|
||||
StreamMessagesHandler(
|
||||
stream_put,
|
||||
subgraphs,
|
||||
parent_ns=tuple(ns_.split(NS_SEP)) if ns_ else None,
|
||||
)
|
||||
StreamMessagesHandler(stream_put, subgraphs)
|
||||
)
|
||||
|
||||
# set up custom stream mode
|
||||
@@ -2867,11 +2862,11 @@ class Pregel(
|
||||
pass
|
||||
|
||||
# set durability mode for subgraphs
|
||||
if durability is not None:
|
||||
if durability is not None or deprecated_checkpoint_during is not None:
|
||||
config[CONF][CONFIG_KEY_DURABILITY] = durability_
|
||||
|
||||
runtime = Runtime(
|
||||
context=_coerce_context(self.context_schema, context),
|
||||
context=context,
|
||||
store=store,
|
||||
stream_writer=stream_writer,
|
||||
previous=None,
|
||||
@@ -2992,7 +2987,6 @@ class Pregel(
|
||||
output_keys: str | Sequence[str] | None = None,
|
||||
interrupt_before: All | Sequence[str] | None = None,
|
||||
interrupt_after: All | Sequence[str] | None = None,
|
||||
durability: Durability | None = None,
|
||||
**kwargs: Any,
|
||||
) -> dict[str, Any] | Any:
|
||||
"""Run the graph with a single input and config.
|
||||
@@ -3007,10 +3001,6 @@ class Pregel(
|
||||
output_keys: Optional. The output keys to retrieve from the graph run.
|
||||
interrupt_before: Optional. The nodes to interrupt the graph run before.
|
||||
interrupt_after: Optional. The nodes to interrupt the graph run after.
|
||||
durability: The durability mode for the graph execution, defaults to "async". Options are:
|
||||
- `"sync"`: Changes are persisted synchronously before the next step starts.
|
||||
- `"async"`: Changes are persisted asynchronously while the next step executes.
|
||||
- `"exit"`: Changes are persisted only when the graph exits.
|
||||
**kwargs: Additional keyword arguments to pass to the graph run.
|
||||
|
||||
Returns:
|
||||
@@ -3034,7 +3024,6 @@ class Pregel(
|
||||
output_keys=output_keys,
|
||||
interrupt_before=interrupt_before,
|
||||
interrupt_after=interrupt_after,
|
||||
durability=durability,
|
||||
**kwargs,
|
||||
):
|
||||
if stream_mode == "values":
|
||||
@@ -3077,7 +3066,6 @@ class Pregel(
|
||||
output_keys: str | Sequence[str] | None = None,
|
||||
interrupt_before: All | Sequence[str] | None = None,
|
||||
interrupt_after: All | Sequence[str] | None = None,
|
||||
durability: Durability | None = None,
|
||||
**kwargs: Any,
|
||||
) -> dict[str, Any] | Any:
|
||||
"""Asynchronously invoke the graph on a single input.
|
||||
@@ -3092,10 +3080,6 @@ class Pregel(
|
||||
output_keys: Optional. The output keys to include in the result. Default is None.
|
||||
interrupt_before: Optional. The nodes to interrupt before. Default is None.
|
||||
interrupt_after: Optional. The nodes to interrupt after. Default is None.
|
||||
durability: The durability mode for the graph execution, defaults to "async". Options are:
|
||||
- `"sync"`: Changes are persisted synchronously before the next step starts.
|
||||
- `"async"`: Changes are persisted asynchronously while the next step executes.
|
||||
- `"exit"`: Changes are persisted only when the graph exits.
|
||||
**kwargs: Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
@@ -3120,7 +3104,6 @@ class Pregel(
|
||||
output_keys=output_keys,
|
||||
interrupt_before=interrupt_before,
|
||||
interrupt_after=interrupt_after,
|
||||
durability=durability,
|
||||
**kwargs,
|
||||
):
|
||||
if stream_mode == "values":
|
||||
@@ -3241,33 +3224,3 @@ def _output(
|
||||
yield (ns, payload)
|
||||
else:
|
||||
yield payload
|
||||
|
||||
|
||||
def _coerce_context(
|
||||
context_schema: type[ContextT] | None, context: Any
|
||||
) -> ContextT | None:
|
||||
"""Coerce context input to the appropriate schema type.
|
||||
|
||||
If context is a dict and context_schema is a dataclass or pydantic model, we coerce.
|
||||
Else, we return the context as-is.
|
||||
|
||||
Args:
|
||||
context_schema: The schema type to coerce to (BaseModel, dataclass, or TypedDict)
|
||||
context: The context value to coerce
|
||||
|
||||
Returns:
|
||||
The coerced context value or None if context is None
|
||||
"""
|
||||
if context is None:
|
||||
return None
|
||||
|
||||
if context_schema is None:
|
||||
return context
|
||||
|
||||
schema_is_class = issubclass(context_schema, BaseModel) or is_dataclass(
|
||||
context_schema
|
||||
)
|
||||
if isinstance(context, dict) and schema_is_class:
|
||||
return context_schema(**context) # type: ignore[misc]
|
||||
|
||||
return cast(ContextT, context)
|
||||
|
||||
@@ -191,7 +191,10 @@ class Interrupt:
|
||||
return cls(value=value, id=xxh3_128_hexdigest(ns.encode()))
|
||||
|
||||
@property
|
||||
@deprecated("`interrupt_id` is deprecated. Use `id` instead.", category=None)
|
||||
@deprecated(
|
||||
"`interrupt_id` is deprecated. Use `id` instead.",
|
||||
stacklevel=2,
|
||||
)
|
||||
def interrupt_id(self) -> str:
|
||||
warn(
|
||||
"`interrupt_id` is deprecated. Use `id` instead.",
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph"
|
||||
version = "0.6.4"
|
||||
version = "0.6.1"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
requires-python = ">=3.9"
|
||||
@@ -49,7 +49,6 @@ dev = [
|
||||
"types-requests",
|
||||
"pycryptodome",
|
||||
"langgraph-cli[inmem]",
|
||||
"redis",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
|
||||
@@ -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 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"}}, "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"}}, "description": "The state of the agent.", "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}, "is_last_step": {"title": "Is Last Step", "type": "boolean"}, "remaining_steps": {"title": "Remaining Steps", "type": "integer"}}, "required": ["messages", "is_last_step", "remaining_steps"], "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 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"}}, "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"}}, "description": "The state of the agent.", "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}, "is_last_step": {"title": "Is Last Step", "type": "boolean"}, "remaining_steps": {"title": "Remaining Steps", "type": "integer"}}, "required": ["messages", "is_last_step", "remaining_steps"], "title": "AgentState", "type": "object"}'
|
||||
# ---
|
||||
# name: test_prebuilt_tool_chat.2
|
||||
'''
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
name: langgraph-tests
|
||||
services:
|
||||
redis-test:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||||
healthcheck:
|
||||
test: redis-cli ping
|
||||
start_period: 10s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
interval: 5s
|
||||
start_interval: 1s
|
||||
tmpfs:
|
||||
- /data # Use tmpfs for faster testing
|
||||
@@ -3,12 +3,10 @@ from collections.abc import AsyncIterator, Iterator
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
import redis
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.cache.memory import InMemoryCache
|
||||
from langgraph.cache.redis import RedisCache
|
||||
from langgraph.cache.sqlite import SqliteCache
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.store.base import BaseStore
|
||||
@@ -57,34 +55,12 @@ def durability(request: pytest.FixtureRequest) -> Durability:
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
scope="function",
|
||||
params=["sqlite", "memory"] if NO_DOCKER else ["sqlite", "memory", "redis"],
|
||||
)
|
||||
@pytest.fixture(scope="function", params=["sqlite", "memory"])
|
||||
def cache(request: pytest.FixtureRequest) -> Iterator[BaseCache]:
|
||||
if request.param == "sqlite":
|
||||
yield SqliteCache(path=":memory:")
|
||||
elif request.param == "memory":
|
||||
yield InMemoryCache()
|
||||
elif request.param == "redis":
|
||||
# Get worker ID for parallel test isolation
|
||||
worker_id = getattr(request.config, "workerinput", {}).get("workerid", "master")
|
||||
|
||||
redis_client = redis.Redis(
|
||||
host="localhost", port=6379, db=0, decode_responses=False
|
||||
)
|
||||
# Use worker-specific prefix to avoid cache pollution between parallel tests
|
||||
cache = RedisCache(redis_client, prefix=f"test:cache:{worker_id}:")
|
||||
yield cache
|
||||
|
||||
try:
|
||||
# Only clear keys with our specific prefix
|
||||
pattern = f"test:cache:{worker_id}:*"
|
||||
keys = redis_client.keys(pattern)
|
||||
if keys:
|
||||
redis_client.delete(*keys)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
raise ValueError(f"Unknown cache type: {request.param}")
|
||||
|
||||
|
||||
@@ -330,7 +330,6 @@ SAVED_CHECKPOINTS = {
|
||||
"docs": ["doc1", "doc2", "doc3", "doc4"],
|
||||
"answer": "doc1,doc2,doc3,doc4",
|
||||
},
|
||||
"updated_channels": None,
|
||||
},
|
||||
metadata={
|
||||
"source": "loop",
|
||||
@@ -391,7 +390,6 @@ SAVED_CHECKPOINTS = {
|
||||
"docs": ["doc1", "doc2", "doc3", "doc4"],
|
||||
"branch:to:qa": None,
|
||||
},
|
||||
"updated_channels": None,
|
||||
},
|
||||
metadata={
|
||||
"source": "loop",
|
||||
@@ -467,7 +465,6 @@ SAVED_CHECKPOINTS = {
|
||||
"branch:to:retriever_one": None,
|
||||
"docs": ["doc3", "doc4"],
|
||||
},
|
||||
"updated_channels": None,
|
||||
},
|
||||
metadata={
|
||||
"source": "loop",
|
||||
@@ -519,7 +516,6 @@ SAVED_CHECKPOINTS = {
|
||||
"branch:to:analyzer_one": None,
|
||||
"branch:to:retriever_two": None,
|
||||
},
|
||||
"updated_channels": None,
|
||||
},
|
||||
metadata={
|
||||
"source": "loop",
|
||||
@@ -574,7 +570,6 @@ SAVED_CHECKPOINTS = {
|
||||
"query": "what is weather in sf",
|
||||
"branch:to:rewrite_query": None,
|
||||
},
|
||||
"updated_channels": None,
|
||||
},
|
||||
metadata={
|
||||
"source": "loop",
|
||||
@@ -623,7 +618,6 @@ SAVED_CHECKPOINTS = {
|
||||
},
|
||||
"versions_seen": {"__input__": {}},
|
||||
"channel_values": {"__start__": {"query": "what is weather in sf"}},
|
||||
"updated_channels": None,
|
||||
},
|
||||
metadata={
|
||||
"source": "input",
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from typing import Any, Optional
|
||||
|
||||
import pytest
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from pytest_mock import MockerFixture
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.channels.last_value import LastValue
|
||||
from langgraph.errors import NodeInterrupt
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.graph.message import MessageGraph
|
||||
from langgraph.pregel import NodeBuilder, Pregel
|
||||
from langgraph.types import Interrupt, RetryPolicy
|
||||
from langgraph.warnings import LangGraphDeprecatedSinceV05, LangGraphDeprecatedSinceV10
|
||||
@@ -101,13 +94,14 @@ def test_pregel_types_deprecation() -> None:
|
||||
from langgraph.pregel.types import StateSnapshot # noqa: F401
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:`config_schema` is deprecated")
|
||||
@pytest.mark.filterwarnings("ignore:`get_config_jsonschema` is deprecated")
|
||||
def test_config_schema_deprecation() -> None:
|
||||
with pytest.warns(
|
||||
LangGraphDeprecatedSinceV10,
|
||||
match="`config_schema` is deprecated and will be removed. Please use `context_schema` instead.",
|
||||
):
|
||||
builder = StateGraph(PlainState, config_schema=PlainState)
|
||||
assert builder.context_schema == PlainState
|
||||
|
||||
builder.add_node("test_node", lambda state: state)
|
||||
builder.set_entry_point("test_node")
|
||||
@@ -117,7 +111,7 @@ def test_config_schema_deprecation() -> None:
|
||||
LangGraphDeprecatedSinceV10,
|
||||
match="`config_schema` is deprecated. Use `get_context_jsonschema` for the relevant schema instead.",
|
||||
):
|
||||
assert graph.config_schema() is not None
|
||||
graph.config_schema()
|
||||
|
||||
with pytest.warns(
|
||||
LangGraphDeprecatedSinceV10,
|
||||
@@ -126,25 +120,6 @@ def test_config_schema_deprecation() -> None:
|
||||
graph.get_config_jsonschema()
|
||||
|
||||
|
||||
def test_config_schema_deprecation_on_entrypoint() -> None:
|
||||
with pytest.warns(
|
||||
LangGraphDeprecatedSinceV10,
|
||||
match="`config_schema` is deprecated and will be removed. Please use `context_schema` instead.",
|
||||
):
|
||||
|
||||
@entrypoint(config_schema=PlainState) # type: ignore[arg-type]
|
||||
def my_entrypoint(state: PlainState) -> PlainState:
|
||||
return state
|
||||
|
||||
with pytest.warns(
|
||||
LangGraphDeprecatedSinceV10,
|
||||
match="`config_schema` is deprecated. Use `get_context_jsonschema` for the relevant schema instead.",
|
||||
):
|
||||
assert my_entrypoint.context_schema == PlainState
|
||||
assert my_entrypoint.config_schema() is not None
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:`config_type` is deprecated")
|
||||
def test_config_type_deprecation_pregel(mocker: MockerFixture) -> None:
|
||||
add_one = mocker.Mock(side_effect=lambda x: x + 1)
|
||||
chain = NodeBuilder().subscribe_only("input").do(add_one).write_to("output")
|
||||
@@ -153,7 +128,7 @@ def test_config_type_deprecation_pregel(mocker: MockerFixture) -> None:
|
||||
LangGraphDeprecatedSinceV10,
|
||||
match="`config_type` is deprecated and will be removed. Please use `context_schema` instead.",
|
||||
):
|
||||
instance = Pregel(
|
||||
Pregel(
|
||||
nodes={
|
||||
"one": chain,
|
||||
},
|
||||
@@ -165,9 +140,9 @@ def test_config_type_deprecation_pregel(mocker: MockerFixture) -> None:
|
||||
output_channels="output",
|
||||
config_type=PlainState,
|
||||
)
|
||||
assert instance.context_schema == PlainState
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:`interrupt_id` is deprecated. Use `id` instead.")
|
||||
def test_interrupt_attributes_deprecation() -> None:
|
||||
interrupt = Interrupt(value="question", id="abc")
|
||||
|
||||
@@ -178,6 +153,7 @@ def test_interrupt_attributes_deprecation() -> None:
|
||||
interrupt.interrupt_id
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:NodeInterrupt is deprecated.")
|
||||
def test_node_interrupt_deprecation() -> None:
|
||||
with pytest.warns(
|
||||
LangGraphDeprecatedSinceV10,
|
||||
@@ -192,152 +168,3 @@ def test_deprecated_import() -> None:
|
||||
match="Importing PREVIOUS from langgraph.constants is deprecated. This constant is now private and should not be used directly.",
|
||||
):
|
||||
from langgraph.constants import PREVIOUS # noqa: F401
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings(
|
||||
"ignore:`durability` has no effect when no checkpointer is present"
|
||||
)
|
||||
def test_checkpoint_during_deprecation_state_graph() -> None:
|
||||
class CheckDurability(TypedDict):
|
||||
durability: NotRequired[str]
|
||||
|
||||
def plain_node(state: CheckDurability, config: RunnableConfig) -> CheckDurability:
|
||||
return {"durability": config["configurable"]["__pregel_durability"]}
|
||||
|
||||
builder = StateGraph(CheckDurability)
|
||||
builder.add_node("plain_node", plain_node)
|
||||
builder.set_entry_point("plain_node")
|
||||
graph = builder.compile()
|
||||
|
||||
with pytest.warns(
|
||||
LangGraphDeprecatedSinceV10,
|
||||
match="`checkpoint_during` is deprecated and will be removed. Please use `durability` instead.",
|
||||
):
|
||||
result = graph.invoke({}, checkpoint_during=True)
|
||||
assert result["durability"] == "async"
|
||||
|
||||
with pytest.warns(
|
||||
LangGraphDeprecatedSinceV10,
|
||||
match="`checkpoint_during` is deprecated and will be removed. Please use `durability` instead.",
|
||||
):
|
||||
result = graph.invoke({}, checkpoint_during=False)
|
||||
assert result["durability"] == "exit"
|
||||
|
||||
with pytest.warns(
|
||||
LangGraphDeprecatedSinceV10,
|
||||
match="`checkpoint_during` is deprecated and will be removed. Please use `durability` instead.",
|
||||
):
|
||||
for chunk in graph.stream({}, checkpoint_during=True): # type: ignore[arg-type]
|
||||
assert chunk["plain_node"]["durability"] == "async"
|
||||
|
||||
with pytest.warns(
|
||||
LangGraphDeprecatedSinceV10,
|
||||
match="`checkpoint_during` is deprecated and will be removed. Please use `durability` instead.",
|
||||
):
|
||||
for chunk in graph.stream({}, checkpoint_during=False): # type: ignore[arg-type]
|
||||
assert chunk["plain_node"]["durability"] == "exit"
|
||||
|
||||
|
||||
def test_config_parameter_incorrect_typing() -> None:
|
||||
"""Test that a warning is raised when config parameter is typed incorrectly."""
|
||||
builder = StateGraph(PlainState)
|
||||
|
||||
# Test sync function with config: dict
|
||||
with pytest.warns(
|
||||
UserWarning,
|
||||
match="The 'config' parameter should be typed as 'RunnableConfig' or 'RunnableConfig | None', not '.*dict.*'. ",
|
||||
):
|
||||
|
||||
def sync_node_with_dict_config(state: PlainState, config: dict) -> PlainState:
|
||||
return state
|
||||
|
||||
builder.add_node(sync_node_with_dict_config)
|
||||
|
||||
# Test async function with config: dict
|
||||
with pytest.warns(
|
||||
UserWarning,
|
||||
match="The 'config' parameter should be typed as 'RunnableConfig' or 'RunnableConfig | None', not '.*dict.*'. ",
|
||||
):
|
||||
|
||||
async def async_node_with_dict_config(
|
||||
state: PlainState, config: dict
|
||||
) -> PlainState:
|
||||
return state
|
||||
|
||||
builder.add_node(async_node_with_dict_config)
|
||||
|
||||
# Test with other incorrect types
|
||||
with pytest.warns(
|
||||
UserWarning,
|
||||
match="The 'config' parameter should be typed as 'RunnableConfig' or 'RunnableConfig | None', not '.*Any.*'. ",
|
||||
):
|
||||
|
||||
def sync_node_with_any_config(state: PlainState, config: Any) -> PlainState:
|
||||
return state
|
||||
|
||||
builder.add_node(sync_node_with_any_config)
|
||||
|
||||
with pytest.warns(
|
||||
UserWarning,
|
||||
match="The 'config' parameter should be typed as 'RunnableConfig' or 'RunnableConfig | None', not '.*Any.*'. ",
|
||||
):
|
||||
|
||||
async def async_node_with_any_config(
|
||||
state: PlainState, config: Any
|
||||
) -> PlainState:
|
||||
return state
|
||||
|
||||
builder.add_node(async_node_with_any_config)
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
|
||||
def node_with_correct_config(
|
||||
state: PlainState, config: RunnableConfig
|
||||
) -> PlainState:
|
||||
return state
|
||||
|
||||
builder.add_node(node_with_correct_config)
|
||||
|
||||
def node_with_optional_config(
|
||||
state: PlainState,
|
||||
config: Optional[RunnableConfig], # noqa: UP045
|
||||
) -> PlainState:
|
||||
return state
|
||||
|
||||
builder.add_node(node_with_optional_config)
|
||||
|
||||
def node_with_untyped_config(state: PlainState, config) -> PlainState:
|
||||
return state
|
||||
|
||||
builder.add_node(node_with_untyped_config)
|
||||
|
||||
async def async_node_with_correct_config(
|
||||
state: PlainState, config: RunnableConfig
|
||||
) -> PlainState:
|
||||
return state
|
||||
|
||||
builder.add_node(async_node_with_correct_config)
|
||||
|
||||
async def async_node_with_optional_config(
|
||||
state: PlainState,
|
||||
config: Optional[RunnableConfig], # noqa: UP045
|
||||
) -> PlainState:
|
||||
return state
|
||||
|
||||
builder.add_node(async_node_with_optional_config)
|
||||
|
||||
async def async_node_with_untyped_config(
|
||||
state: PlainState, config
|
||||
) -> PlainState:
|
||||
return state
|
||||
|
||||
builder.add_node(async_node_with_untyped_config)
|
||||
assert len(w) == 0
|
||||
|
||||
|
||||
def test_message_graph_deprecation() -> None:
|
||||
with pytest.warns(
|
||||
LangGraphDeprecatedSinceV10,
|
||||
match="MessageGraph is deprecated in LangGraph v1.0.0, to be removed in v2.0.0. Please use StateGraph with a `messages` key instead.",
|
||||
):
|
||||
MessageGraph()
|
||||
|
||||
@@ -6,9 +6,7 @@ from dataclasses import replace
|
||||
from typing import Annotated, Any, Literal, Optional, Union, cast
|
||||
|
||||
import pytest
|
||||
from langchain_core.messages import AIMessage, AnyMessage, ToolCall
|
||||
from langchain_core.runnables import RunnableConfig, RunnableMap, RunnablePick
|
||||
from langchain_core.tools import tool
|
||||
from pytest_mock import MockerFixture
|
||||
from syrupy import SnapshotAssertion
|
||||
from typing_extensions import TypedDict
|
||||
@@ -20,7 +18,7 @@ from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.constants import END, START
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.graph.message import MessagesState, add_messages
|
||||
from langgraph.graph.message import MessageGraph, MessagesState, add_messages
|
||||
from langgraph.prebuilt.chat_agent_executor import create_react_agent
|
||||
from langgraph.prebuilt.tool_node import ToolNode
|
||||
from langgraph.pregel import NodeBuilder, Pregel
|
||||
@@ -2443,7 +2441,7 @@ def test_message_graph(
|
||||
return "continue"
|
||||
|
||||
# Define a new graph
|
||||
workflow = StateGraph(state_schema=Annotated[list[AnyMessage], add_messages]) # type: ignore[arg-type]
|
||||
workflow = MessageGraph()
|
||||
|
||||
# Define the two nodes we will cycle between
|
||||
workflow.add_node("agent", model)
|
||||
@@ -2489,7 +2487,7 @@ def test_message_graph(
|
||||
assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot
|
||||
assert app.get_graph().draw_mermaid(with_styles=False) == snapshot
|
||||
|
||||
assert app.invoke([HumanMessage(content="what is weather in sf")]) == [
|
||||
assert app.invoke(HumanMessage(content="what is weather in sf")) == [
|
||||
_AnyIdHumanMessage(
|
||||
content="what is weather in sf",
|
||||
),
|
||||
@@ -6437,6 +6435,10 @@ def test_weather_subgraph(
|
||||
from langchain_core.language_models.fake_chat_models import (
|
||||
FakeMessagesListChatModel,
|
||||
)
|
||||
from langchain_core.messages import AIMessage, ToolCall
|
||||
from langchain_core.tools import tool
|
||||
|
||||
from langgraph.graph import MessagesState
|
||||
|
||||
# setup subgraph
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from typing import (
|
||||
)
|
||||
|
||||
import pytest
|
||||
from langchain_core.messages import AnyMessage, ToolCall
|
||||
from langchain_core.messages import ToolCall
|
||||
from langchain_core.runnables import RunnableConfig, RunnablePick
|
||||
from pytest_mock import MockerFixture
|
||||
from typing_extensions import TypedDict
|
||||
@@ -21,7 +21,7 @@ from langgraph.channels.last_value import LastValue
|
||||
from langgraph.channels.untracked_value import UntrackedValue
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.constants import END, START
|
||||
from langgraph.graph.message import add_messages
|
||||
from langgraph.graph.message import MessageGraph, add_messages
|
||||
from langgraph.graph.state import StateGraph
|
||||
from langgraph.prebuilt.chat_agent_executor import create_react_agent
|
||||
from langgraph.prebuilt.tool_node import ToolNode
|
||||
@@ -2117,7 +2117,7 @@ async def test_message_graph(async_checkpointer: BaseCheckpointSaver) -> None:
|
||||
return "continue"
|
||||
|
||||
# Define a new graph
|
||||
workflow = StateGraph(state_schema=Annotated[list[AnyMessage], add_messages]) # type: ignore[arg-type]
|
||||
workflow = MessageGraph()
|
||||
|
||||
# Define the two nodes we will cycle between
|
||||
workflow.add_node("agent", model)
|
||||
@@ -2157,7 +2157,7 @@ async def test_message_graph(async_checkpointer: BaseCheckpointSaver) -> None:
|
||||
# meaning you can use it as you would any other runnable
|
||||
app = workflow.compile()
|
||||
|
||||
assert await app.ainvoke([HumanMessage(content="what is weather in sf")]) == [
|
||||
assert await app.ainvoke(HumanMessage(content="what is weather in sf")) == [
|
||||
_AnyIdHumanMessage(
|
||||
content="what is weather in sf",
|
||||
),
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
from typing_extensions import NotRequired, Required, TypedDict
|
||||
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.managed import RemainingSteps
|
||||
|
||||
|
||||
class StatePlain(TypedDict):
|
||||
remaining_steps: RemainingSteps
|
||||
|
||||
|
||||
class StateNotRequired(TypedDict):
|
||||
remaining_steps: NotRequired[RemainingSteps]
|
||||
|
||||
|
||||
class StateRequired(TypedDict):
|
||||
remaining_steps: Required[RemainingSteps]
|
||||
|
||||
|
||||
def test_managed_values_recognized() -> None:
|
||||
graph = StateGraph(StatePlain)
|
||||
assert "remaining_steps" in graph.managed
|
||||
|
||||
graph = StateGraph(StateNotRequired)
|
||||
assert "remaining_steps" in graph.managed
|
||||
|
||||
graph = StateGraph(StateRequired)
|
||||
assert "remaining_steps" in graph.managed
|
||||
@@ -16,7 +16,6 @@ from typing import Annotated, Any, Literal, Optional, Union, get_type_hints
|
||||
|
||||
import pytest
|
||||
from langchain_core.language_models import GenericFakeChatModel
|
||||
from langchain_core.messages import AnyMessage
|
||||
from langchain_core.runnables import (
|
||||
RunnableConfig,
|
||||
RunnableLambda,
|
||||
@@ -27,7 +26,7 @@ from langsmith import traceable
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
from pytest_mock import MockerFixture
|
||||
from syrupy import SnapshotAssertion
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph._internal._constants import CONFIG_KEY_NODE_FINISHED, ERROR, PULL
|
||||
from langgraph.cache.base import BaseCache
|
||||
@@ -46,7 +45,7 @@ from langgraph.config import get_stream_writer
|
||||
from langgraph.errors import GraphRecursionError, InvalidUpdateError, ParentCommand
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.graph import END, START, StateGraph
|
||||
from langgraph.graph.message import MessagesState, add_messages
|
||||
from langgraph.graph.message import MessageGraph, MessagesState, add_messages
|
||||
from langgraph.prebuilt.tool_node import ToolNode
|
||||
from langgraph.pregel import (
|
||||
NodeBuilder,
|
||||
@@ -968,7 +967,6 @@ def test_pending_writes_resume(
|
||||
"branch:to:two": AnyVersion(),
|
||||
},
|
||||
"channel_values": {"value": 6},
|
||||
"updated_channels": ["value"],
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
@@ -1016,7 +1014,6 @@ def test_pending_writes_resume(
|
||||
"branch:to:one": None,
|
||||
"branch:to:two": None,
|
||||
},
|
||||
"updated_channels": ["branch:to:one", "branch:to:two", "value"],
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
@@ -1068,7 +1065,6 @@ def test_pending_writes_resume(
|
||||
"__start__": AnyVersion(),
|
||||
},
|
||||
"channel_values": {"__start__": {"value": 1}},
|
||||
"updated_channels": ["__start__"],
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
@@ -3911,7 +3907,7 @@ def test_remove_message_via_state_update(
|
||||
) -> None:
|
||||
from langchain_core.messages import AIMessage, HumanMessage, RemoveMessage
|
||||
|
||||
workflow = StateGraph(state_schema=Annotated[list[AnyMessage], add_messages]) # type: ignore[arg-type]
|
||||
workflow = MessageGraph()
|
||||
workflow.add_node(
|
||||
"chatbot",
|
||||
lambda state: [
|
||||
@@ -3944,7 +3940,7 @@ def test_remove_message_via_state_update(
|
||||
def test_remove_message_from_node():
|
||||
from langchain_core.messages import AIMessage, HumanMessage, RemoveMessage
|
||||
|
||||
workflow = StateGraph(state_schema=Annotated[list[AnyMessage], add_messages]) # type: ignore[arg-type]
|
||||
workflow = MessageGraph()
|
||||
workflow.add_node(
|
||||
"chatbot",
|
||||
lambda state: [
|
||||
@@ -8266,53 +8262,3 @@ def test_fork_and_update_task_results(sync_checkpointer: BaseCheckpointSaver) ->
|
||||
],
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
def test_subgraph_streaming_sync() -> None:
|
||||
"""Test subgraph streaming when used as a node in sync version"""
|
||||
|
||||
# Create a fake chat model that returns a simple response
|
||||
model = GenericFakeChatModel(messages=iter(["The weather is sunny today."]))
|
||||
|
||||
# Create a subgraph that uses the fake chat model
|
||||
def call_model_node(state: MessagesState, config: RunnableConfig) -> MessagesState:
|
||||
"""Node that calls the model with the last message."""
|
||||
messages = state["messages"]
|
||||
last_message = messages[-1].content if messages else ""
|
||||
response = model.invoke([("user", last_message)], config)
|
||||
return {"messages": [response]}
|
||||
|
||||
# Build the subgraph
|
||||
subgraph = StateGraph(MessagesState)
|
||||
subgraph.add_node("call_model", call_model_node)
|
||||
subgraph.add_edge(START, "call_model")
|
||||
compiled_subgraph = subgraph.compile()
|
||||
|
||||
class SomeCustomState(TypedDict):
|
||||
last_chunk: NotRequired[str]
|
||||
num_chunks: NotRequired[int]
|
||||
|
||||
# Will invoke a subgraph as a function
|
||||
def parent_node(state: SomeCustomState, config: RunnableConfig) -> dict:
|
||||
"""Node that runs the subgraph."""
|
||||
msgs = {"messages": [("user", "What is the weather in Tokyo?")]}
|
||||
events = []
|
||||
for event in compiled_subgraph.stream(msgs, config, stream_mode="messages"):
|
||||
events.append(event)
|
||||
ai_msg_chunks = [ai_msg_chunk for ai_msg_chunk, _ in events]
|
||||
return {
|
||||
"last_chunk": ai_msg_chunks[-1],
|
||||
"num_chunks": len(ai_msg_chunks),
|
||||
}
|
||||
|
||||
# Build the main workflow
|
||||
workflow = StateGraph(SomeCustomState)
|
||||
workflow.add_node("subgraph", parent_node)
|
||||
workflow.add_edge(START, "subgraph")
|
||||
compiled_workflow = workflow.compile()
|
||||
|
||||
# Test the basic functionality
|
||||
result = compiled_workflow.invoke({})
|
||||
|
||||
assert result["last_chunk"].content == "today."
|
||||
assert result["num_chunks"] == 9
|
||||
|
||||
@@ -26,7 +26,7 @@ from langchain_core.utils.aiter import aclosing
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
from pytest_mock import MockerFixture
|
||||
from syrupy import SnapshotAssertion
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph._internal._constants import CONFIG_KEY_NODE_FINISHED, ERROR, PULL
|
||||
from langgraph.cache.base import BaseCache
|
||||
@@ -1908,7 +1908,6 @@ async def test_pending_writes_resume(
|
||||
"branch:to:two": AnyVersion(),
|
||||
},
|
||||
"channel_values": {"value": 6},
|
||||
"updated_channels": ["value"],
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
@@ -1956,7 +1955,6 @@ async def test_pending_writes_resume(
|
||||
"branch:to:one": None,
|
||||
"branch:to:two": None,
|
||||
},
|
||||
"updated_channels": ["branch:to:one", "branch:to:two", "value"],
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
@@ -2004,7 +2002,6 @@ async def test_pending_writes_resume(
|
||||
"__start__": AnyVersion(),
|
||||
},
|
||||
"channel_values": {"__start__": {"value": 1}},
|
||||
"updated_channels": ["__start__"],
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
@@ -9053,57 +9050,3 @@ async def test_fork_and_update_task_results(
|
||||
],
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
async def test_subgraph_streaming_async() -> None:
|
||||
"""Test subgraph streaming when used as a node in async version"""
|
||||
|
||||
# Create a fake chat model that returns a simple response
|
||||
model = GenericFakeChatModel(messages=iter(["The weather is sunny today."]))
|
||||
|
||||
# Create a subgraph that uses the fake chat model
|
||||
async def call_model_node(
|
||||
state: MessagesState, config: RunnableConfig
|
||||
) -> MessagesState:
|
||||
"""Node that calls the model with the last message."""
|
||||
messages = state["messages"]
|
||||
last_message = messages[-1].content if messages else ""
|
||||
response = await model.ainvoke([("user", last_message)], config)
|
||||
return {"messages": [response]}
|
||||
|
||||
# Build the subgraph
|
||||
subgraph = StateGraph(MessagesState)
|
||||
subgraph.add_node("call_model", call_model_node)
|
||||
subgraph.add_edge(START, "call_model")
|
||||
compiled_subgraph = subgraph.compile()
|
||||
|
||||
class SomeCustomState(TypedDict):
|
||||
last_chunk: NotRequired[str]
|
||||
num_chunks: NotRequired[int]
|
||||
|
||||
# Will invoke a subgraph as a function
|
||||
async def parent_node(state: SomeCustomState, config: RunnableConfig) -> dict:
|
||||
"""Node that runs the subgraph."""
|
||||
msgs = {"messages": [("user", "What is the weather in Tokyo?")]}
|
||||
events = []
|
||||
async for event in compiled_subgraph.astream(
|
||||
msgs, config, stream_mode="messages"
|
||||
):
|
||||
events.append(event)
|
||||
ai_msg_chunks = [ai_msg_chunk for ai_msg_chunk, _ in events]
|
||||
return {
|
||||
"last_chunk": ai_msg_chunks[-1],
|
||||
"num_chunks": len(ai_msg_chunks),
|
||||
}
|
||||
|
||||
# Build the main workflow
|
||||
workflow = StateGraph(SomeCustomState)
|
||||
workflow.add_node("subgraph", parent_node)
|
||||
workflow.add_edge(START, "subgraph")
|
||||
compiled_workflow = workflow.compile()
|
||||
|
||||
# Test the basic functionality
|
||||
result = await compiled_workflow.ainvoke({})
|
||||
|
||||
assert result["last_chunk"].content == "today."
|
||||
assert result["num_chunks"] == 9
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.graph import END, START, StateGraph
|
||||
@@ -108,284 +106,3 @@ def test_runtime_propogated_to_subgraph() -> None:
|
||||
context = Context(username="Alice")
|
||||
result = graph.invoke({}, context=context)
|
||||
assert result == {"subgraph": "Alice!", "main": "Alice!"}
|
||||
|
||||
|
||||
def test_context_coercion_dataclass() -> None:
|
||||
"""Test that dict context is coerced to dataclass."""
|
||||
|
||||
@dataclass
|
||||
class Context:
|
||||
api_key: str
|
||||
timeout: int = 30
|
||||
|
||||
class State(TypedDict):
|
||||
message: str
|
||||
|
||||
def node_with_context(state: State, runtime: Runtime[Context]) -> dict[str, Any]:
|
||||
return {
|
||||
"message": f"api_key: {runtime.context.api_key}, timeout: {runtime.context.timeout}"
|
||||
}
|
||||
|
||||
graph = StateGraph(state_schema=State, context_schema=Context)
|
||||
graph.add_node("node", node_with_context)
|
||||
graph.add_edge(START, "node")
|
||||
graph.add_edge("node", END)
|
||||
compiled = graph.compile()
|
||||
|
||||
# Test dict coercion with all fields
|
||||
result = compiled.invoke(
|
||||
{"message": "test"}, context={"api_key": "sk_test", "timeout": 60}
|
||||
)
|
||||
assert result == {"message": "api_key: sk_test, timeout: 60"}
|
||||
|
||||
# Test dict coercion with default field
|
||||
result = compiled.invoke({"message": "test"}, context={"api_key": "sk_test2"})
|
||||
assert result == {"message": "api_key: sk_test2, timeout: 30"}
|
||||
|
||||
# Test with actual dataclass instance (should still work)
|
||||
result = compiled.invoke(
|
||||
{"message": "test"}, context=Context(api_key="sk_test3", timeout=90)
|
||||
)
|
||||
assert result == {"message": "api_key: sk_test3, timeout: 90"}
|
||||
|
||||
|
||||
def test_context_coercion_pydantic() -> None:
|
||||
"""Test that dict context is coerced to Pydantic model."""
|
||||
|
||||
class Context(BaseModel):
|
||||
api_key: str
|
||||
timeout: int = 30
|
||||
tags: list[str] = []
|
||||
|
||||
class State(TypedDict):
|
||||
message: str
|
||||
|
||||
def node_with_context(state: State, runtime: Runtime[Context]) -> dict[str, Any]:
|
||||
return {
|
||||
"message": f"api_key: {runtime.context.api_key}, timeout: {runtime.context.timeout}, tags: {runtime.context.tags}"
|
||||
}
|
||||
|
||||
graph = StateGraph(state_schema=State, context_schema=Context)
|
||||
graph.add_node("node", node_with_context)
|
||||
graph.add_edge(START, "node")
|
||||
graph.add_edge("node", END)
|
||||
compiled = graph.compile()
|
||||
|
||||
# Test dict coercion with all fields
|
||||
result = compiled.invoke(
|
||||
{"message": "test"},
|
||||
context={"api_key": "sk_test", "timeout": 60, "tags": ["prod", "v2"]},
|
||||
)
|
||||
assert result == {"message": "api_key: sk_test, timeout: 60, tags: ['prod', 'v2']"}
|
||||
|
||||
# Test dict coercion with defaults
|
||||
result = compiled.invoke({"message": "test"}, context={"api_key": "sk_test2"})
|
||||
assert result == {"message": "api_key: sk_test2, timeout: 30, tags: []"}
|
||||
|
||||
# Test with actual Pydantic instance (should still work)
|
||||
result = compiled.invoke(
|
||||
{"message": "test"},
|
||||
context=Context(api_key="sk_test3", timeout=90, tags=["test"]),
|
||||
)
|
||||
assert result == {"message": "api_key: sk_test3, timeout: 90, tags: ['test']"}
|
||||
|
||||
|
||||
def test_context_coercion_typeddict() -> None:
|
||||
"""Test that dict context with TypedDict schema passes through as-is."""
|
||||
|
||||
class Context(TypedDict):
|
||||
api_key: str
|
||||
timeout: int
|
||||
|
||||
class State(TypedDict):
|
||||
message: str
|
||||
|
||||
def node_with_context(state: State, runtime: Runtime[Context]) -> dict[str, Any]:
|
||||
# TypedDict context is just a dict at runtime
|
||||
return {
|
||||
"message": f"api_key: {runtime.context['api_key']}, timeout: {runtime.context['timeout']}"
|
||||
}
|
||||
|
||||
graph = StateGraph(state_schema=State, context_schema=Context)
|
||||
graph.add_node("node", node_with_context)
|
||||
graph.add_edge(START, "node")
|
||||
graph.add_edge("node", END)
|
||||
compiled = graph.compile()
|
||||
|
||||
# Test dict passes through for TypedDict
|
||||
result = compiled.invoke(
|
||||
{"message": "test"}, context={"api_key": "sk_test", "timeout": 60}
|
||||
)
|
||||
assert result == {"message": "api_key: sk_test, timeout: 60"}
|
||||
|
||||
|
||||
def test_context_coercion_none() -> None:
|
||||
"""Test that None context is handled properly."""
|
||||
|
||||
@dataclass
|
||||
class Context:
|
||||
api_key: str
|
||||
|
||||
class State(TypedDict):
|
||||
message: str
|
||||
|
||||
def node_without_context(state: State, runtime: Runtime[Context]) -> dict[str, Any]:
|
||||
# Should be None when no context provided
|
||||
return {"message": f"context is None: {runtime.context is None}"}
|
||||
|
||||
graph = StateGraph(state_schema=State, context_schema=Context)
|
||||
graph.add_node("node", node_without_context)
|
||||
graph.add_edge(START, "node")
|
||||
graph.add_edge("node", END)
|
||||
compiled = graph.compile()
|
||||
|
||||
# Test with None context
|
||||
result = compiled.invoke({"message": "test"}, context=None)
|
||||
assert result == {"message": "context is None: True"}
|
||||
|
||||
# Test without context parameter (defaults to None)
|
||||
result = compiled.invoke({"message": "test"})
|
||||
assert result == {"message": "context is None: True"}
|
||||
|
||||
|
||||
def test_context_coercion_errors() -> None:
|
||||
"""Test error handling for invalid context."""
|
||||
|
||||
@dataclass
|
||||
class Context:
|
||||
api_key: str # Required field
|
||||
|
||||
class State(TypedDict):
|
||||
message: str
|
||||
|
||||
def node_with_context(state: State, runtime: Runtime[Context]) -> dict[str, Any]:
|
||||
return {"message": "should not reach here"}
|
||||
|
||||
graph = StateGraph(state_schema=State, context_schema=Context)
|
||||
graph.add_node("node", node_with_context)
|
||||
graph.add_edge(START, "node")
|
||||
graph.add_edge("node", END)
|
||||
compiled = graph.compile()
|
||||
|
||||
# Test missing required field
|
||||
with pytest.raises(TypeError):
|
||||
compiled.invoke({"message": "test"}, context={"timeout": 60})
|
||||
|
||||
# Test invalid dict keys
|
||||
with pytest.raises(TypeError):
|
||||
compiled.invoke(
|
||||
{"message": "test"}, context={"api_key": "test", "invalid_field": "value"}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_context_coercion_async() -> None:
|
||||
"""Test context coercion with async methods."""
|
||||
|
||||
@dataclass
|
||||
class Context:
|
||||
api_key: str
|
||||
async_mode: bool = True
|
||||
|
||||
class State(TypedDict):
|
||||
message: str
|
||||
|
||||
async def async_node(state: State, runtime: Runtime[Context]) -> dict[str, Any]:
|
||||
return {
|
||||
"message": f"async api_key: {runtime.context.api_key}, async_mode: {runtime.context.async_mode}"
|
||||
}
|
||||
|
||||
graph = StateGraph(state_schema=State, context_schema=Context)
|
||||
graph.add_node("node", async_node)
|
||||
graph.add_edge(START, "node")
|
||||
graph.add_edge("node", END)
|
||||
compiled = graph.compile()
|
||||
|
||||
# Test dict coercion with ainvoke
|
||||
result = await compiled.ainvoke(
|
||||
{"message": "test"}, context={"api_key": "sk_async", "async_mode": False}
|
||||
)
|
||||
assert result == {"message": "async api_key: sk_async, async_mode: False"}
|
||||
|
||||
# Test dict coercion with astream
|
||||
chunks = []
|
||||
async for chunk in compiled.astream(
|
||||
{"message": "test"}, context={"api_key": "sk_stream"}
|
||||
):
|
||||
chunks.append(chunk)
|
||||
|
||||
# Find the chunk with our node output
|
||||
node_output = None
|
||||
for chunk in chunks:
|
||||
if "node" in chunk:
|
||||
node_output = chunk["node"]
|
||||
break
|
||||
|
||||
assert node_output == {"message": "async api_key: sk_stream, async_mode: True"}
|
||||
|
||||
|
||||
def test_context_coercion_stream() -> None:
|
||||
"""Test context coercion with sync stream method."""
|
||||
|
||||
@dataclass
|
||||
class Context:
|
||||
api_key: str
|
||||
stream_mode: str = "default"
|
||||
|
||||
class State(TypedDict):
|
||||
message: str
|
||||
|
||||
def node_with_context(state: State, runtime: Runtime[Context]) -> dict[str, Any]:
|
||||
return {
|
||||
"message": f"stream api_key: {runtime.context.api_key}, mode: {runtime.context.stream_mode}"
|
||||
}
|
||||
|
||||
graph = StateGraph(state_schema=State, context_schema=Context)
|
||||
graph.add_node("node", node_with_context)
|
||||
graph.add_edge(START, "node")
|
||||
graph.add_edge("node", END)
|
||||
compiled = graph.compile()
|
||||
|
||||
# Test dict coercion with stream
|
||||
chunks = []
|
||||
for chunk in compiled.stream(
|
||||
{"message": "test"}, context={"api_key": "sk_stream", "stream_mode": "fast"}
|
||||
):
|
||||
chunks.append(chunk)
|
||||
|
||||
# Find the chunk with our node output
|
||||
node_output = None
|
||||
for chunk in chunks:
|
||||
if "node" in chunk:
|
||||
node_output = chunk["node"]
|
||||
break
|
||||
|
||||
assert node_output == {"message": "stream api_key: sk_stream, mode: fast"}
|
||||
|
||||
|
||||
def test_context_coercion_pydantic_validation_errors() -> None:
|
||||
"""Test that Pydantic validation errors are raised."""
|
||||
|
||||
class Context(BaseModel):
|
||||
api_key: str
|
||||
timeout: int
|
||||
|
||||
class State(TypedDict):
|
||||
message: str
|
||||
|
||||
def node_with_context(state: State, runtime: Runtime[Context]) -> dict[str, Any]:
|
||||
return {
|
||||
"message": f"api_key: {runtime.context.api_key}, timeout: {runtime.context.timeout}"
|
||||
}
|
||||
|
||||
graph = StateGraph(state_schema=State, context_schema=Context)
|
||||
graph.add_node("node", node_with_context)
|
||||
graph.add_edge(START, "node")
|
||||
graph.add_edge("node", END)
|
||||
|
||||
compiled = graph.compile()
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
compiled.invoke(
|
||||
{"message": "test"}, context={"api_key": "sk_test", "timeout": "not_an_int"}
|
||||
)
|
||||
|
||||
Generated
+2
-26
@@ -119,15 +119,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl", hash = "sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943", size = 6069, upload-time = "2025-03-16T17:25:35.422Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-timeout"
|
||||
version = "5.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "25.3.0"
|
||||
@@ -1201,7 +1192,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "0.6.4"
|
||||
version = "0.6.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -1234,7 +1225,6 @@ dev = [
|
||||
{ name = "pytest-repeat" },
|
||||
{ name = "pytest-watcher" },
|
||||
{ name = "pytest-xdist", extra = ["psutil"] },
|
||||
{ name = "redis" },
|
||||
{ name = "ruff" },
|
||||
{ name = "syrupy" },
|
||||
{ name = "types-requests" },
|
||||
@@ -1273,7 +1263,6 @@ dev = [
|
||||
{ name = "pytest-repeat" },
|
||||
{ name = "pytest-watcher" },
|
||||
{ name = "pytest-xdist", extras = ["psutil"] },
|
||||
{ name = "redis" },
|
||||
{ name = "ruff" },
|
||||
{ name = "syrupy" },
|
||||
{ name = "types-requests" },
|
||||
@@ -1337,7 +1326,6 @@ dev = [
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
{ name = "pytest-watcher" },
|
||||
{ name = "redis" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
@@ -1445,7 +1433,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "0.6.4"
|
||||
version = "0.6.1"
|
||||
source = { editable = "../prebuilt" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -2640,18 +2628,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/8b/619a9ee2fa4d3c724fbadde946427735ade64da03894b071bbdc3b789d83/pyzmq-27.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:096af9e133fec3a72108ddefba1e42985cb3639e9de52cfd336b6fc23aa083e9", size = 544715, upload-time = "2025-06-13T14:09:05.579Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "6.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "async-timeout", marker = "python_full_version < '3.11.3'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/cd/030274634a1a052b708756016283ea3d84e91ae45f74d7f5dcf55d753a0f/redis-6.3.0.tar.gz", hash = "sha256:3000dbe532babfb0999cdab7b3e5744bcb23e51923febcfaeb52c8cfb29632ef", size = 4647275, upload-time = "2025-08-05T08:12:31.648Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/a7/2fe45801534a187543fc45d28b3844d84559c1589255bc2ece30d92dc205/redis-6.3.0-py3-none-any.whl", hash = "sha256:92f079d656ded871535e099080f70fab8e75273c0236797126ac60242d638e9b", size = 280018, upload-time = "2025-08-05T08:12:30.093Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "referencing"
|
||||
version = "0.36.2"
|
||||
|
||||
+8
-11
@@ -7,11 +7,11 @@ all: help
|
||||
# TESTING AND COVERAGE
|
||||
######################
|
||||
|
||||
start-services:
|
||||
docker compose -f tests/compose-postgres.yml -f tests/compose-redis.yml up -V --force-recreate --wait --remove-orphans
|
||||
start-postgres:
|
||||
docker compose -f tests/compose-postgres.yml up -V --force-recreate --wait --remove-orphans
|
||||
|
||||
stop-services:
|
||||
docker compose -f tests/compose-postgres.yml -f tests/compose-redis.yml down -v
|
||||
stop-postgres:
|
||||
docker compose -f tests/compose-postgres.yml down -v
|
||||
|
||||
TEST ?= .
|
||||
|
||||
@@ -19,20 +19,17 @@ test-fast:
|
||||
LANGGRAPH_TEST_FAST=1 uv run pytest $(TEST)
|
||||
|
||||
test:
|
||||
make start-services && LANGGRAPH_TEST_FAST=0 uv run pytest $(TEST); \
|
||||
make start-postgres && LANGGRAPH_TEST_FAST=0 uv run pytest $(TEST); \
|
||||
EXIT_CODE=$$?; \
|
||||
make stop-services; \
|
||||
make stop-postgres; \
|
||||
exit $$EXIT_CODE
|
||||
|
||||
test_watch:
|
||||
make start-services && LANGGRAPH_TEST_FAST=0 uv run ptw $(TEST); \
|
||||
make start-postgres && LANGGRAPH_TEST_FAST=0 uv run ptw $(TEST); \
|
||||
EXIT_CODE=$$?; \
|
||||
make stop-services; \
|
||||
make stop-postgres; \
|
||||
exit $$EXIT_CODE
|
||||
|
||||
snapshot_upate:
|
||||
LANGGRAPH_TEST_FAST=1 uv run pytest --snapshot-update $(TEST)
|
||||
|
||||
######################
|
||||
# LINTING AND FORMATTING
|
||||
######################
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
from typing import Any, Literal, TypedDict
|
||||
|
||||
from langchain_core.messages import ToolCall
|
||||
|
||||
|
||||
class ToolCallWithContext(TypedDict):
|
||||
"""ToolCall with additional context for graph state.
|
||||
|
||||
This is an internal data-structure meant to help the ToolNode accept
|
||||
tools calls with additional context (e.g. state) when dispatched using the
|
||||
`Send` API.
|
||||
|
||||
The Send API is used in create_react_agent to be able to distribute the tool
|
||||
calls in parallel and support human-in-the-loop workflows where graph execution
|
||||
may be paused for an indefinite time.
|
||||
"""
|
||||
|
||||
tool_call: ToolCall
|
||||
__type: Literal["tool_call_with_context"]
|
||||
"""Type to parameterize the payload.
|
||||
|
||||
Using "__" as a prefix to be defensive against potential name collisions with
|
||||
regular user state.
|
||||
"""
|
||||
state: Any
|
||||
"""The state is provided as additional context."""
|
||||
File diff suppressed because it is too large
Load Diff
@@ -74,6 +74,7 @@ from typing_extensions import Annotated, get_args, get_origin
|
||||
from langgraph._internal._runnable import RunnableCallable
|
||||
from langgraph.errors import GraphBubbleUp
|
||||
from langgraph.graph.message import REMOVE_ALL_MESSAGES
|
||||
from langgraph.prebuilt._internal import ToolCallWithContext
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import Command, Send
|
||||
|
||||
@@ -340,22 +341,14 @@ class ToolNode(RunnableCallable):
|
||||
self.tools_by_name: dict[str, BaseTool] = {}
|
||||
self.tool_to_state_args: dict[str, dict[str, Optional[str]]] = {}
|
||||
self.tool_to_store_arg: dict[str, Optional[str]] = {}
|
||||
self.structured_output_tools: list[str] = []
|
||||
self.handle_tool_errors = handle_tool_errors
|
||||
self.messages_key = messages_key
|
||||
for tool_ in tools:
|
||||
if inspect.isclass(tool_) and issubclass(tool_, BaseModel):
|
||||
# Handle Pydantic model classes as structured output tools
|
||||
self.tools_by_name[tool_.__name__] = tool_
|
||||
self.tool_to_state_args[tool_.__name__] = {}
|
||||
self.tool_to_store_arg[tool_.__name__] = None
|
||||
self.structured_output_tools.append(tool_.__name__)
|
||||
else:
|
||||
if not isinstance(tool_, BaseTool):
|
||||
tool_ = create_tool(tool_)
|
||||
self.tools_by_name[tool_.name] = tool_
|
||||
self.tool_to_state_args[tool_.name] = _get_state_args(tool_)
|
||||
self.tool_to_store_arg[tool_.name] = _get_store_arg(tool_)
|
||||
if not isinstance(tool_, BaseTool):
|
||||
tool_ = create_tool(tool_)
|
||||
self.tools_by_name[tool_.name] = tool_
|
||||
self.tool_to_state_args[tool_.name] = _get_state_args(tool_)
|
||||
self.tool_to_store_arg[tool_.name] = _get_store_arg(tool_)
|
||||
|
||||
def _func(
|
||||
self,
|
||||
@@ -368,7 +361,8 @@ class ToolNode(RunnableCallable):
|
||||
*,
|
||||
store: Optional[BaseStore],
|
||||
) -> Any:
|
||||
tool_calls, input_type = self._parse_input(input, store)
|
||||
tool_calls, input_type = self._parse_input(input)
|
||||
tool_calls = [self.inject_tool_args(call, input, store) for call in tool_calls]
|
||||
config_list = get_config_list(config, len(tool_calls))
|
||||
input_types = [input_type] * len(tool_calls)
|
||||
with get_executor_for_config(config) as executor:
|
||||
@@ -389,7 +383,8 @@ class ToolNode(RunnableCallable):
|
||||
*,
|
||||
store: Optional[BaseStore],
|
||||
) -> Any:
|
||||
tool_calls, input_type = self._parse_input(input, store)
|
||||
tool_calls, input_type = self._parse_input(input)
|
||||
tool_calls = [self.inject_tool_args(call, input, store) for call in tool_calls]
|
||||
outputs = await asyncio.gather(
|
||||
*(self._arun_one(call, input_type, config) for call in tool_calls)
|
||||
)
|
||||
@@ -398,7 +393,7 @@ class ToolNode(RunnableCallable):
|
||||
|
||||
def _combine_tool_outputs(
|
||||
self,
|
||||
outputs: list[Union[ToolMessage, Command]],
|
||||
outputs: list[ToolMessage],
|
||||
input_type: Literal["list", "dict", "tool_calls"],
|
||||
) -> list[Union[Command, list[ToolMessage], dict[str, list[ToolMessage]]]]:
|
||||
# preserve existing behavior for non-command tool outputs for backwards
|
||||
@@ -445,27 +440,10 @@ class ToolNode(RunnableCallable):
|
||||
call: ToolCall,
|
||||
input_type: Literal["list", "dict", "tool_calls"],
|
||||
config: RunnableConfig,
|
||||
) -> Union[ToolMessage, Command]:
|
||||
) -> ToolMessage:
|
||||
"""Run a single tool call synchronously."""
|
||||
if invalid_tool_message := self._validate_tool_call(call):
|
||||
return invalid_tool_message
|
||||
|
||||
# Handle structured output tools
|
||||
if call["name"] in self.structured_output_tools:
|
||||
response_schema = self.tools_by_name[call["name"]]
|
||||
return Command(
|
||||
update={
|
||||
"messages": [
|
||||
ToolMessage(
|
||||
content=msg_content_output(call["args"]),
|
||||
name=call["name"],
|
||||
tool_call_id=call["id"],
|
||||
)
|
||||
],
|
||||
"structured_response": response_schema(**call["args"]),
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
call_args = {**call, **{"type": "tool_call"}}
|
||||
response = self.tools_by_name[call["name"]].invoke(call_args, config)
|
||||
@@ -518,36 +496,19 @@ class ToolNode(RunnableCallable):
|
||||
call: ToolCall,
|
||||
input_type: Literal["list", "dict", "tool_calls"],
|
||||
config: RunnableConfig,
|
||||
) -> Union[ToolMessage, Command]:
|
||||
) -> ToolMessage:
|
||||
"""Run a single tool call asynchronously."""
|
||||
if invalid_tool_message := self._validate_tool_call(call):
|
||||
return invalid_tool_message
|
||||
|
||||
# Handle structured output tools
|
||||
if call["name"] in self.structured_output_tools:
|
||||
response_schema = self.tools_by_name[call["name"]]
|
||||
return Command(
|
||||
update={
|
||||
"messages": [
|
||||
ToolMessage(
|
||||
content=msg_content_output(call["args"]),
|
||||
name=call["name"],
|
||||
tool_call_id=call["id"],
|
||||
)
|
||||
],
|
||||
"structured_response": response_schema(**call["args"]),
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
call_args = {**call, **{"type": "tool_call"}}
|
||||
response = await self.tools_by_name[call["name"]].ainvoke(call_args, config)
|
||||
input = {**call, **{"type": "tool_call"}}
|
||||
response = await self.tools_by_name[call["name"]].ainvoke(input, config)
|
||||
|
||||
# GraphInterrupt is a special exception that will always be raised.
|
||||
# It can be triggered in the following scenarios,
|
||||
# Where GraphInterrupt(GraphBubbleUp) is raised from an `interrupt` invocation most commonly:
|
||||
# (1) a GraphInterrupt is raised inside a tool
|
||||
# (2) a GraphInterrupt is raised inside a graph node for a graph called as a tool
|
||||
# It can be triggered in the following scenarios:
|
||||
# (1) a NodeInterrupt is raised inside a tool
|
||||
# (2) a NodeInterrupt is raised inside a graph node for a graph called as a tool
|
||||
# (3) a GraphInterrupt is raised when a subgraph is interrupted inside a graph called as a tool
|
||||
# (2 and 3 can happen in a "supervisor w/ tools" multi-agent architecture)
|
||||
except GraphBubbleUp as e:
|
||||
@@ -594,7 +555,6 @@ class ToolNode(RunnableCallable):
|
||||
dict[str, Any],
|
||||
BaseModel,
|
||||
],
|
||||
store: Optional[BaseStore],
|
||||
) -> Tuple[list[ToolCall], Literal["list", "dict", "tool_calls"]]:
|
||||
input_type: Literal["list", "dict", "tool_calls"]
|
||||
if isinstance(input, list):
|
||||
@@ -605,6 +565,15 @@ class ToolNode(RunnableCallable):
|
||||
else:
|
||||
input_type = "list"
|
||||
messages = input
|
||||
elif (
|
||||
isinstance(input, dict) and input.get("__type") == "tool_call_with_context"
|
||||
):
|
||||
# mypy will not be able to type narrow correctly since the signature
|
||||
# for input contains dict[str, Any]. We'd need to type dict[str, Any]
|
||||
# before we can apply correct typing.
|
||||
input = cast(ToolCallWithContext, input) # type: ignore[assignment]
|
||||
input_type = "tool_calls"
|
||||
return [input["tool_call"]], input_type
|
||||
elif isinstance(input, dict) and (messages := input.get(self.messages_key, [])):
|
||||
input_type = "dict"
|
||||
elif messages := getattr(input, self.messages_key, []):
|
||||
@@ -620,10 +589,7 @@ class ToolNode(RunnableCallable):
|
||||
except StopIteration:
|
||||
raise ValueError("No AIMessage found in input")
|
||||
|
||||
tool_calls = [
|
||||
self.inject_tool_args(call, input, store)
|
||||
for call in latest_ai_message.tool_calls
|
||||
]
|
||||
tool_calls = [call for call in latest_ai_message.tool_calls]
|
||||
return tool_calls, input_type
|
||||
|
||||
def _validate_tool_call(self, call: ToolCall) -> Optional[ToolMessage]:
|
||||
@@ -666,14 +632,19 @@ class ToolNode(RunnableCallable):
|
||||
err_msg += f" State should contain fields {required_fields_str}."
|
||||
raise ValueError(err_msg)
|
||||
|
||||
if isinstance(input, dict):
|
||||
if isinstance(input, dict) and input.get("__type") == "tool_call_with_context":
|
||||
state = input["state"]
|
||||
else:
|
||||
state = input
|
||||
|
||||
if isinstance(state, dict):
|
||||
tool_state_args = {
|
||||
tool_arg: input[state_field] if state_field else input
|
||||
tool_arg: state[state_field] if state_field else state
|
||||
for tool_arg, state_field in state_args.items()
|
||||
}
|
||||
else:
|
||||
tool_state_args = {
|
||||
tool_arg: getattr(input, state_field) if state_field else input
|
||||
tool_arg: getattr(state, state_field) if state_field else state
|
||||
for tool_arg, state_field in state_args.items()
|
||||
}
|
||||
|
||||
@@ -831,6 +802,7 @@ def tools_condition(
|
||||
|
||||
Args:
|
||||
state: The current graph state to examine for tool calls. Supported formats:
|
||||
- List of messages (for MessageGraph)
|
||||
- Dictionary containing a messages key (for StateGraph)
|
||||
- BaseModel instance with a messages attribute
|
||||
messages_key: The key or attribute name containing the message list in the state.
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
in a langchain graph. It applies a pydantic schema to tool_calls in the models' outputs,
|
||||
and returns a ToolMessage with the validated content. If the schema is not valid, it
|
||||
returns a ToolMessage with the error message. The ValidationNode can be used in a
|
||||
StateGraph with a "messages" key. If multiple tool calls are requested, they will be run in parallel.
|
||||
StateGraph with a "messages" key or in a MessageGraph. If multiple tool calls are
|
||||
requested, they will be run in parallel.
|
||||
"""
|
||||
|
||||
from typing import (
|
||||
@@ -48,7 +49,7 @@ def _default_format_error(
|
||||
class ValidationNode(RunnableCallable):
|
||||
"""A node that validates all tools requests from the last AIMessage.
|
||||
|
||||
It can be used either in StateGraph with a "messages" key.
|
||||
It can be used either in StateGraph with a "messages" key or in MessageGraph.
|
||||
|
||||
!!! note
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "0.6.4"
|
||||
version = "0.6.1"
|
||||
description = "Library with high-level APIs for creating and executing LangGraph agents and tools."
|
||||
authors = []
|
||||
requires-python = ">=3.9"
|
||||
|
||||
@@ -171,191 +171,3 @@
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_react_agent_graph_structure_with_individual_nodes[no_response_format-no_post_hook-no_pre_hook-no_tools]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> agent;
|
||||
agent --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_react_agent_graph_structure_with_individual_nodes[no_response_format-no_post_hook-no_pre_hook-two_tools]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> agent;
|
||||
agent -.-> __end__;
|
||||
agent -.-> tool;
|
||||
agent -.-> tool2;
|
||||
tool --> agent;
|
||||
tool2 --> agent;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_react_agent_graph_structure_with_individual_nodes[no_response_format-no_post_hook-with_pre_hook-no_tools]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> pre_model_hook;
|
||||
pre_model_hook --> agent;
|
||||
agent --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_react_agent_graph_structure_with_individual_nodes[no_response_format-no_post_hook-with_pre_hook-two_tools]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> pre_model_hook;
|
||||
agent -.-> __end__;
|
||||
agent -.-> tool;
|
||||
agent -.-> tool2;
|
||||
pre_model_hook --> agent;
|
||||
tool --> pre_model_hook;
|
||||
tool2 --> pre_model_hook;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_react_agent_graph_structure_with_individual_nodes[no_response_format-with_post_hook-no_pre_hook-no_tools]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> agent;
|
||||
agent --> post_model_hook;
|
||||
post_model_hook --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_react_agent_graph_structure_with_individual_nodes[no_response_format-with_post_hook-no_pre_hook-two_tools]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> agent;
|
||||
agent --> post_model_hook;
|
||||
post_model_hook -.-> __end__;
|
||||
post_model_hook -.-> agent;
|
||||
post_model_hook -.-> tool;
|
||||
post_model_hook -.-> tool2;
|
||||
tool --> agent;
|
||||
tool2 --> agent;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_react_agent_graph_structure_with_individual_nodes[no_response_format-with_post_hook-with_pre_hook-no_tools]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> pre_model_hook;
|
||||
agent --> post_model_hook;
|
||||
pre_model_hook --> agent;
|
||||
post_model_hook --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_react_agent_graph_structure_with_individual_nodes[no_response_format-with_post_hook-with_pre_hook-two_tools]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> pre_model_hook;
|
||||
agent --> post_model_hook;
|
||||
post_model_hook -.-> __end__;
|
||||
post_model_hook -.-> pre_model_hook;
|
||||
post_model_hook -.-> tool;
|
||||
post_model_hook -.-> tool2;
|
||||
pre_model_hook --> agent;
|
||||
tool --> pre_model_hook;
|
||||
tool2 --> pre_model_hook;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_react_agent_graph_structure_with_individual_nodes[with_response_format-no_post_hook-no_pre_hook-no_tools]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> agent;
|
||||
agent --> generate_structured_response;
|
||||
generate_structured_response --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_react_agent_graph_structure_with_individual_nodes[with_response_format-no_post_hook-no_pre_hook-two_tools]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> agent;
|
||||
agent -.-> generate_structured_response;
|
||||
agent -.-> tool;
|
||||
agent -.-> tool2;
|
||||
tool --> agent;
|
||||
tool2 --> agent;
|
||||
generate_structured_response --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_react_agent_graph_structure_with_individual_nodes[with_response_format-no_post_hook-with_pre_hook-no_tools]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> pre_model_hook;
|
||||
agent --> generate_structured_response;
|
||||
pre_model_hook --> agent;
|
||||
generate_structured_response --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_react_agent_graph_structure_with_individual_nodes[with_response_format-no_post_hook-with_pre_hook-two_tools]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> pre_model_hook;
|
||||
agent -.-> generate_structured_response;
|
||||
agent -.-> tool;
|
||||
agent -.-> tool2;
|
||||
pre_model_hook --> agent;
|
||||
tool --> pre_model_hook;
|
||||
tool2 --> pre_model_hook;
|
||||
generate_structured_response --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_react_agent_graph_structure_with_individual_nodes[with_response_format-with_post_hook-no_pre_hook-no_tools]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> agent;
|
||||
agent --> post_model_hook;
|
||||
post_model_hook --> generate_structured_response;
|
||||
generate_structured_response --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_react_agent_graph_structure_with_individual_nodes[with_response_format-with_post_hook-no_pre_hook-two_tools]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> agent;
|
||||
agent --> post_model_hook;
|
||||
post_model_hook -.-> agent;
|
||||
post_model_hook -.-> generate_structured_response;
|
||||
post_model_hook -.-> tool;
|
||||
post_model_hook -.-> tool2;
|
||||
tool --> agent;
|
||||
tool2 --> agent;
|
||||
generate_structured_response --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_react_agent_graph_structure_with_individual_nodes[with_response_format-with_post_hook-with_pre_hook-no_tools]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> pre_model_hook;
|
||||
agent --> post_model_hook;
|
||||
post_model_hook --> generate_structured_response;
|
||||
pre_model_hook --> agent;
|
||||
generate_structured_response --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_react_agent_graph_structure_with_individual_nodes[with_response_format-with_post_hook-with_pre_hook-two_tools]
|
||||
'''
|
||||
graph TD;
|
||||
__start__ --> pre_model_hook;
|
||||
agent --> post_model_hook;
|
||||
post_model_hook -.-> generate_structured_response;
|
||||
post_model_hook -.-> pre_model_hook;
|
||||
post_model_hook -.-> tool;
|
||||
post_model_hook -.-> tool2;
|
||||
pre_model_hook --> agent;
|
||||
tool --> pre_model_hook;
|
||||
tool2 --> pre_model_hook;
|
||||
generate_structured_response --> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
name: langgraph-tests-redis
|
||||
services:
|
||||
redis-test:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||||
healthcheck:
|
||||
test: redis-cli ping
|
||||
start_period: 10s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
interval: 5s
|
||||
start_interval: 1s
|
||||
tmpfs:
|
||||
- /data # Use tmpfs for faster testing
|
||||
@@ -1,41 +0,0 @@
|
||||
import pytest
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
from langgraph.warnings import LangGraphDeprecatedSinceV10
|
||||
from tests.model import FakeToolCallingModel
|
||||
|
||||
|
||||
class Config(TypedDict):
|
||||
model: str
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:`config_schema` is deprecated")
|
||||
@pytest.mark.filterwarnings("ignore:`get_config_jsonschema` is deprecated")
|
||||
def test_config_schema_deprecation() -> None:
|
||||
with pytest.warns(
|
||||
LangGraphDeprecatedSinceV10,
|
||||
match="`config_schema` is deprecated and will be removed. Please use `context_schema` instead.",
|
||||
):
|
||||
agent = create_react_agent(FakeToolCallingModel(), [], config_schema=Config)
|
||||
assert agent.context_schema == Config
|
||||
|
||||
with pytest.warns(
|
||||
LangGraphDeprecatedSinceV10,
|
||||
match="`config_schema` is deprecated. Use `get_context_jsonschema` for the relevant schema instead.",
|
||||
):
|
||||
assert agent.config_schema() is not None
|
||||
|
||||
with pytest.warns(
|
||||
LangGraphDeprecatedSinceV10,
|
||||
match="`get_config_jsonschema` is deprecated. Use `get_context_jsonschema` instead.",
|
||||
):
|
||||
assert agent.get_config_jsonschema() is not None
|
||||
|
||||
|
||||
def test_extra_kwargs_deprecation() -> None:
|
||||
with pytest.raises(
|
||||
TypeError,
|
||||
match="create_react_agent\(\) got unexpected keyword arguments: \{'extra': 'extra'\}",
|
||||
):
|
||||
create_react_agent(FakeToolCallingModel(), [], extra="extra")
|
||||
@@ -1,9 +1,14 @@
|
||||
import dataclasses
|
||||
import inspect
|
||||
import json
|
||||
from functools import partial
|
||||
from typing import (
|
||||
Annotated,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
@@ -11,6 +16,7 @@ import pytest
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
from langchain_core.messages import (
|
||||
AIMessage,
|
||||
AnyMessage,
|
||||
HumanMessage,
|
||||
MessageLikeRepresentation,
|
||||
RemoveMessage,
|
||||
@@ -18,18 +24,21 @@ from langchain_core.messages import (
|
||||
ToolCall,
|
||||
ToolMessage,
|
||||
)
|
||||
from langchain_core.runnables import RunnableConfig, RunnableLambda
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
from langchain_core.tools import InjectedToolCallId, ToolException
|
||||
from langchain_core.tools import tool as dec_tool
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.graph import START, MessagesState, StateGraph
|
||||
from langgraph.config import get_stream_writer
|
||||
from langgraph.graph import START, MessagesState, StateGraph, add_messages
|
||||
from langgraph.graph.message import REMOVE_ALL_MESSAGES
|
||||
from langgraph.prebuilt import (
|
||||
ToolNode,
|
||||
create_react_agent,
|
||||
tools_condition,
|
||||
)
|
||||
from langgraph.prebuilt.chat_agent_executor import (
|
||||
AgentState,
|
||||
@@ -175,7 +184,7 @@ def test_runnable_prompt():
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
|
||||
def test_prompt_with_store(version: Literal["v1", "v2"]):
|
||||
def test_prompt_with_store(version: str):
|
||||
def add(a: int, b: int):
|
||||
"""Adds a and b"""
|
||||
return a + b
|
||||
@@ -645,6 +654,124 @@ def test_react_agent_parallel_tool_calls(
|
||||
assert get_weather_execution_count == 1
|
||||
|
||||
|
||||
class _InjectStateSchema(TypedDict):
|
||||
messages: list
|
||||
foo: str
|
||||
|
||||
|
||||
class _InjectedStatePydanticSchema(BaseModelV1):
|
||||
messages: list
|
||||
foo: str
|
||||
|
||||
|
||||
class _InjectedStatePydanticV2Schema(BaseModel):
|
||||
messages: list
|
||||
foo: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _InjectedStateDataclassSchema:
|
||||
messages: list
|
||||
foo: str
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"schema_",
|
||||
[
|
||||
_InjectStateSchema,
|
||||
_InjectedStatePydanticSchema,
|
||||
_InjectedStatePydanticV2Schema,
|
||||
_InjectedStateDataclassSchema,
|
||||
],
|
||||
)
|
||||
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"]
|
||||
else:
|
||||
return getattr(state, "foo")
|
||||
|
||||
def tool2(some_val: int, state: Annotated[T, InjectedState()]) -> str:
|
||||
"""Tool 2 docstring."""
|
||||
if isinstance(state, dict):
|
||||
return state["foo"]
|
||||
else:
|
||||
return getattr(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])
|
||||
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"}))
|
||||
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},
|
||||
"id": "some 0",
|
||||
"type": "tool_call",
|
||||
}
|
||||
msg = AIMessage("hi?", tool_calls=[tool_call])
|
||||
result = node.invoke(schema_(**{"messages": [msg], "foo": ""}))
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "hi?"
|
||||
|
||||
result = node.invoke([msg])
|
||||
tool_message = result[-1]
|
||||
assert tool_message.content == "hi?"
|
||||
|
||||
|
||||
class AgentStateExtraKey(AgentState):
|
||||
foo: int
|
||||
|
||||
@@ -653,24 +780,14 @@ class AgentStateExtraKeyPydantic(AgentStatePydantic):
|
||||
foo: int
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["v1", "v2"])
|
||||
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
|
||||
@pytest.mark.parametrize(
|
||||
"state_schema", [AgentStateExtraKey, AgentStateExtraKeyPydantic]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"use_individual_tool_nodes",
|
||||
[False, True],
|
||||
ids=["single_tool_node", "node_per_tool"],
|
||||
)
|
||||
def test_create_react_agent_inject_vars(
|
||||
version: Literal["v1", "v2"],
|
||||
state_schema: StateSchemaType,
|
||||
use_individual_tool_nodes: bool,
|
||||
version: Literal["v1", "v2"], state_schema: StateSchemaType
|
||||
) -> None:
|
||||
"""Test that the agent can inject state and store into tool functions."""
|
||||
if version == "v1" and use_individual_tool_nodes:
|
||||
pytest.skip("v1 does not support individual tool nodes")
|
||||
|
||||
store = InMemoryStore()
|
||||
namespace = ("test",)
|
||||
store.put(namespace, "test_key", {"bar": 3})
|
||||
@@ -709,7 +826,6 @@ def test_create_react_agent_inject_vars(
|
||||
state_schema=state_schema,
|
||||
store=store,
|
||||
version=version,
|
||||
use_individual_tool_nodes=use_individual_tool_nodes,
|
||||
)
|
||||
result = agent.invoke({"messages": [{"role": "user", "content": "hi"}], "foo": 2})
|
||||
assert result["messages"] == [
|
||||
@@ -721,18 +837,137 @@ def test_create_react_agent_inject_vars(
|
||||
assert result["foo"] == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
|
||||
@pytest.mark.parametrize(
|
||||
"use_individual_tool_nodes",
|
||||
[False, True],
|
||||
ids=["single_tool_node", "node_per_tool"],
|
||||
)
|
||||
async def test_return_direct(
|
||||
version: Literal["v1", "v2"], use_individual_tool_nodes: bool
|
||||
) -> None:
|
||||
if version == "v1" and use_individual_tool_nodes:
|
||||
pytest.skip("v1 does not support individual tool nodes")
|
||||
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]}, 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"}, 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)]
|
||||
)
|
||||
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):
|
||||
"""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):
|
||||
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"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
|
||||
async def test_return_direct(version: str) -> None:
|
||||
@dec_tool(return_direct=True)
|
||||
def tool_return_direct(input: str) -> str:
|
||||
"""A tool that returns directly."""
|
||||
@@ -760,7 +995,6 @@ async def test_return_direct(
|
||||
model,
|
||||
[tool_return_direct, tool_normal],
|
||||
version=version,
|
||||
use_individual_tool_nodes=use_individual_tool_nodes,
|
||||
)
|
||||
|
||||
# Test direct return for tool_return_direct
|
||||
@@ -854,27 +1088,15 @@ def test__get_state_args() -> None:
|
||||
|
||||
|
||||
def test_inspect_react() -> None:
|
||||
"""Test that we can inspect the agent and its nodes."""
|
||||
model = FakeToolCallingModel(tool_calls=[])
|
||||
agent = create_react_agent(model, [])
|
||||
inspect.getclosurevars(agent.nodes["agent"].bound.func)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
|
||||
@pytest.mark.parametrize(
|
||||
"use_individual_tool_nodes",
|
||||
[False, True],
|
||||
ids=["single_tool_node", "node_per_tool"],
|
||||
)
|
||||
def test_react_with_subgraph_tools(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
version: Literal["v1", "v2"],
|
||||
use_individual_tool_nodes: bool,
|
||||
sync_checkpointer: BaseCheckpointSaver, version: Literal["v1", "v2"]
|
||||
) -> None:
|
||||
"""Test React agent with subgraph tools."""
|
||||
if version == "v1" and use_individual_tool_nodes:
|
||||
pytest.skip("v1 does not support individual tool nodes")
|
||||
|
||||
class State(TypedDict):
|
||||
a: int
|
||||
b: int
|
||||
@@ -930,7 +1152,6 @@ def test_react_with_subgraph_tools(
|
||||
tool_node,
|
||||
checkpointer=sync_checkpointer,
|
||||
version=version,
|
||||
use_individual_tool_nodes=use_individual_tool_nodes,
|
||||
)
|
||||
result = agent.invoke(
|
||||
{"messages": [HumanMessage(content="What's 2 + 3 and 2 * 3?")]},
|
||||
@@ -961,198 +1182,58 @@ def test_react_with_subgraph_tools(
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
|
||||
@pytest.mark.parametrize(
|
||||
"use_individual_tool_nodes",
|
||||
[False, True],
|
||||
ids=["single_tool_node", "node_per_tool"],
|
||||
)
|
||||
def test_react_agent_subgraph_streaming_sync(
|
||||
version: Literal["v1", "v2"], use_individual_tool_nodes: bool
|
||||
) -> None:
|
||||
"""Test React agent streaming when used as a subgraph node sync version"""
|
||||
if version == "v1" and use_individual_tool_nodes:
|
||||
pytest.skip("v1 does not support individual tool nodes")
|
||||
|
||||
def test_tool_node_stream_writer() -> None:
|
||||
@dec_tool
|
||||
def get_weather(city: str) -> str:
|
||||
"""Get the weather of a city."""
|
||||
return f"The weather of {city} is sunny."
|
||||
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})
|
||||
|
||||
# Create a React agent
|
||||
model = FakeToolCallingModel(
|
||||
tool_calls=[
|
||||
[{"args": {"city": "Tokyo"}, "id": "1", "name": "get_weather"}],
|
||||
[],
|
||||
]
|
||||
return x
|
||||
|
||||
tool_node = ToolNode([streaming_tool])
|
||||
graph = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("tools", tool_node)
|
||||
.add_edge(START, "tools")
|
||||
.compile()
|
||||
)
|
||||
|
||||
agent = create_react_agent(
|
||||
model,
|
||||
tools=[get_weather],
|
||||
prompt="You are a helpful travel assistant.",
|
||||
version=version,
|
||||
use_individual_tool_nodes=use_individual_tool_nodes,
|
||||
)
|
||||
tool_call = {
|
||||
"name": "streaming_tool",
|
||||
"args": {"x": 1},
|
||||
"id": "1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
inputs = {
|
||||
"messages": [AIMessage("", tool_calls=[tool_call])],
|
||||
}
|
||||
|
||||
# Create a subgraph that uses the React agent as a node
|
||||
def react_agent_node(state: MessagesState, config: RunnableConfig) -> MessagesState:
|
||||
"""Node that runs the React agent and collects streaming output."""
|
||||
collected_content = ""
|
||||
|
||||
# Stream the agent output and collect content
|
||||
for msg_chunk, msg_metadata in agent.stream(
|
||||
{"messages": [("user", state["messages"][-1].content)]},
|
||||
config,
|
||||
stream_mode="messages",
|
||||
):
|
||||
if hasattr(msg_chunk, "content") and msg_chunk.content:
|
||||
collected_content += msg_chunk.content
|
||||
|
||||
return {"messages": [("assistant", collected_content)]}
|
||||
|
||||
# Create the main workflow with the React agent as a subgraph node
|
||||
workflow = StateGraph(MessagesState)
|
||||
workflow.add_node("react_agent", react_agent_node)
|
||||
workflow.add_edge(START, "react_agent")
|
||||
workflow.add_edge("react_agent", "__end__")
|
||||
compiled_workflow = workflow.compile()
|
||||
|
||||
# Test the streaming functionality
|
||||
result = compiled_workflow.invoke(
|
||||
{"messages": [("user", "What is the weather in Tokyo?")]}
|
||||
)
|
||||
|
||||
# Verify the result contains expected structure
|
||||
assert len(result["messages"]) == 2
|
||||
assert result["messages"][0].content == "What is the weather in Tokyo?"
|
||||
assert "assistant" in str(result["messages"][1])
|
||||
|
||||
# Test streaming with subgraphs = True
|
||||
result = compiled_workflow.invoke(
|
||||
{"messages": [("user", "What is the weather in Tokyo?")]},
|
||||
subgraphs=True,
|
||||
)
|
||||
assert len(result["messages"]) == 2
|
||||
|
||||
events = []
|
||||
for event in compiled_workflow.stream(
|
||||
{"messages": [("user", "What is the weather in Tokyo?")]},
|
||||
stream_mode="messages",
|
||||
subgraphs=False,
|
||||
):
|
||||
events.append(event)
|
||||
|
||||
assert len(events) == 0
|
||||
|
||||
events = []
|
||||
for event in compiled_workflow.stream(
|
||||
{"messages": [("user", "What is the weather in Tokyo?")]},
|
||||
stream_mode="messages",
|
||||
subgraphs=True,
|
||||
):
|
||||
events.append(event)
|
||||
|
||||
assert len(events) == 3
|
||||
namespace, (msg, metadata) = events[0]
|
||||
# FakeToolCallingModel returns a single AIMessage with tool calls
|
||||
# The content of the AIMessage reflects the input message
|
||||
assert msg.content.startswith("You are a helpful travel assistant")
|
||||
namespace, (msg, metadata) = events[1] # ToolMessage
|
||||
assert msg.content.startswith("The weather of Tokyo is sunny.")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
|
||||
async def test_react_agent_subgraph_streaming(version: Literal["v1", "v2"]) -> None:
|
||||
"""Test React agent streaming when used as a subgraph node."""
|
||||
|
||||
@dec_tool
|
||||
def get_weather(city: str) -> str:
|
||||
"""Get the weather of a city."""
|
||||
return f"The weather of {city} is sunny."
|
||||
|
||||
# Create a React agent
|
||||
model = FakeToolCallingModel(
|
||||
tool_calls=[
|
||||
[{"args": {"city": "Tokyo"}, "id": "1", "name": "get_weather"}],
|
||||
[],
|
||||
]
|
||||
)
|
||||
|
||||
agent = create_react_agent(
|
||||
model,
|
||||
tools=[get_weather],
|
||||
prompt="You are a helpful travel assistant.",
|
||||
version=version,
|
||||
)
|
||||
|
||||
# Create a subgraph that uses the React agent as a node
|
||||
async def react_agent_node(
|
||||
state: MessagesState, config: RunnableConfig
|
||||
) -> MessagesState:
|
||||
"""Node that runs the React agent and collects streaming output."""
|
||||
collected_content = ""
|
||||
|
||||
# Stream the agent output and collect content
|
||||
async for msg_chunk, msg_metadata in agent.astream(
|
||||
{"messages": [("user", state["messages"][-1].content)]},
|
||||
config,
|
||||
stream_mode="messages",
|
||||
):
|
||||
if hasattr(msg_chunk, "content") and msg_chunk.content:
|
||||
collected_content += msg_chunk.content
|
||||
|
||||
return {"messages": [("assistant", collected_content)]}
|
||||
|
||||
# Create the main workflow with the React agent as a subgraph node
|
||||
workflow = StateGraph(MessagesState)
|
||||
workflow.add_node("react_agent", react_agent_node)
|
||||
workflow.add_edge(START, "react_agent")
|
||||
workflow.add_edge("react_agent", "__end__")
|
||||
compiled_workflow = workflow.compile()
|
||||
|
||||
# Test the streaming functionality
|
||||
result = await compiled_workflow.ainvoke(
|
||||
{"messages": [("user", "What is the weather in Tokyo?")]}
|
||||
)
|
||||
|
||||
# Verify the result contains expected structure
|
||||
assert len(result["messages"]) == 2
|
||||
assert result["messages"][0].content == "What is the weather in Tokyo?"
|
||||
assert "assistant" in str(result["messages"][1])
|
||||
|
||||
# Test streaming with subgraphs = True
|
||||
result = await compiled_workflow.ainvoke(
|
||||
{"messages": [("user", "What is the weather in Tokyo?")]},
|
||||
subgraphs=True,
|
||||
)
|
||||
assert len(result["messages"]) == 2
|
||||
|
||||
events = []
|
||||
async for event in compiled_workflow.astream(
|
||||
{"messages": [("user", "What is the weather in Tokyo?")]},
|
||||
stream_mode="messages",
|
||||
subgraphs=False,
|
||||
):
|
||||
events.append(event)
|
||||
|
||||
assert len(events) == 0
|
||||
|
||||
events = []
|
||||
async for event in compiled_workflow.astream(
|
||||
{"messages": [("user", "What is the weather in Tokyo?")]},
|
||||
stream_mode="messages",
|
||||
subgraphs=True,
|
||||
):
|
||||
events.append(event)
|
||||
|
||||
assert len(events) == 3
|
||||
namespace, (msg, metadata) = events[0]
|
||||
# FakeToolCallingModel returns a single AIMessage with tool calls
|
||||
# The content of the AIMessage reflects the input message
|
||||
assert msg.content.startswith("You are a helpful travel assistant")
|
||||
namespace, (msg, metadata) = events[1] # ToolMessage
|
||||
assert msg.content.startswith("The weather of Tokyo is sunny.")
|
||||
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",
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
|
||||
|
||||
@@ -15,11 +15,6 @@ def tool() -> None:
|
||||
...
|
||||
|
||||
|
||||
def tool2() -> None:
|
||||
"""Another testing tool."""
|
||||
...
|
||||
|
||||
|
||||
def pre_model_hook() -> None:
|
||||
"""Pre-model hook."""
|
||||
...
|
||||
@@ -54,44 +49,4 @@ def test_react_agent_graph_structure(
|
||||
post_model_hook=post_model_hook,
|
||||
response_format=response_format,
|
||||
)
|
||||
try:
|
||||
assert agent.get_graph().draw_mermaid(with_styles=False) == snapshot
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
"The graph structure has changed. Please update the snapshot."
|
||||
"Configuration used:\n"
|
||||
f"tools: {tools}, "
|
||||
f"pre_model_hook: {pre_model_hook}, "
|
||||
f"post_model_hook: {post_model_hook}, "
|
||||
f"response_format: {response_format}"
|
||||
) from e
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tools", [[], [tool, tool2]], ids=["no_tools", "two_tools"])
|
||||
@pytest.mark.parametrize(
|
||||
"pre_model_hook", [None, pre_model_hook], ids=["no_pre_hook", "with_pre_hook"]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"post_model_hook", [None, post_model_hook], ids=["no_post_hook", "with_post_hook"]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"response_format",
|
||||
[None, ResponseFormat],
|
||||
ids=["no_response_format", "with_response_format"],
|
||||
)
|
||||
def test_react_agent_graph_structure_with_individual_nodes(
|
||||
snapshot: SnapshotAssertion,
|
||||
tools: list[Callable],
|
||||
pre_model_hook: Union[Callable, None],
|
||||
post_model_hook: Union[Callable, None],
|
||||
response_format: Union[type[BaseModel], None],
|
||||
) -> None:
|
||||
agent = create_react_agent(
|
||||
model,
|
||||
tools=tools,
|
||||
pre_model_hook=pre_model_hook,
|
||||
post_model_hook=post_model_hook,
|
||||
response_format=response_format,
|
||||
use_individual_tool_nodes=True,
|
||||
)
|
||||
assert agent.get_graph().draw_mermaid(with_styles=False) == snapshot
|
||||
|
||||
@@ -1,49 +1,25 @@
|
||||
import dataclasses
|
||||
import json
|
||||
from functools import partial
|
||||
from typing import (
|
||||
Annotated,
|
||||
Any,
|
||||
List,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
import pytest
|
||||
from langchain_core.messages import (
|
||||
AIMessage,
|
||||
AnyMessage,
|
||||
HumanMessage,
|
||||
RemoveMessage,
|
||||
ToolCall,
|
||||
ToolMessage,
|
||||
)
|
||||
from langchain_core.tools import BaseTool, ToolException
|
||||
from langchain_core.tools import tool as dec_tool
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from pydantic.v1 import ValidationError as ValidationErrorV1
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
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.prebuilt import (
|
||||
ToolNode,
|
||||
)
|
||||
from langgraph.prebuilt.tool_node import (
|
||||
TOOL_CALL_ERROR_TEMPLATE,
|
||||
InjectedState,
|
||||
InjectedStore,
|
||||
tools_condition,
|
||||
)
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
from langgraph.graph.message import REMOVE_ALL_MESSAGES
|
||||
from langgraph.prebuilt import ToolNode
|
||||
from langgraph.prebuilt.tool_node import TOOL_CALL_ERROR_TEMPLATE
|
||||
from langgraph.types import Command, Send
|
||||
from tests.messages import _AnyIdHumanMessage, _AnyIdToolMessage
|
||||
from tests.model import FakeToolCallingModel
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
@@ -1180,372 +1156,3 @@ async def test_tool_node_command_remove_all_messages():
|
||||
command = result[0]
|
||||
assert isinstance(command, Command)
|
||||
assert command.update == {"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES)]}
|
||||
|
||||
|
||||
class _InjectStateSchema(TypedDict):
|
||||
messages: list
|
||||
foo: str
|
||||
|
||||
|
||||
class _InjectedStatePydanticSchema(BaseModelV1):
|
||||
messages: list
|
||||
foo: str
|
||||
|
||||
|
||||
class _InjectedStatePydanticV2Schema(BaseModel):
|
||||
messages: list
|
||||
foo: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _InjectedStateDataclassSchema:
|
||||
messages: list
|
||||
foo: str
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"schema_",
|
||||
[
|
||||
_InjectStateSchema,
|
||||
_InjectedStatePydanticSchema,
|
||||
_InjectedStatePydanticV2Schema,
|
||||
_InjectedStateDataclassSchema,
|
||||
],
|
||||
)
|
||||
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"]
|
||||
else:
|
||||
return getattr(state, "foo")
|
||||
|
||||
def tool2(some_val: int, state: Annotated[T, InjectedState()]) -> str:
|
||||
"""Tool 2 docstring."""
|
||||
if isinstance(state, dict):
|
||||
return state["foo"]
|
||||
else:
|
||||
return getattr(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])
|
||||
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"}))
|
||||
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},
|
||||
"id": "some 0",
|
||||
"type": "tool_call",
|
||||
}
|
||||
msg = AIMessage("hi?", tool_calls=[tool_call])
|
||||
result = node.invoke(schema_(**{"messages": [msg], "foo": ""}))
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "hi?"
|
||||
|
||||
result = node.invoke([msg])
|
||||
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]}, 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"}, 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)]
|
||||
)
|
||||
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):
|
||||
"""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):
|
||||
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",
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
async def test_structured_output_tools():
|
||||
"""Test that ToolNode handles Pydantic model classes as structured output tools."""
|
||||
|
||||
class OutputSchema(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
location: str
|
||||
|
||||
tool_node = ToolNode([OutputSchema])
|
||||
|
||||
# Test that the structured output tool is registered correctly
|
||||
assert "OutputSchema" in tool_node.tools_by_name
|
||||
assert "OutputSchema" in tool_node.structured_output_tools
|
||||
|
||||
# Create a tool call that matches the schema
|
||||
tool_call = {
|
||||
"name": "OutputSchema",
|
||||
"args": {"name": "Alice", "age": 30, "location": "NYC"},
|
||||
"id": "call_123",
|
||||
"type": "tool_call",
|
||||
}
|
||||
|
||||
# Test sync execution
|
||||
result = tool_node.invoke(
|
||||
{"messages": [AIMessage(content="", tool_calls=[tool_call])]}
|
||||
)
|
||||
|
||||
# Should return a Command with structured response
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
command = result[0]
|
||||
assert isinstance(command, Command)
|
||||
|
||||
# Check the update structure
|
||||
assert "messages" in command.update
|
||||
assert "structured_response" in command.update
|
||||
|
||||
# Check the tool message
|
||||
tool_message = command.update["messages"][0]
|
||||
assert isinstance(tool_message, ToolMessage)
|
||||
assert tool_message.name == "OutputSchema"
|
||||
assert tool_message.tool_call_id == "call_123"
|
||||
|
||||
# Check the structured response
|
||||
structured_response = command.update["structured_response"]
|
||||
assert isinstance(structured_response, OutputSchema)
|
||||
assert structured_response.name == "Alice"
|
||||
assert structured_response.age == 30
|
||||
assert structured_response.location == "NYC"
|
||||
|
||||
# Test async execution
|
||||
result_async = await tool_node.ainvoke(
|
||||
{"messages": [AIMessage(content="", tool_calls=[tool_call])]}
|
||||
)
|
||||
|
||||
# Should produce the same result
|
||||
assert isinstance(result_async, list)
|
||||
assert len(result_async) == 1
|
||||
command_async = result_async[0]
|
||||
assert isinstance(command_async, Command)
|
||||
assert "structured_response" in command_async.update
|
||||
|
||||
structured_response_async = command_async.update["structured_response"]
|
||||
assert isinstance(structured_response_async, OutputSchema)
|
||||
assert structured_response_async.name == "Alice"
|
||||
|
||||
Generated
+2
-4
@@ -316,7 +316,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "0.6.4"
|
||||
version = "0.6.1"
|
||||
source = { editable = "../langgraph" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -359,7 +359,6 @@ dev = [
|
||||
{ name = "pytest-repeat" },
|
||||
{ name = "pytest-watcher" },
|
||||
{ name = "pytest-xdist", extras = ["psutil"] },
|
||||
{ name = "redis" },
|
||||
{ name = "ruff" },
|
||||
{ name = "syrupy" },
|
||||
{ name = "types-requests" },
|
||||
@@ -393,7 +392,6 @@ dev = [
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
{ name = "pytest-watcher" },
|
||||
{ name = "redis" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
@@ -462,7 +460,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "0.6.4"
|
||||
version = "0.6.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
|
||||
Reference in New Issue
Block a user