Compare commits

..
Author SHA1 Message Date
Tat Dat DuongandHunter Lovell 602cddb155 Fix broken code 2025-07-28 14:56:27 -07:00
Tat Dat DuongandHunter Lovell a328d43c53 Update examples 2025-07-28 14:52:10 -07:00
Tat Dat DuongandHunter Lovell 0711fccd44 Update last message 2025-07-28 14:52:10 -07:00
Tat Dat DuongandHunter Lovell 50a9cc5fbc Another pass at four pages of get started guide
- Make sure that we're calling `getType` instead of deprecated `_getType()`
- Make sure that we're referencing the proper camelCase symbols instead of snake_case symbols
- Make sure that we're using proper links to the API reference
- Make sure to chain calls of StateGraph methods
- Make sure to use Zod state in all snippets
2025-07-28 14:51:04 -07:00
Tat Dat DuongandHunter Lovell b3ec283b1e Update core snippet 2025-07-28 14:47:34 -07:00
Tat Dat DuongandHunter Lovell 17f2f4df5d Update tools 2025-07-28 14:47:34 -07:00
Tat Dat DuongandHunter Lovell 9378ad38dc Update first page 2025-07-28 14:47:13 -07:00
Eugene YurtsevandHunter Lovell e3d36cd59b Add part 6 raw translation 2025-07-28 14:44:50 -07:00
Eugene YurtsevandHunter Lovell 9f12c142e5 Set build target to js add translations for 4th and 5th parts of the tutorial 2025-07-28 14:44:50 -07:00
Eugene YurtsevandHunter Lovell 54e64640de Raw output unreviewed 2025-07-28 14:43:27 -07:00
Eugene YurtsevandHunter Lovell d1d4abf70e x 2025-07-28 14:21:55 -07:00
17 changed files with 2244 additions and 828 deletions
+2 -2
View File
@@ -15,8 +15,8 @@ build-prebuilt:
fi
uv run python -m _scripts.third_party_page.create_third_party_page stats.yml docs/agents/prebuilt.md --language python
build-docs: build-prebuilt
uv run python -m mkdocs build --clean -f mkdocs.yml --strict
build-docs: build-typedoc build-prebuilt
TARGET_LANGUAGE=python uv run python -m mkdocs build --clean -f mkdocs.yml --strict
llms-text:
uv run python -m _scripts.generate_llms_text docs/llms-full.txt
-181
View File
@@ -1,181 +0,0 @@
"""Logic to identify and transform cross-reference links in markdown files.
This module allows supporting custom markdown syntax for "autolinks". These are links
that will be transformed based on the current scope context, such as "global", "python",
or "js" into an appropriate markdown link format.
For example,
```markdown
@[StateGraph]
```
May be transformed into:
```markdown
[StateGraph](some_path/api-reference/state-graph.md)
```
The transformation value depends on the scope in which the link is used.
"""
import logging
import re
from typing import Optional
from _scripts.link_map import SCOPE_LINK_MAPS
logger = logging.getLogger(__name__)
def _transform_link(
link_name: str, scope: str, file_path: str, line_number: int, custom_title: Optional[str] = None
) -> Optional[str]:
"""Transform a cross-reference link based on the current scope.
Args:
link_name: The name of the link to transform (e.g., "StateGraph").
scope: The current scope context ("global", "python", "js", etc.).
file_path: The file path for error reporting.
line_number: The line number for error reporting.
custom_title: Optional custom title for the link. If None, uses link_name.
Returns:
A formatted markdown link if the link is found in the scope mapping,
None otherwise.
Example:
>>> _transform_link("StateGraph", "python", "file.md", 5)
"[StateGraph](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.StateGraph)"
>>> _transform_link("StateGraph", "python", "file.md", 5, "Custom Title")
"[Custom Title](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.StateGraph)"
>>> _transform_link("unknown-link", "python", "file.md", 5)
None
"""
if scope == "global":
# Special scope that is composed of both Python and JS links
# For now, we will substitute in the python scope!
# But we need to add support for handling both scopes.
scope = "python"
logger.error(
"Encountered unhandled 'global' scope. Defaulting to 'python'."
"In file: %s, line %d, link_name: %s",
file_path,
line_number,
link_name,
)
link_map = SCOPE_LINK_MAPS.get(scope, {})
url = link_map.get(link_name)
if url:
title = custom_title if custom_title is not None else link_name
return f"[{title}]({url})"
else:
# Log error with file location information
logger.info(
# Using %s
"Link '%s' not found in scope '%s'. "
"In file: %s, line %d. Available links in scope: %s",
link_name,
scope,
file_path,
line_number,
list(link_map.keys() if link_map else []),
)
return None
CONDITIONAL_FENCE_PATTERN = re.compile(
r"""
^ # Start of line
(?P<indent>[ \t]*) # Optional indentation (spaces or tabs)
::: # Literal fence marker
(?P<language>\w+)? # Optional language identifier (named group: language)
\s* # Optional trailing whitespace
$ # End of line
""",
re.VERBOSE,
)
CROSS_REFERENCE_PATTERN = re.compile(
r"""
@ # Literal @ symbol
(?: # Non-capturing group for two possible formats:
\[ # Opening bracket for title
(?P<title>[^\]]+) # Custom title - one or more non-bracket characters
\] # Closing bracket for title
\[ # Opening bracket for link name
(?P<link_name_with_title>[^\]]+) # Link name - one or more non-bracket characters
\] # Closing bracket for link name
| # OR
\[ # Opening bracket
(?P<link_name>[^\]]+) # Link name - one or more non-bracket characters
\] # Closing bracket
)
""",
re.VERBOSE,
)
def _replace_autolinks(markdown: str, file_path: str) -> str:
"""Preprocess markdown lines to handle @[links] with conditional fence scopes.
This function processes markdown content to transform @[link_name] references
based on the current conditional fence scope. Conditional fences use the
syntax :::language to define scope boundaries.
Args:
markdown: The markdown content to process.
file_path: The file path for error reporting.
Returns:
Processed markdown content with @[references] transformed to proper
markdown links or left unchanged if not found.
Example:
Input:
"@[StateGraph]\\n:::python\\n@[Command]\\n:::\\n"
Output:
"[StateGraph](url)\\n:::python\\n[Command](url)\\n:::\\n"
"""
# Track the current scope context
current_scope = "global"
lines = markdown.splitlines(keepends=True)
processed_lines = []
for line_number, line in enumerate(lines, 1):
line_stripped = line.strip()
# Check if this line defines a new conditional fence scope
fence_match = CONDITIONAL_FENCE_PATTERN.match(line_stripped)
if fence_match:
language = fence_match.group("language")
# Set scope to the specified language, or reset to global if no language
current_scope = language.lower() if language else "global"
processed_lines.append(line)
continue
# Transform all @[link_name] references in this line based on current scope
def replace_cross_reference(match: re.Match[str]) -> str:
"""Replace a single @[link_name] with the scoped equivalent."""
# Check if this is the @[title][ref] format or @[ref] format
title = match.group("title")
if title is not None:
# This is @[title][ref] format
link_name = match.group("link_name_with_title")
custom_title = title
else:
# This is @[ref] format
link_name = match.group("link_name")
custom_title = None
transformed = _transform_link(
link_name, current_scope, file_path, line_number, custom_title
)
return transformed if transformed is not None else match.group(0)
transformed_line = CROSS_REFERENCE_PATTERN.sub(replace_cross_reference, line)
processed_lines.append(transformed_line)
return "".join(processed_lines)
+3 -140
View File
@@ -1,142 +1,5 @@
"""Link mapping for cross-reference resolution across different scopes.
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.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/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/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": "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/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/prebuilt/#langgraph.prebuilt.InjectedState",
"InMemorySaver": "reference/checkpoints/#langgraph.checkpoint.memory.InMemorySaver",
"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": "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/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",
"SerializerProtocol": "reference/checkpoints/#langgraph.checkpoint.serde.base.SerializerProtocol",
"SqliteSaver": "reference/checkpoints/#langgraph.checkpoint.sqlite.SqliteSaver",
"START": "reference/constants/#langgraph.constants.START",
"CompiledStateGraph.stream": "reference/graphs/#langgraph.graph.state.CompiledStateGraph.stream",
"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 = {
"StateGraph": "reference/classes/langgraph.StateGraph.html",
"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",
"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",
"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": "<insert-ref>",
"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/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": "<insert-ref>",
# "messagesStateReducer": "<insert-ref>",
"PostgresSaver": "reference/classes/langgraph_checkpoint_postgres.PostgresSaver.html",
"Pregel": "reference/classes/langgraph.Pregel.html",
"Pregel.stream": "reference/functions/langgraph_Pregel.stream.html",
"pre_model_hook": "reference/functions/langgraph_prebuilt.createReactAgent.html",
"protocol": "reference/classes/langgraph_checkpoint_serde_base.SerializerProtocol.html",
"Send": "reference/classes/langgraph.Send.html",
"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
PY_REFERENCE_HOST = "https://langchain-ai.github.io/langgraph/"
JS_REFERENCE_HOST = "https://langchain-ai.github.io/langgraphjs/"
for key, value in PYTHON_LINK_MAP.items():
# Ensure the link is absolute
if not value.startswith("http"):
PYTHON_LINK_MAP[key] = f"{PY_REFERENCE_HOST}{value}"
for key, value in JS_LINK_MAP.items():
# Ensure the link is absolute
if not value.startswith("http"):
JS_LINK_MAP[key] = f"{JS_REFERENCE_HOST}{value}"
# Global scope is assembled from the Python and JS mappings
# Combined mapping by scope
SCOPE_LINK_MAPS = {
"python": PYTHON_LINK_MAP,
"js": JS_LINK_MAP,
"langgraph.types.interrupt": "https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph.interrupt-2.html",
"create_react_agent": "https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph_prebuilt.createReactAgent.html",
"langgraph.types.Command": "https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph.Command.html",
}
+51 -16
View File
@@ -16,7 +16,7 @@ from mkdocs.structure.files import Files, File
from mkdocs.structure.pages import Page
from _scripts.generate_api_reference_links import update_markdown_with_imports
from _scripts.handle_auto_links import _replace_autolinks
from _scripts.link_map import JS_LINK_MAP
from _scripts.notebook_convert import convert_notebook
logger = logging.getLogger(__name__)
@@ -176,7 +176,31 @@ def _add_path_to_code_blocks(markdown: str, page: Page) -> str:
return code_block_pattern.sub(replace_code_block_header, markdown)
# Compiled regex patterns for better performance and readability
def _resolve_cross_references(md_text: str, link_map: dict[str, str]) -> str:
"""Replace [title][identifier] with [title](url) using language-specific link_map.
Args:
md_text: The markdown text to process.
link_map: mapping of identifier to URL.
Returns:
The processed markdown text with cross-references resolved.
"""
# Pattern to match [title][identifier]
pattern = re.compile(r"\[([^\]]+)\]\[([^\]]+)\]")
def replace_reference(match: re.Match) -> str:
"""Replace the matched reference with the corresponding URL."""
title, identifier = match.group(1), match.group(2)
url = link_map.get(identifier)
if url:
return f"[{title}]({url})"
else:
# Leave it unchanged if not found
return match.group(0)
return pattern.sub(replace_reference, md_text)
def _apply_conditional_rendering(md_text: str, target_language: str) -> str:
@@ -271,7 +295,7 @@ def _highlight_code_blocks(markdown: str) -> str:
opening_fence += f" {attributes}"
if highlighted_lines:
opening_fence += f' hl_lines="{" ".join(highlighted_lines)}"'
opening_fence += f" hl_lines=\"{' '.join(highlighted_lines)}\""
return (
# The indent and opening fence
@@ -286,6 +310,12 @@ def _highlight_code_blocks(markdown: str) -> str:
return markdown
TARGET_LANGUAGE = os.environ.get("TARGET_LANGUAGE", "python")
if TARGET_LANGUAGE not in {"python", "js"}:
raise ValueError(f"TARGET_LANGUAGE must be 'python' or 'js', got {TARGET_LANGUAGE}")
def _on_page_markdown_with_config(
markdown: str,
page: Page,
@@ -301,9 +331,6 @@ def _on_page_markdown_with_config(
# logger.info("Processing Jupyter notebook: %s", page.file.src_path)
markdown = convert_notebook(page.file.abs_src_path)
# Apply cross-reference preprocessing to all markdown content
markdown = _replace_autolinks(markdown, page.file.src_path)
# Append API reference links to code blocks
if add_api_references:
markdown = update_markdown_with_imports(markdown, page.file.abs_src_path)
@@ -311,8 +338,17 @@ def _on_page_markdown_with_config(
markdown = _highlight_code_blocks(markdown)
# Apply conditional rendering for code blocks
target_language = kwargs.get("target_language", "python")
markdown = _apply_conditional_rendering(markdown, target_language)
markdown = _apply_conditional_rendering(markdown, TARGET_LANGUAGE)
if TARGET_LANGUAGE == "js":
markdown = _resolve_cross_references(markdown, JS_LINK_MAP)
elif TARGET_LANGUAGE == "python":
# Via a dedicated plugin
pass
else:
raise ValueError(
f"Unsupported target language: {TARGET_LANGUAGE}. "
"Supported languages are 'python' and 'js'."
)
# Add file path as an attribute to code blocks that are executable.
# This file path is used to associate fixtures with the executable code
@@ -327,11 +363,13 @@ def _on_page_markdown_with_config(
def on_page_markdown(markdown: str, page: Page, **kwargs: Dict[str, Any]):
finalized_markdown = _on_page_markdown_with_config(
markdown,
page,
add_api_references=True,
**kwargs,
finalized_markdown = (
_on_page_markdown_with_config(
markdown,
page,
add_api_references=True,
**kwargs,
)
)
page.meta["original_markdown"] = finalized_markdown
return finalized_markdown
@@ -404,7 +442,6 @@ height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
else:
return html # fallback if no <body> found
def _inject_markdown_into_html(html: str, page: Page) -> str:
"""Inject the original markdown content into the HTML page as JSON."""
original_markdown = page.meta.get("original_markdown", "")
@@ -437,7 +474,6 @@ def _inject_markdown_into_html(html: str, page: Page) -> str:
)
return html.replace("</head>", f"{script_content}</head>")
def on_post_page(html: str, page: Page, config: MkDocsConfig) -> str:
"""Inject Google Tag Manager noscript tag immediately after <body>.
@@ -452,7 +488,6 @@ def on_post_page(html: str, page: Page, config: MkDocsConfig) -> str:
html = _inject_markdown_into_html(html, page)
return _inject_gtm(html)
# Create HTML files for redirects after site dir has been built
def on_post_build(config):
use_directory_urls = config.get("use_directory_urls")
+28 -36
View File
@@ -1,43 +1,40 @@
# Context
**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:
**Context engineering** is the practice of building dynamic systems that provide the right information and tools, in the right format, so that a language model can plausibly accomplish a task.
1. By **mutability**:
Context includes *any* data outside the message list that can shape behavior. This can be:
- **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)
- Information passed at runtime, like a `user_id` or API credentials.
- Internal state updated during a multi-step reasoning process.
- Persistent memory or facts from previous interactions.
2. By **lifetime**:
LangGraph provides **three** primary ways to manage context:
- **Runtime context**: Data scoped to a single run or invocation
- **Cross-conversation context**: Data that persists across multiple conversations or sessions
| Type | Description | Mutable? | Lifetime |
|------------------------------------------------------------------------------|-----------------------------------------------|----------|-------------------------|
| [**Runtime Context**](#runtime-context) | data passed at the start of a run | ❌ | per run |
| [**Short-term memory (State)**](#short-term-memory-mutable-context) | dynamic data that can change during execution | ✅ | per run or conversation |
| [**Long-term memory (Store)**](#long-term-memory-cross-conversation-context) | data that can be shared between conversations | ✅ | across conversations |
!!! tip "Runtime context vs LLM context"
### Runtime Context
Runtime context refers to local context: data and dependencies your code needs to run. It does **not** refer to:
Runtime context is for immutable data like user metadata, tools, db connections, etc. Use this when you have values that don't change mid-run.
!!! version-added "New in LangGraph v0.6: `Runtime.context` replaces `config['configurable']`"
The `Runtime` object is recommended to access static context and runtime-specific information like the store and stream writer.
!!! note
Runtime context refers to local context: data and dependencies your code needs to run. It does not refer to:
* The LLM context, which is the data passed into the LLM's prompt.
* The "context window", which is the maximum number of tokens that can be passed to the LLM.
Runtime context can be used to optimize the LLM context. For example, you can use user metadata
in the runtime context to fetch user preferences and feed them into the context window.
You likely want to use the local context to optimize the LLM's context window. For example, you
could use a user id to fetch a user's name and information from a database to populate the context window with relevant memories.
LangGraph provides three ways to manage context, which combines the mutability and lifetime dimensions:
| Context type | Description | Mutability | Lifetime | Access method |
|------------------------------------------------------------------------------|--------------------------------------------------------|------------|-------------------------|-----------------------------------|
| [**Static runtime context**](#static-runtime-context) | User metadata, tools, db connections passed at startup | Static | Single run | `context` argument to `invoke`/`stream` |
| [**Dynamic runtime context (state)**](#dynamic-runtime-context-state) | Mutable data that evolves during a single run | Dynamic | Single run | LangGraph state object |
| [**Dynamic cross-conversation context (store)**](#dynamic-cross-conversation-context-store) | Persistent data shared across conversations | Dynamic | Cross-conversation | LangGraph store |
## Static runtime context
**Static runtime context** represents immutable data like user metadata, tools, and database connections that are passed to an application at the start of a run via the `context` argument to `invoke`/`stream`. This data does not change during execution.
!!! version-added "New in LangGraph v0.6: `context` replaces `config['configurable']`"
Runtime context is now passed to the `context` argument of `invoke`/`stream`,
which replaces the previous pattern of passing application configuration to `config['configurable']`.
Specify static context via the `context` argument to `invoke` / `stream`, which is reserved for this purpose:
```python
@dataclass
@@ -115,14 +112,9 @@ graph.invoke( # (1)!
See the [tool calling guide](../how-tos/tool-calling.md#configuration) for details.
!!! tip
### Short-term memory (mutable context)
The `Runtime` object can be used to access static context and other utilities like the active store and stream writer.
See the [Runtime][langgraph.runtime.Runtime] documentation for details.
## Dynamic runtime context (state)
**Dynamic runtime context** represents mutable data that can evolve during a single run and is managed through the LangGraph state object. This includes conversation history, intermediate results, and values derived from tools or LLM outputs. In LangGraph, the state object acts as [short-term memory](../concepts/memory.md) during a run.
State acts as [short-term memory](../concepts/memory.md) during a run. It holds dynamic data that can evolve during execution, such as values derived from tools or LLM outputs.
=== "In an agent"
@@ -202,8 +194,8 @@ graph.invoke( # (1)!
Please see the [memory guide](../how-tos/memory/add-memory.md) for more details on how to enable memory. This is a powerful feature that allows you to persist the agent's state across multiple invocations. Otherwise, the state is scoped only to a single run.
## Dynamic cross-conversation context (store)
### Long-term memory (cross-conversation context)
**Dynamic cross-conversation context** represents persistent, mutable data that spans across multiple conversations or sessions and is managed through the LangGraph store. This includes user profiles, preferences, and historical interactions. The LangGraph store acts as [long-term memory](../concepts/memory.md#long-term-memory) across multiple runs. This can be used to read or update persistent facts (e.g., user profiles, preferences, prior interactions).
For context that spans *across* conversations or sessions, LangGraph allows access to **long-term memory** via a `store`. This can be used to read or update persistent facts (e.g., user profiles, preferences, prior interactions).
For more information, see the [Memory guide](../how-tos/memory/add-memory.md).
@@ -1,6 +1,6 @@
# Build a basic chatbot
In this tutorial, you will build a basic chatbot. This chatbot is the basis for the following series of tutorials where you will progressively add more sophisticated capabilities, and be introduced to key LangGraph concepts along the way. Let’s dive in! 🌟
In this tutorial, you will build a basic chatbot. This chatbot is the basis for the following series of tutorials where you will progressively add more sophisticated capabilities, and be introduced to key LangGraph concepts along the way. Let's dive in! 🌟
## Prerequisites
@@ -13,13 +13,45 @@ tool-calling features, such as [OpenAI](https://platform.openai.com/api-keys),
Install the required packages:
:::python
```bash
pip install -U langgraph langsmith
```
:::
:::js
=== "npm"
```bash
npm install @langchain/langgraph @langchain/core zod
```
=== "yarn"
```bash
yarn add @langchain/langgraph @langchain/core zod
```
=== "pnpm"
```bash
pnpm add @langchain/langgraph @langchain/core zod
```
=== "bun"
```bash
bun add @langchain/langgraph @langchain/core zod
```
:::
!!! tip
Sign up for LangSmith 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. For more information on how to get started, see [LangSmith docs](https://docs.smith.langchain.com).
Sign up for LangSmith 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. For more information on how to get started, see [LangSmith docs](https://docs.smith.langchain.com).
Sign up for LangSmith 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. For more information on how to get started, see [LangSmith docs](https://docs.smith.langchain.com).
## 2. Create a `StateGraph`
@@ -27,6 +59,8 @@ Now you can create a basic chatbot using LangGraph. This chatbot will respond di
Start by creating a `StateGraph`. A `StateGraph` object defines the structure of our chatbot as a "state machine". We'll add `nodes` to represent the llm and functions our chatbot can call and `edges` to specify how the bot should transition between these functions.
:::python
```python
from typing import Annotated
@@ -46,24 +80,43 @@ class State(TypedDict):
graph_builder = StateGraph(State)
```
:::
:::js
```typescript
import { StateGraph, MessagesZodState, START } from "@langchain/langgraph";
import { z } from "zod";
const State = z.object({ messages: MessagesZodState.shape.messages });
const graph = new StateGraph(State).compile();
```
:::
Our graph can now handle two key tasks:
1. Each `node` can receive the current `State` as input and output an update to the state.
2. Updates to `messages` will be appended to the existing list rather than overwriting it, thanks to the prebuilt [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/?h=add+messages#add_messages) function used with the `Annotated` syntax.
2. Updates to `messages` will be appended to the existing list rather than overwriting it, thanks to the prebuilt reducer function.
------
---
---
!!! tip "Concept"
When defining a graph, the first step is to define its `State`. The `State` includes the graph's schema and [reducer functions](https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers) that handle state updates. In our example, `State` is a `TypedDict` with one key: `messages`. The [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages) reducer function is used to append new messages to the list instead of overwriting it. Keys without a reducer annotation will overwrite previous values. To learn more about state, reducers, and related concepts, see [LangGraph reference docs](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages).
When defining a graph, the first step is to define its `State`. The `State` includes the graph's schema and [reducer functions](https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers) that handle state updates. In our example, `State` is a schema with one key: `messages`. The reducer function is used to append new messages to the list instead of overwriting it. Keys without a reducer annotation will overwrite previous values.
To learn more about state, reducers, and related concepts, see [LangGraph reference docs](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages).
## 3. Add a node
Next, add a "`chatbot`" node. **Nodes** represent units of work and are typically regular Python functions.
Next, add a "`chatbot`" node. **Nodes** represent units of work and are typically regular functions.
Let's first select a chat model:
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
{!snippets/chat_model_tabs.md!}
<!---
```python
@@ -73,9 +126,26 @@ llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
```
-->
:::
:::js
```typescript
import { ChatOpenAI } from "@langchain/openai";
// or import { ChatAnthropic } from "@langchain/anthropic";
const llm = new ChatOpenAI({
model: "gpt-4o",
temperature: 0,
});
```
:::
We can now incorporate the chat model into a simple node:
:::python
```python
def chatbot(state: State):
@@ -88,38 +158,133 @@ def chatbot(state: State):
graph_builder.add_node("chatbot", chatbot)
```
:::
:::js
```typescript hl_lines="7-9"
import { StateGraph, MessagesZodState, START } from "@langchain/langgraph";
import { z } from "zod";
const State = z.object({ messages: MessagesZodState.shape.messages });
const graph = new StateGraph(State)
.addNode("chatbot", async (state: z.infer<typeof State>) => {
return { messages: [await llm.invoke(state.messages)] };
})
.compile();
```
:::
**Notice** how the `chatbot` node function takes the current `State` as input and returns a dictionary containing an updated `messages` list under the key "messages". This is the basic pattern for all LangGraph node functions.
:::python
The `add_messages` function in our `State` will append the LLM's response messages to whatever messages are already in the state.
:::
:::js
The `addMessages` function used within `MessagesZodState` will append the LLM's response messages to whatever messages are already in the state.
:::
## 4. Add an `entry` point
Add an `entry` point to tell the graph **where to start its work** each time it is run:
:::python
```python
graph_builder.add_edge(START, "chatbot")
```
:::
:::js
```typescript hl_lines="10"
import { StateGraph, MessagesZodState, START } from "@langchain/langgraph";
import { z } from "zod";
const State = z.object({ messages: MessagesZodState.shape.messages });
const graph = new StateGraph(State)
.addNode("chatbot", async (state: z.infer<typeof State>) => {
return { messages: [await llm.invoke(state.messages)] };
})
.addEdge(START, "chatbot")
.compile();
```
:::
## 5. Add an `exit` point
Add an `exit` point to indicate **where the graph should finish execution**. This is helpful for more complex flows, but even in a simple graph like this, adding an end node improves clarity.
:::python
```python
graph_builder.add_edge("chatbot", END)
```
:::
:::js
```typescript hl_lines="11"
import { StateGraph, MessagesZodState, START, END } from "@langchain/langgraph";
import { z } from "zod";
const State = z.object({ messages: MessagesZodState.shape.messages });
const graph = new StateGraph(State)
.addNode("chatbot", async (state: z.infer<typeof State>) => {
return { messages: [await llm.invoke(state.messages)] };
})
.addEdge(START, "chatbot")
.addEdge("chatbot", END)
.compile();
```
:::
This tells the graph to terminate after running the chatbot node.
## 6. Compile the graph
Before running the graph, we'll need to compile it. We can do so by calling `compile()`
on the graph builder. This creates a `CompiledStateGraph` we can invoke on our state.
on the graph builder. This creates a `CompiledGraph` we can invoke on our state.
:::python
```python
graph = graph_builder.compile()
```
:::
:::js
```typescript hl_lines="12"
import { StateGraph, MessagesZodState, START, END } from "@langchain/langgraph";
import { z } from "zod";
const State = z.object({ messages: MessagesZodState.shape.messages });
const graph = new StateGraph(State)
.addNode("chatbot", async (state: z.infer<typeof State>) => {
return { messages: [await llm.invoke(state.messages)] };
})
.addEdge(START, "chatbot")
.addEdge("chatbot", END)
.compile();
```
:::
## 7. Visualize the graph (optional)
:::python
You can visualize the graph using the `get_graph` method and one of the "draw" methods, like `draw_ascii` or `draw_png`. The `draw` methods each require additional dependencies.
```python
@@ -132,17 +297,35 @@ except Exception:
pass
```
![basic chatbot diagram](basic-chatbot.png)
:::
:::js
You can visualize the graph using the `getGraph` method and render the graph with the `drawMermaidPng` method.
```typescript
import * as fs from "node:fs/promises";
const drawableGraph = await graph.getGraphAsync();
const image = await drawableGraph.drawMermaidPng();
const imageBuffer = new Uint8Array(await image.arrayBuffer());
await fs.writeFile("basic-chatbot.png", imageBuffer);
```
:::
![basic chatbot diagram](basic-chatbot.png)
## 8. Run the chatbot
Now run the chatbot!
Now run the chatbot!
!!! tip
You can exit the chat loop at any time by typing `quit`, `exit`, or `q`.
:::python
```python
def stream_graph_updates(user_input: str):
for event in graph.stream({"messages": [{"role": "user", "content": user_input}]}):
@@ -165,15 +348,90 @@ while True:
break
```
:::
:::js
```typescript
import { HumanMessage } from "@langchain/core/messages";
async function streamGraphUpdates(userInput: string) {
const stream = await graph.stream({
messages: [new HumanMessage(userInput)],
});
import * as readline from "node:readline/promises";
import { StateGraph, MessagesZodState, START, END } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
const llm = new ChatOpenAI({ model: "gpt-4o-mini" });
const State = z.object({ messages: MessagesZodState.shape.messages });
const graph = new StateGraph(State)
.addNode("chatbot", async (state: z.infer<typeof State>) => {
return { messages: [await llm.invoke(state.messages)] };
})
.addEdge(START, "chatbot")
.addEdge("chatbot", END)
.compile();
async function generateText(content: string) {
const stream = await graph.stream(
{ messages: [{ type: "human", content }] },
{ streamMode: "values" }
);
for await (const event of stream) {
for (const value of Object.values(event)) {
console.log(
"Assistant:",
value.messages[value.messages.length - 1].content
);
const lastMessage = event.messages.at(-1);
if (lastMessage?.getType() === "ai") {
console.log(`Assistant: ${lastMessage.text}`);
}
}
}
const prompt = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
while (true) {
const human = await prompt.question("User: ");
if (["quit", "exit", "q"].includes(human.trim())) break;
await generateText(human || "What do you know about LangGraph?");
}
prompt.close();
```
:::
```
Assistant: LangGraph is a library designed to help build stateful multi-agent applications using language models. It provides tools for creating workflows and state machines to coordinate multiple AI agents or language model interactions. LangGraph is built on top of LangChain, leveraging its components while adding graph-based coordination capabilities. It's particularly useful for developing more complex, stateful AI applications that go beyond simple query-response interactions.
```
:::python
```
Goodbye!
```
:::
**Congratulations!** You've built your first chatbot using LangGraph. This bot can engage in basic conversation by taking user input and generating responses using an LLM. You can inspect a [LangSmith Trace](https://smith.langchain.com/public/7527e308-9502-4894-b347-f34385740d5a/r) for the call above.
:::python
Below is the full code for this tutorial:
:::python
```python
from typing import Annotated
@@ -207,8 +465,44 @@ graph_builder.add_edge("chatbot", END)
graph = graph_builder.compile()
```
:::
:::js
```typescript
import { Annotation } from "@langchain/langgraph";
import { StateGraph, START, END } from "@langchain/langgraph";
import { BaseMessage, HumanMessage } from "@langchain/core/messages";
import { ChatOpenAI } from "@langchain/openai";
const State = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (x, y) => x.concat(y),
}),
});
const graphBuilder = new StateGraph(State);
const llm = new ChatOpenAI({
model: "gpt-4o",
temperature: 0,
});
const chatbot = async (state: typeof State.State) => {
return { messages: [await llm.invoke(state.messages)] };
};
// The first argument is the unique node name
// The second argument is the function or object that will be called whenever
// the node is used.
graphBuilder.addNode("chatbot", chatbot);
graphBuilder.addEdge(START, "chatbot");
graphBuilder.addEdge("chatbot", END);
const graph = graphBuilder.compile();
```
:::
## Next steps
You may have noticed that the bot's knowledge is limited to what's in its training data. In the next part, we'll [add a web search tool](./2-add-tools.md) to expand the bot's knowledge and make it more capable.
+410 -12
View File
@@ -10,24 +10,65 @@ To handle queries that your chatbot can't answer "from memory", integrate a web
Before you start this tutorial, ensure you have the following:
:::python
- An API key for the [Tavily Search Engine](https://python.langchain.com/docs/integrations/tools/tavily_search/).
:::
:::js
- An API key for the [Tavily Search Engine](https://js.langchain.com/docs/integrations/tools/tavily_search/).
:::
## 1. Install the search engine
:::python
Install the requirements to use the [Tavily Search Engine](https://python.langchain.com/docs/integrations/tools/tavily_search/):
```bash
pip install -U langchain-tavily
```
:::
:::js
Install the requirements to use the [Tavily Search Engine](https://docs.tavily.com/):
=== "npm"
```bash
npm install @langchain/tavily
```
=== "yarn"
```bash
yarn add @langchain/tavily
```
=== "pnpm"
```bash
pnpm add @langchain/tavily
```
=== "bun"
```bash
bun add @langchain/tavily
```
:::
## 2. Configure your environment
Configure your environment with your search engine API key:
```python
def _set_env(var: str):
if not os.environ.get(var):
os.environ[var] = getpass.getpass(f"{var}: ")
:::python
```bash
_set_env("TAVILY_API_KEY")
```
@@ -35,10 +76,22 @@ _set_env("TAVILY_API_KEY")
os.environ["TAVILY_API_KEY"]: "········"
```
:::
:::js
```typescript
process.env.TAVILY_API_KEY = "tvly-...";
```
:::
## 3. Define the tool
Define the web search tool:
:::python
```python
from langchain_tavily import TavilySearch
@@ -47,8 +100,25 @@ tools = [tool]
tool.invoke("What's a 'node' in LangGraph?")
```
:::
:::js
```typescript
import { TavilySearch } from "@langchain/tavily";
const tool = new TavilySearch({ maxResults: 2 });
const tools = [tool];
await tool.invoke({ query: "What's a 'node' in LangGraph?" });
```
:::
The results are page summaries our chat bot can use to answer questions:
:::python
```
{'query': "What's a 'node' in LangGraph?",
'follow_up_questions': None,
@@ -67,13 +137,51 @@ The results are page summaries our chat bot can use to answer questions:
'response_time': 1.38}
```
:::
:::js
```json
{
"query": "What's a 'node' in LangGraph?",
"follow_up_questions": null,
"answer": null,
"images": [],
"results": [
{
"url": "https://blog.langchain.dev/langgraph/",
"title": "LangGraph - LangChain Blog",
"content": "TL;DR: LangGraph is module built on top of LangChain to better enable creation of cyclical graphs, often needed for agent runtimes. This state is updated by nodes in the graph, which return operations to attributes of this state (in the form of a key-value store). After adding nodes, you can then add edges to create the graph. An example of this may be in the basic agent runtime, where we always want the model to be called after we call a tool. The state of this graph by default contains concepts that should be familiar to you if you've used LangChain agents: `input`, `chat_history`, `intermediate_steps` (and `agent_outcome` to represent the most recent agent outcome)",
"score": 0.7407191,
"raw_content": null
},
{
"url": "https://medium.com/@cplog/introduction-to-langgraph-a-beginners-guide-14f9be027141",
"title": "Introduction to LangGraph: A Beginner's Guide - Medium",
"content": "* **Stateful Graph:** LangGraph revolves around the concept of a stateful graph, where each node in the graph represents a step in your computation, and the graph maintains a state that is passed around and updated as the computation progresses. LangGraph supports conditional edges, allowing you to dynamically determine the next node to execute based on the current state of the graph. Image 10: Introduction to AI Agent with LangChain and LangGraph: A Beginner’s Guide Image 18: How to build LLM Agent with LangGraph — StateGraph and Reducer Image 20: Simplest Graphs using LangGraph Framework Image 24: Building a ReAct Agent with Langgraph: A Step-by-Step Guide Image 28: Building an Agentic RAG with LangGraph: A Step-by-Step Guide",
"score": 0.65279555,
"raw_content": null
}
],
"response_time": 1.34
}
```
:::
## 4. Define the graph
:::python
For the `StateGraph` you created in the [first tutorial](./1-build-basic-chatbot.md), add `bind_tools` on the LLM. This lets the LLM know the correct JSON format to use if it wants to use the search engine.
:::
:::js
For the `StateGraph` you created in the [first tutorial](./1-build-basic-chatbot.md), add `bindTools` on the LLM. This lets the LLM know the correct JSON format to use if it wants to use the search engine.
:::
Let's first select our LLM:
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
{!snippets/chat_model_tabs.md!}
<!---
```python
@@ -83,8 +191,22 @@ llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
```
-->
:::
:::js
```typescript
import { ChatAnthropic } from "@langchain/anthropic";
const llm = new ChatAnthropic({ model: "claude-3-5-sonnet-latest" });
```
:::
We can now incorporate it into a `StateGraph`:
:::python
```python hl_lines="15"
from typing import Annotated
@@ -108,9 +230,31 @@ def chatbot(state: State):
graph_builder.add_node("chatbot", chatbot)
```
:::
:::js
```typescript hl_lines="7-8"
import { StateGraph, MessagesZodState } from "@langchain/langgraph";
import { z } from "zod";
const State = z.object({ messages: MessagesZodState.shape.messages });
const chatbot = async (state: z.infer<typeof State>) => {
// Modification: tell the LLM which tools it can call
const llmWithTools = llm.bindTools(tools);
return { messages: [await llmWithTools.invoke(state.messages)] };
};
```
:::
## 5. Create a function to run the tools
Now, create a function to run the tools if they are called. Do this by adding the tools to a new node called`BasicToolNode` that checks the most recent message in the state and calls tools if the message contains `tool_calls`. It relies on the LLM's `tool_calling` support, which is available in Anthropic, OpenAI, Google Gemini, and a number of other LLM providers.
:::python
Now, create a function to run the tools if they are called. Do this by adding the tools to a new node called `BasicToolNode` that checks the most recent message in the state and calls tools if the message contains `tool_calls`. It relies on the LLM's `tool_calling` support, which is available in Anthropic, OpenAI, Google Gemini, and a number of other LLM providers.
```python
import json
@@ -152,16 +296,80 @@ graph_builder.add_node("tools", tool_node)
If you do not want to build this yourself in the future, you can use LangGraph's prebuilt [ToolNode](https://langchain-ai.github.io/langgraph/reference/agents/#langgraph.prebuilt.tool_node.ToolNode).
:::
:::js
Now, create a function to run the tools if they are called. Do this by adding the tools to a new node called `"tools"` that checks the most recent message in the state and calls tools if the message contains `tool_calls`. It relies on the LLM's tool calling support, which is available in Anthropic, OpenAI, Google Gemini, and a number of other LLM providers.
```typescript
import type { StructuredToolInterface } from "@langchain/core/tools";
import { isAIMessage, ToolMessage } from "@langchain/core/messages";
function createToolNode(tools: StructuredToolInterface[]) {
const toolByName: Record<string, StructuredToolInterface> = {};
for (const tool of tools) {
toolByName[tool.name] = tool;
}
return async (inputs: z.infer<typeof State>) => {
const { messages } = inputs;
if (!messages || messages.length === 0) {
throw new Error("No message found in input");
}
const message = messages.at(-1);
if (!message || !isAIMessage(message) || !message.tool_calls) {
throw new Error("Last message is not an AI message with tool calls");
}
const outputs: ToolMessage[] = [];
for (const toolCall of message.tool_calls) {
if (!toolCall.id) throw new Error("Tool call ID is required");
const tool = toolByName[toolCall.name];
if (!tool) throw new Error(`Tool ${toolCall.name} not found`);
const result = await tool.invoke(toolCall.args);
outputs.push(
new ToolMessage({
content: JSON.stringify(result),
name: toolCall.name,
tool_call_id: toolCall.id,
})
);
}
return { messages: outputs };
};
}
```
!!! note
If you do not want to build this yourself in the future, you can use LangGraph's prebuilt [ToolNode](https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph_prebuilt.ToolNode.html).
:::
## 6. Define the `conditional_edges`
With the tool node added, now you can define the `conditional_edges`.
With the tool node added, now you can define the `conditional_edges`.
**Edges** route the control flow from one node to the next. **Conditional edges** start from a single node and usually contain "if" statements to route to different nodes depending on the current graph state. These functions receive the current graph `state` and return a string or list of strings indicating which node(s) to call next.
Next, define a router function called `route_tools` that checks for `tool_calls` in the chatbot's output. Provide this function to the graph by calling `add_conditional_edges`, which tells the graph that whenever the `chatbot` node completes to check this function to see where to go next.
:::python
Next, define a router function called `route_tools` that checks for `tool_calls` in the chatbot's output. Provide this function to the graph by calling `add_conditional_edges`, which tells the graph that whenever the `chatbot` node completes to check this function to see where to go next.
:::
:::js
Next, define a router function called `routeTools` that checks for `tool_calls` in the chatbot's output. Provide this function to the graph by calling `addConditionalEdges`, which tells the graph that whenever the `chatbot` node completes to check this function to see where to go next.
:::
The condition will route to `tools` if tool calls are present and `END` if not. Because the condition can return `END`, you do not need to explicitly set a `finish_point` this time.
:::python
```python
def route_tools(
state: State,
@@ -201,10 +409,61 @@ graph = graph_builder.compile()
!!! note
You can replace this with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition) to be more concise.
You can replace this with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition) to be more concise.
:::
:::js
```typescript
import { END, START } from "@langchain/langgraph";
const routeTools = (state: z.infer<typeof State>) => {
/**
* Use as conditional edge to route to the ToolNode if the last message
* has tool calls.
*/
const lastMessage = state.messages.at(-1);
if (
lastMessage &&
isAIMessage(lastMessage) &&
lastMessage.tool_calls?.length
) {
return "tools";
}
/** Otherwise, route to the end. */
return END;
};
const graph = new StateGraph(State)
.addNode("chatbot", chatbot)
// The `routeTools` function returns "tools" if the chatbot asks to use a tool, and "END" if
// it is fine directly responding. This conditional routing defines the main agent loop.
.addNode("tools", createToolNode(tools))
// Start the graph with the chatbot
.addEdge(START, "chatbot")
// The `routeTools` function returns "tools" if the chatbot asks to use a tool, and "END" if
// it is fine directly responding.
.addConditionalEdges("chatbot", routeTools, ["tools", END])
// Any time a tool is called, we need to return to the chatbot
.addEdge("tools", "chatbot")
.compile();
```
!!! note
You can replace this with the prebuilt [toolsCondition](https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph_prebuilt.toolsCondition.html) to be more concise.
:::
## 7. Visualize the graph (optional)
:::python
You can visualize the graph using the `get_graph` method and one of the "draw" methods, like `draw_ascii` or `draw_png`. The `draw` methods each require additional dependencies.
```python
@@ -217,12 +476,31 @@ except Exception:
pass
```
:::
:::js
You can visualize the graph using the `getGraph` method and render the graph with the `drawMermaidPng` method.
```typescript
import * as fs from "node:fs/promises";
const drawableGraph = await graph.getGraphAsync();
const image = await drawableGraph.drawMermaidPng();
const imageBuffer = new Uint8Array(await image.arrayBuffer());
await fs.writeFile("chatbot-with-tools.png", imageBuffer);
```
:::
![chatbot-with-tools-diagram](chatbot-with-tools.png)
## 8. Ask the bot questions
Now you can ask the chatbot questions outside its training data:
:::python
```python
def stream_graph_updates(user_input: str):
for event in graph.stream({"messages": [{"role": "user", "content": user_input}]}):
@@ -245,7 +523,7 @@ while True:
break
```
```
```
Assistant: [{'text': "To provide you with accurate and up-to-date information about LangGraph, I'll need to search for the latest details. Let me do that for you.", 'type': 'text'}, {'id': 'toolu_01Q588CszHaSvvP2MxRq9zRD', 'input': {'query': 'LangGraph AI tool information'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]
Assistant: [{"url": "https://www.langchain.com/langgraph", "content": "LangGraph sets the foundation for how we can build and scale AI workloads \u2014 from conversational agents, complex task automation, to custom LLM-backed experiences that 'just work'. The next chapter in building complex production-ready features with LLMs is agentic, and with LangGraph and LangSmith, LangChain delivers an out-of-the-box solution ..."}, {"url": "https://github.com/langchain-ai/langgraph", "content": "Overview. LangGraph is a library for building stateful, multi-actor applications with LLMs, used to create agent and multi-agent workflows. Compared to other LLM frameworks, it offers these core benefits: cycles, controllability, and persistence. LangGraph allows you to define flows that involve cycles, essential for most agentic architectures ..."}]
Assistant: Based on the search results, I can provide you with information about LangGraph:
@@ -276,18 +554,99 @@ Assistant: Based on the search results, I can provide you with information about
LangGraph appears to be a significant tool in the evolving landscape of LLM-based application development, offering developers new ways to create more complex, stateful, and interactive AI systems.
Goodbye!
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
```
:::
:::js
```typescript
import readline from "node:readline/promises";
const prompt = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
async function generateText(content: string) {
const stream = await graph.stream(
{ messages: [{ type: "human", content }] },
{ streamMode: "values" }
);
for await (const event of stream) {
const lastMessage = event.messages.at(-1);
if (lastMessage?.getType() === "ai" || lastMessage?.getType() === "tool") {
console.log(`Assistant: ${lastMessage?.text}`);
}
}
}
while (true) {
const human = await prompt.question("User: ");
if (["quit", "exit", "q"].includes(human.trim())) break;
await generateText(human || "What do you know about LangGraph?");
}
prompt.close();
```
```
User: What do you know about LangGraph?
Assistant: I'll search for the latest information about LangGraph for you.
Assistant: [{"title":"Introduction to LangGraph: A Beginner's Guide - Medium","url":"https://medium.com/@cplog/introduction-to-langgraph-a-beginners-guide-14f9be027141","content":"..."}]
Assistant: Based on the search results, I can provide you with information about LangGraph:
LangGraph is a library within the LangChain ecosystem designed for building stateful, multi-actor applications with Large Language Models (LLMs). Here are the key aspects:
**Core Purpose:**
- LangGraph is specifically designed for creating agent and multi-agent workflows
- It provides a framework for defining, coordinating, and executing multiple LLM agents in a structured manner
**Key Features:**
1. **Stateful Graph Architecture**: LangGraph revolves around a stateful graph where each node represents a step in computation, and the graph maintains state that is passed around and updated as the computation progresses
2. **Conditional Edges**: It supports conditional edges, allowing you to dynamically determine the next node to execute based on the current state of the graph
3. **Cycles**: Unlike other LLM frameworks, LangGraph allows you to define flows that involve cycles, which is essential for most agentic architectures
4. **Controllability**: It offers enhanced control over the application flow
5. **Persistence**: The library provides ways to maintain state and persistence in LLM-based applications
**Use Cases:**
- Conversational agents
- Complex task automation
- Custom LLM-backed experiences
- Multi-agent systems that perform complex tasks
**Benefits:**
LangGraph allows developers to focus on the high-level logic of their applications rather than the intricacies of agent coordination, making it easier to build complex, production-ready features with LLMs.
This makes LangGraph a significant tool in the evolving landscape of LLM-based application development.
```
:::
## 9. Use prebuilts
For ease of use, adjust your code to replace the following with LangGraph prebuilt components. These have built in functionality like parallel API execution.
:::python
- `BasicToolNode` is replaced with the prebuilt [ToolNode](https://langchain-ai.github.io/langgraph/reference/prebuilt/#toolnode)
- `route_tools` is replaced with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition)
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
<!---
```python
from langchain.chat_models import init_chat_model
llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
```
-->
```python hl_lines="25 30"
from typing import Annotated
@@ -327,7 +686,46 @@ graph_builder.add_edge(START, "chatbot")
graph = graph_builder.compile()
```
**Congratulations!** You've created a conversational agent in LangGraph that can use a search engine to retrieve updated information when needed. Now it can handle a wider range of user queries. To inspect all the steps your agent just took, check out this [LangSmith trace](https://smith.langchain.com/public/4fbd7636-25af-4638-9587-5a02fdbb0172/r).
:::
:::js
- `createToolNode` is replaced with the prebuilt [ToolNode](https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph_prebuilt.ToolNode.html)
- `routeTools` is replaced with the prebuilt [toolsCondition](https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph_prebuilt.toolsCondition.html)
```typescript
import { TavilySearch } from "@langchain/tavily";
import { ChatOpenAI } from "@langchain/openai";
import { StateGraph, START, MessagesZodState, END } from "@langchain/langgraph";
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
import { z } from "zod";
const State = z.object({ messages: MessagesZodState.shape.messages });
const tools = [new TavilySearch({ maxResults: 2 })];
const llm = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools(tools);
const graph = new StateGraph(State)
.addNode("chatbot", async (state) => ({
messages: [await llm.invoke(state.messages)],
}))
.addNode("tools", new ToolNode(tools))
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
.addEdge("tools", "chatbot")
.addEdge(START, "chatbot")
.compile();
```
:::
**Congratulations!** You've created a conversational agent in LangGraph that can use a search engine to retrieve updated information when needed. Now it can handle a wider range of user queries.
:::python
To inspect all the steps your agent just took, check out this [LangSmith trace](https://smith.langchain.com/public/4fbd7636-25af-4638-9587-5a02fdbb0172/r).
:::
## Next steps
+253 -14
View File
@@ -2,7 +2,7 @@
The chatbot can now [use tools](./2-add-tools.md) to answer user questions, but it does not remember the context of previous interactions. This limits its ability to have coherent, multi-turn conversations.
LangGraph solves this problem through **persistent checkpointing**. If you provide a `checkpointer` when compiling the graph and a `thread_id` when calling your graph, LangGraph automatically saves the state after each step. When you invoke the graph again using the same `thread_id`, the graph loads its saved state, allowing the chatbot to pick up where it left off.
LangGraph solves this problem through **persistent checkpointing**. If you provide a `checkpointer` when compiling the graph and a `thread_id` when calling your graph, LangGraph automatically saves the state after each step. When you invoke the graph again using the same `thread_id`, the graph loads its saved state, allowing the chatbot to pick up where it left off.
We will see later that **checkpointing** is _much_ more powerful than simple chat memory - it lets you save and resume complex state at any time for error recovery, human-in-the-loop workflows, time travel interactions, and more. But first, let's add checkpointing to enable multi-turn conversations.
@@ -14,43 +14,79 @@ We will see later that **checkpointing** is _much_ more powerful than simple cha
Create a `InMemorySaver` checkpointer:
``` python
:::python
```python
from langgraph.checkpoint.memory import InMemorySaver
memory = InMemorySaver()
```
:::
:::js
```typescript
import { MemorySaver } from "@langchain/langgraph";
const memory = new MemorySaver();
```
:::
This is in-memory checkpointer, which is convenient for the tutorial. However, in a production application, you would likely change this to use `SqliteSaver` or `PostgresSaver` and connect a database.
## 2. Compile the graph
Compile the graph with the provided checkpointer, which will checkpoint the `State` as the graph works through each node:
``` python
:::python
```python
graph = graph_builder.compile(checkpointer=memory)
```
``` python
from IPython.display import Image, display
:::
try:
display(Image(graph.get_graph().draw_mermaid_png()))
except Exception:
# This requires some extra dependencies and is optional
pass
:::js
```typescript hl_lines="7"
const graph = new StateGraph(State)
.addNode("chatbot", chatbot)
.addNode("tools", new ToolNode(tools))
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
.addEdge("tools", "chatbot")
.addEdge(START, "chatbot")
.compile({ checkpointer: memory });
```
:::
## 3. Interact with your chatbot
Now you can interact with your bot!
1. Pick a thread to use as the key for this conversation.
1. Pick a thread to use as the key for this conversation.
:::python
```python
config = {"configurable": {"thread_id": "1"}}
```
2. Call your chatbot:
:::
:::js
```typescript
const config = { configurable: { thread_id: "1" } };
```
:::
2. Call your chatbot:
:::python
```python
user_input = "Hi there! My name is Will."
@@ -74,14 +110,45 @@ Now you can interact with your bot!
Hello Will! It's nice to meet you. How can I assist you today? Is there anything specific you'd like to know or discuss?
```
!!! note
!!! note
The config was provided as the **second positional argument** when calling our graph. It importantly is _not_ nested within the graph inputs (`{'messages': []}`).
:::
:::js
```typescript
const userInput = "Hi there! My name is Will.";
const events = await graph.stream(
{ messages: [{ type: "human", content: userInput }] },
{ configurable: { thread_id: "1" }, streamMode: "values" }
);
for await (const event of events) {
const lastMessage = event.messages.at(-1);
console.log(`${lastMessage?.getType()}: ${lastMessage?.text}`);
}
```
```
human: Hi there! My name is Will.
ai: Hello Will! It's nice to meet you. How can I assist you today? Is there anything specific you'd like to know or discuss?
```
!!! note
The config was provided as the **second parameter** when calling our graph. It importantly is _not_ nested within the graph inputs (`{"messages": []}`).
:::
## 4. Ask a follow up question
Ask a follow up question:
:::python
```python
user_input = "Remember my name?"
@@ -104,10 +171,37 @@ Remember my name?
Of course, I remember your name, Will. I always try to pay attention to important details that users share with me. Is there anything else you'd like to talk about or any questions you have? I'm here to help with a wide range of topics or tasks.
```
:::
:::js
```typescript
const userInput2 = "Remember my name?";
const events2 = await graph.stream(
{ messages: [{ type: "human", content: userInput2 }] },
{ configurable: { thread_id: "1" }, streamMode: "values" }
);
for await (const event of events2) {
const lastMessage = event.messages.at(-1);
console.log(`${lastMessage?.getType()}: ${lastMessage?.text}`);
}
```
```
human: Remember my name?
ai: Yes, your name is Will. How can I help you today?
```
:::
**Notice** that we aren't using an external list for memory: it's all handled by the checkpointer! You can inspect the full execution in this [LangSmith trace](https://smith.langchain.com/public/29ba22b5-6d40-4fbe-8d27-b369e3329c84/r) to see what's going on.
Don't believe me? Try this using a different config.
:::python
```python
# The only difference is we change the `thread_id` here to "2" instead of "1"
events = graph.stream(
@@ -129,10 +223,36 @@ Remember my name?
I apologize, but I don't have any previous context or memory of your name. As an AI assistant, I don't retain information from past conversations. Each interaction starts fresh. Could you please tell me your name so I can address you properly in this conversation?
```
:::
:::js
```typescript hl_lines="3-4"
const events3 = await graph.stream(
{ messages: [{ type: "human", content: userInput2 }] },
// The only difference is we change the `thread_id` here to "2" instead of "1"
{ configurable: { thread_id: "2" }, streamMode: "values" }
);
for await (const event of events3) {
const lastMessage = event.messages.at(-1);
console.log(`${lastMessage?.getType()}: ${lastMessage?.text}`);
}
```
```
human: Remember my name?
ai: I don't have the ability to remember personal information about users between interactions. However, I'm here to help you with any questions or topics you want to discuss!
```
:::
**Notice** that the **only** change we've made is to modify the `thread_id` in the config. See this call's [LangSmith trace](https://smith.langchain.com/public/51a62351-2f0a-4058-91cc-9996c5561428/r) for comparison.
## 5. Inspect the state
:::python
By now, we have made a few checkpoints across two different threads. But what goes into a checkpoint? To inspect a graph's `state` for a given config at any time, call `get_state(config)`.
```python
@@ -148,12 +268,94 @@ StateSnapshot(values={'messages': [HumanMessage(content='Hi there! My name is Wi
snapshot.next # (since the graph ended this turn, `next` is empty. If you fetch a state from within a graph invocation, next tells which node will execute next)
```
:::
:::js
By now, we have made a few checkpoints across two different threads. But what goes into a checkpoint? To inspect a graph's `state` for a given config at any time, call `getState(config)`.
```typescript
await graph.getState({ configurable: { thread_id: "1" } });
```
```typescript
{
values: {
messages: [
HumanMessage {
"id": "32fabcef-b3b8-481f-8bcb-fd83399a5f8d",
"content": "Hi there! My name is Will.",
"additional_kwargs": {},
"response_metadata": {}
},
AIMessage {
"id": "chatcmpl-BrPbTsCJbVqBvXWySlYoTJvM75Kv8",
"content": "Hello Will! How can I assist you today?",
"additional_kwargs": {},
"response_metadata": {},
"tool_calls": [],
"invalid_tool_calls": []
},
HumanMessage {
"id": "561c3aad-f8fc-4fac-94a6-54269a220856",
"content": "Remember my name?",
"additional_kwargs": {},
"response_metadata": {}
},
AIMessage {
"id": "chatcmpl-BrPbU4BhhsUikGbW37hYuF5vvnnE2",
"content": "Yes, I remember your name, Will! How can I help you today?",
"additional_kwargs": {},
"response_metadata": {},
"tool_calls": [],
"invalid_tool_calls": []
}
]
},
next: [],
tasks: [],
metadata: {
source: 'loop',
step: 4,
parents: {},
thread_id: '1'
},
config: {
configurable: {
thread_id: '1',
checkpoint_id: '1f05cccc-9bb6-6270-8004-1d2108bcec77',
checkpoint_ns: ''
}
},
createdAt: '2025-07-09T13:58:27.607Z',
parentConfig: {
configurable: {
thread_id: '1',
checkpoint_ns: '',
checkpoint_id: '1f05cccc-78fa-68d0-8003-ffb01a76b599'
}
}
}
```
```typescript
import * as assert from "node:assert";
// Since the graph ended this turn, `next` is empty.
// If you fetch a state from within a graph invocation, next tells which node will execute next)
assert.deepEqual(snapshot.next, []);
```
:::
The snapshot above contains the current state values, corresponding config, and the `next` node to process. In our case, the graph has reached an `END` state, so `next` is empty.
**Congratulations!** Your chatbot can now maintain conversation state across sessions thanks to LangGraph's checkpointing system. This opens up exciting possibilities for more natural, contextual interactions. LangGraph's checkpointing even handles **arbitrarily complex graph states**, which is much more expressive and powerful than simple chat memory.
Check out the code snippet below to review the graph from this tutorial:
:::python
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
<!---
@@ -204,6 +406,43 @@ memory = InMemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
:::
:::js
```typescript hl_lines="16 26"
import { END, MessagesZodState, START } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { TavilySearch } from "@langchain/tavily";
import { MemorySaver } from "@langchain/langgraph";
import { StateGraph } from "@langchain/langgraph";
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
import { z } from "zod";
const State = z.object({
messages: MessagesZodState.shape.messages,
});
const tools = [new TavilySearch({ maxResults: 2 })];
const llm = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools(tools);
// highlight-next-line
const memory = new MemorySaver();
const graph = new StateGraph(State)
.addNode("chatbot", async (state) => ({
messages: [await llm.invoke(state.messages)],
}))
.addNode("tools", new ToolNode(tools))
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
.addEdge("tools", "chatbot")
.addEdge(START, "chatbot")
// highlight-next-line
.compile({ checkpointer: memory });
```
:::
## 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.
@@ -2,7 +2,15 @@
Agents can be unreliable and may need human input to successfully accomplish tasks. Similarly, for some actions, you may want to require human approval before running to ensure that everything is running as intended.
LangGraph's [persistence](../../concepts/persistence.md) layer supports **human-in-the-loop** workflows, allowing execution to pause and resume based on user feedback. The primary interface to this functionality is the [`interrupt`](../../how-tos/human_in_the_loop/add-human-in-the-loop.md) function. Calling `interrupt` inside a node will pause execution. Execution can be resumed, together with new input from a human, by passing in a [Command](../../concepts/low_level.md#command). `interrupt` is ergonomically similar to Python's built-in `input()`, [with some caveats](../../how-tos/human_in_the_loop/add-human-in-the-loop.md).
LangGraph's [persistence](../../concepts/persistence.md) layer supports **human-in-the-loop** workflows, allowing execution to pause and resume based on user feedback. The primary interface to this functionality is the [`interrupt`](../../how-tos/human_in_the_loop/add-human-in-the-loop.md) function. Calling `interrupt` inside a node will pause execution. Execution can be resumed, together with new input from a human, by passing in a [Command](../../concepts/low_level.md#command).
:::python
`interrupt` is ergonomically similar to Python's built-in `input()`, [with some caveats](../../how-tos/human_in_the_loop/add-human-in-the-loop.md).
:::
:::js
`interrupt` is ergonomically similar to Node.js's built-in `readline.question()` function, [with some caveats](../../how-tos/human_in_the_loop/add-human-in-the-loop.md).
:::
!!! note
@@ -14,6 +22,7 @@ Starting with the existing code from the [Add memory to the chatbot](./3-add-mem
Let's first select a chat model:
:::python
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
<!---
@@ -24,9 +33,24 @@ llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
```
-->
:::
:::js
```typescript
// Add your API key here
process.env.ANTHROPIC_API_KEY = "YOUR_API_KEY";
```
:::
We can now incorporate it into our `StateGraph` with an additional tool:
``` python hl_lines="12 19 20 21 22 23"
:::python
````python hl_lines="12 19 20 21 22 23"
```python hl_lines="12 19 20 21 22 23"
from typing import Annotated
from langchain_tavily import TavilySearch
@@ -74,7 +98,103 @@ graph_builder.add_conditional_edges(
)
graph_builder.add_edge("tools", "chatbot")
graph_builder.add_edge(START, "chatbot")
```
````
:::
:::js
````typescript hl_lines="12 19 20 21 22 23"
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
```typescript hl_lines="1 7-19"
import { interrupt, MessagesZodState } from "@langchain/langgraph";
import { ChatAnthropic } from "@langchain/anthropic";
import { TavilySearch } from "@langchain/tavily";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { MemorySaver } from "@langchain/langgraph";
import {
StateGraph,
START,
END,
MessagesAnnotation,
} from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { ChatAnthropic } from "@langchain/anthropic";
import { Command, interrupt } from "@langchain/langgraph";
const humanAssistance = tool(
async ({ query }) => {
const humanResponse = interrupt({ query });
return humanResponse.data;
},
{
name: "humanAssistance",
description: "Request assistance from a human.",
schema: z.object({
query: z.string().describe("Human readable question for the human"),
}),
}
);
const humanAssistance = tool(
async ({ query }) => {
const humanResponse = interrupt({ query });
return humanResponse.data;
},
{
name: "humanAssistance",
description: "Request assistance from a human.",
schema: z.object({
query: z.string().describe("Human readable question for the human"),
}),
}
);
const searchTool = new TavilySearch({ maxResults: 2 });
const tools = [searchTool, humanAssistance];
const llmWithTools = new ChatAnthropic({
model: "claude-3-5-sonnet-latest",
}).bindTools(tools);
async function chatbot(state: z.infer<typeof MessagesZodState>) {
const message = await llmWithTools.invoke(state.messages);
// Because we will be interrupting during tool execution,
// we disable parallel tool calling to avoid repeating any
// tool invocations when we resume.
if (message.tool_calls && message.tool_calls.length > 1) {
throw new Error("Multiple tool calls not supported with interrupts");
}
return { messages: [message] };
}
const graphBuilder = new StateGraph(MessagesAnnotation).addNode(
"chatbot",
chatbot
);
const toolNode = new ToolNode(tools);
graphBuilder.addNode("tools", toolNode);
const shouldContinue = (state: typeof MessagesAnnotation.State) => {
const messages = state.messages;
const lastMessage = messages[messages.length - 1];
if ("tool_calls" in lastMessage && lastMessage.tool_calls?.length) {
return "tools";
}
return END;
};
graphBuilder.addConditionalEdges("chatbot", shouldContinue);
graphBuilder.addEdge("tools", "chatbot");
graphBuilder.addEdge(START, "chatbot");
````
:::
!!! tip
@@ -84,17 +204,39 @@ graph_builder.add_edge(START, "chatbot")
We compile the graph with a checkpointer, as before:
:::python
```python
memory = InMemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
:::
:::js
```typescript
const memory = new MemorySaver();
const graph = new StateGraph(MessagesZodState)
.addNode("chatbot", chatbot)
.addNode("tools", new ToolNode(tools))
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
.addEdge("tools", "chatbot")
.addEdge(START, "chatbot")
.compile({ checkpointer: memory });
```
:::
## 3. Visualize the graph (optional)
Visualizing the graph, you get the same layout as before – just with the added tool!
``` python
:::python
```python
from IPython.display import Image, display
try:
@@ -104,12 +246,30 @@ except Exception:
pass
```
:::
:::js
```typescript
import * as fs from "node:fs/promises";
const drawableGraph = await graph.getGraphAsync();
const image = await drawableGraph.drawMermaidPng();
const imageBuffer = new Uint8Array(await image.arrayBuffer());
await fs.writeFile("chatbot-with-tools.png", imageBuffer);
```
:::
![chatbot-with-tools-diagram](chatbot-with-tools.png)
## 4. Prompt the chatbot
Now, prompt the chatbot with a question that will engage the new `human_assistance` tool:
:::python
```python
user_input = "I need some expert guidance for building an AI agent. Could you request assistance for me?"
config = {"configurable": {"thread_id": "1"}}
@@ -138,8 +298,60 @@ Tool Calls:
query: A user is requesting expert guidance for building an AI agent. Could you please provide some expert advice or resources on this topic?
```
:::
:::js
```typescript
const userInput =
"I need some expert guidance for building an AI agent. Could you request assistance for me?";
const config = {
configurable: { thread_id: "1" },
streamMode: "values" as const,
};
const events = await graph.stream(
{ messages: [{ role: "user", content: userInput }] },
{ configurable: { thread_id: "1" }, streamMode: "values" }
);
for await (const event of events) {
if ("messages" in event) {
const lastMessage = event.messages.at(-1);
console.log(`[${lastMessage?.getType()}]: ${lastMessage?.text}`);
if (
lastMessage &&
isAIMessage(lastMessage) &&
lastMessage.tool_calls?.length
) {
console.log("Tool calls:", lastMessage.tool_calls);
}
}
}
```
```
[human]: I need some expert guidance for building an AI agent. Could you request assistance for me?
[ai]: I'll help you request human assistance for guidance on building an AI agent.
Tool calls: [
{
name: 'humanAssistance',
args: {
query: 'I would like expert guidance on building an AI agent. Could you please provide assistance with this topic?'
},
id: 'toolu_01Bpxc8rFVMhSaRosS6b85Ts',
type: 'tool_call'
}
]
```
:::
The chatbot generated a tool call, but then execution has been interrupted. If you inspect the graph state, you see that it stopped at the tools node:
:::python
```python
snapshot = graph.get_state(config)
snapshot.next
@@ -149,8 +361,28 @@ snapshot.next
('tools',)
```
:::
:::js
```typescript
const snapshot = await graph.getState({ configurable: { thread_id: "1" } });
snapshot.next;
```
```json
["tools"]
```
['tools']
````
:::
!!! info Additional information
:::python
Take a closer look at the `human_assistance` tool:
```python
@@ -162,12 +394,40 @@ snapshot.next
```
Similar to Python's built-in `input()` function, calling `interrupt` inside the tool will pause execution. Progress is persisted based on the [checkpointer](../../concepts/persistence.md#checkpointer-libraries); so if it is persisting with Postgres, it can resume at any time as long as the database is alive. In this example, it is persisting with the in-memory checkpointer and can resume any time if the Python kernel is running.
:::
:::js
Take a closer look at the `humanAssistance` tool:
```typescript hl_lines="3"
const humanAssistance = tool(
async ({ query }) => {
const humanResponse = interrupt({ query });
return humanResponse.data;
},
{
name: "humanAssistance",
description: "Request assistance from a human.",
schema: z.object({
query: z.string().describe("Human readable question for the human"),
}),
},
);
```
Calling `interrupt` inside the tool will pause execution. Progress is persisted based on the [checkpointer](../../concepts/persistence.md#checkpointer-libraries); so if it is persisting with Postgres, it can resume at any time as long as the database is alive. In this example, it is persisting with the in-memory checkpointer and can resume any time if the JavaScript runtime is running.
:::
## 5. Resume execution
To resume execution, pass a [`Command`](../../concepts/low_level.md#command) object containing data expected by the tool. The format of this data can be customized based on needs. For this example, use a dict with a key `"data"`:
To resume execution, pass a [`Command`](../../concepts/low_level.md#command) object containing data expected by the tool. The format of this data can be customized based on needs.
``` python
:::python
For this example, use a dict with a key `"data"`:
```python
human_response = (
"We, the experts are here to help! We'd recommend you check out LangGraph to build your agent."
" It's much more reliable and extensible than simple autonomous agents."
@@ -179,7 +439,7 @@ events = graph.stream(human_command, config, stream_mode="values")
for event in events:
if "messages" in event:
event["messages"][-1].pretty_print()
```
````
```
================================== Ai Message ==================================
@@ -215,13 +475,58 @@ If you'd like more specific information about LangGraph or have any questions ab
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
```
:::
:::js
For this example, use an object with a key `"data"`:
```typescript
const humanResponse = (
"We, the experts are here to help! We'd recommend you check out LangGraph to build your agent." +
" It's much more reliable and extensible than simple autonomous agents.";
const humanCommand = new Command({ resume: { data: humanResponse } });
const resumeEvents = await graph.stream(humanCommand, {
configurable: { thread_id: "1" },
streamMode: "values",
});
for await (const event of resumeEvents) {
if ("messages" in event) {
const lastMessage = event.messages.at(-1);
console.log(`[${lastMessage?.getType()}]: ${lastMessage?.text}`);
}
}
```
```
[tool]: We, the experts are here to help! We'd recommend you check out LangGraph to build your agent. It's much more reliable and extensible than simple autonomous agents.
[ai]: Thank you for your patience. I've received some expert advice regarding your request for guidance on building an AI agent. Here's what the experts have suggested:
The experts recommend that you look into LangGraph for building your AI agent. They mention that LangGraph is a more reliable and extensible option compared to simple autonomous agents.
LangGraph is likely a framework or library designed specifically for creating AI agents with advanced capabilities. Here are a few points to consider based on this recommendation:
1. Reliability: The experts emphasize that LangGraph is more reliable than simpler autonomous agent approaches. This could mean it has better stability, error handling, or consistent performance.
2. Extensibility: LangGraph is described as more extensible, which suggests that it probably offers a flexible architecture that allows you to easily add new features or modify existing ones as your agent's requirements evolve.
3. Advanced capabilities: Given that it's recommended over "simple autonomous agents," LangGraph likely provides more sophisticated tools and techniques for building complex AI agents.
...
```
:::
The input has been received and processed as a tool message. Review this call's [LangSmith trace](https://smith.langchain.com/public/9f0f87e3-56a7-4dde-9c76-b71675624e91/r) to see the exact work that was done in the above call. Notice that the state is loaded in the first step so that our chatbot can continue where it left off.
**Congratulations!** You've used an `interrupt` to add human-in-the-loop execution to your chatbot, allowing for human oversight and intervention when needed. This opens up the potential UIs you can create with your AI systems. Since you have already added a **checkpointer**, as long as the underlying persistence layer is running, the graph can be paused **indefinitely** and resumed at any time as if nothing had happened.
Check out the code snippet below to review the graph from this tutorial:
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
:::python
{!snippets/chat_model_tabs.md!}
```python
from typing import Annotated
@@ -272,6 +577,94 @@ memory = InMemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
:::
:::js
```typescript
import {
interrupt,
MessagesZodState,
StateGraph,
MemorySaver,
START,
END,
} from "@langchain/langgraph";
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
import { isAIMessage } from "@langchain/core/messages";
import { ChatAnthropic } from "@langchain/anthropic";
import { TavilySearch } from "@langchain/tavily";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const humanAssistance = tool(
async ({ query }) => {
const humanResponse = interrupt({ query });
return humanResponse.data;
},
{
name: "humanAssistance",
description: "Request assistance from a human.",
schema: z.object({
query: z.string().describe("Human readable question for the human"),
}),
}
);
const searchTool = new TavilySearch({ maxResults: 2 });
const tools = [searchTool, humanAssistance];
const llmWithTools = new ChatAnthropic({
model: "claude-3-5-sonnet-latest",
}).bindTools(tools);
const chatbot = async (state: z.infer<typeof MessagesZodState>) => {
const message = await llmWithTools.invoke(state.messages);
// Because we will be interrupting during tool execution,
// we disable parallel tool calling to avoid repeating any
// tool invocations when we resume.
if (message.tool_calls && message.tool_calls.length > 1) {
throw new Error("Multiple tool calls not supported with interrupts");
}
return { messages: message };
};
const graphBuilder = new StateGraph(MessagesAnnotation).addNode(
"chatbot",
chatbot
);
const toolNode = new ToolNode(tools);
graphBuilder.addNode("tools", toolNode);
const shouldContinue = (state: typeof MessagesAnnotation.State) => {
const messages = state.messages;
const lastMessage = messages[messages.length - 1];
if ("tool_calls" in lastMessage && lastMessage.tool_calls?.length) {
return "tools";
}
return END;
};
graphBuilder.addConditionalEdges("chatbot", shouldContinue);
graphBuilder.addEdge("tools", "chatbot");
graphBuilder.addEdge(START, "chatbot");
const memory = new MemorySaver();
const graph = new StateGraph(MessagesZodState)
.addNode("chatbot", chatbot)
.addNode("tools", new ToolNode(tools))
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
.addEdge("tools", "chatbot")
.addEdge(START, "chatbot")
.compile({ checkpointer: memory });
```
:::
## Next steps
So far, the tutorial examples have relied on a simple state with one entry: a list of messages. You can go far with this simple state, but if you want to define complex behavior without relying on the message list, you can [add additional fields to the state](./5-customize-state.md).
So far, the tutorial examples have relied on a simple state with one entry: a list of messages. You can go far with this simple state, but if you want to define complex behavior without relying on the message list, you can [add additional fields to the state](./5-customize-state.md).
@@ -10,6 +10,8 @@ In this tutorial, you will add additional fields to the state to define complex
Update the chatbot to research the birthday of an entity by adding `name` and `birthday` keys to the state:
:::python
```python
from typing import Annotated
@@ -26,13 +28,34 @@ class State(TypedDict):
birthday: str
```
:::
:::js
```typescript
import { MessagesZodState } from "@langchain/langgraph";
import { z } from "zod";
const State = z.object({
messages: MessagesZodState.shape.messages,
// highlight-next-line
name: z.string(),
// highlight-next-line
birthday: z.string(),
});
```
:::
Adding this information to the state makes it easily accessible by other graph nodes (like a downstream node that stores or processes the information), as well as the graph's persistence layer.
## 2. Update the state inside the tool
:::python
Now, populate the state keys inside of the `human_assistance` tool. This allows a human to review the information before it is stored in the state. Use [`Command`](../../concepts/low_level.md#using-inside-tools) to issue a state update from inside the tool.
``` python
```python
from langchain_core.messages import ToolMessage
from langchain_core.tools import InjectedToolCallId, tool
@@ -76,10 +99,78 @@ def human_assistance(
return Command(update=state_update)
```
:::
:::js
Now, populate the state keys inside of the `humanAssistance` tool. This allows a human to review the information before it is stored in the state. Use [`Command`](../../concepts/low_level.md#using-inside-tools) to issue a state update from inside the tool.
```typescript
import { tool } from "@langchain/core/tools";
import { ToolMessage } from "@langchain/core/messages";
import { Command, interrupt } from "@langchain/langgraph";
const humanAssistance = tool(
async (input, config) => {
// Note that because we are generating a ToolMessage for a state update,
// we generally require the ID of the corresponding tool call.
// This is available in the tool's config.
const toolCallId = config?.toolCall?.id as string | undefined;
if (!toolCallId) throw new Error("Tool call ID is required");
const humanResponse = await interrupt({
question: "Is this correct?",
name: input.name,
birthday: input.birthday,
});
// We explicitly update the state with a ToolMessage inside the tool.
const stateUpdate = (() => {
// If the information is correct, update the state as-is.
if (humanResponse.correct?.toLowerCase().startsWith("y")) {
return {
name: input.name,
birthday: input.birthday,
messages: [
new ToolMessage({ content: "Correct", tool_call_id: toolCallId }),
],
};
}
// Otherwise, receive information from the human reviewer.
return {
name: humanResponse.name || input.name,
birthday: humanResponse.birthday || input.birthday,
messages: [
new ToolMessage({
content: `Made a correction: ${JSON.stringify(humanResponse)}`,
tool_call_id: toolCallId,
}),
],
};
})();
// We return a Command object in the tool to update our state.
return new Command({ update: stateUpdate });
},
{
name: "humanAssistance",
description: "Request assistance from a human.",
schema: z.object({
name: z.string().describe("The name of the entity"),
birthday: z.string().describe("The birthday/release date of the entity"),
}),
}
);
```
:::
The rest of the graph stays the same.
## 3. Prompt the chatbot
:::python
Prompt the chatbot to look up the "birthday" of the LangGraph library and direct the chatbot to reach out to the `human_assistance` tool once it has the required information. By setting `name` and `birthday` in the arguments for the tool, you force the chatbot to generate proposals for these fields.
```python
@@ -99,6 +190,51 @@ for event in events:
event["messages"][-1].pretty_print()
```
:::
:::js
Prompt the chatbot to look up the "birthday" of the LangGraph library and direct the chatbot to reach out to the `humanAssistance` tool once it has the required information. By setting `name` and `birthday` in the arguments for the tool, you force the chatbot to generate proposals for these fields.
```typescript
import { isAIMessage } from "@langchain/core/messages";
const userInput =
"Can you look up when LangGraph was released? " +
"When you have the answer, use the humanAssistance tool for review.";
const events = await graph.stream(
{ messages: [{ role: "user", content: userInput }] },
{ configurable: { thread_id: "1" }, streamMode: "values" }
);
for await (const event of events) {
if ("messages" in event) {
const lastMessage = event.messages.at(-1);
console.log(
"=".repeat(32),
`${lastMessage?.getType()} Message`,
"=".repeat(32)
);
console.log(lastMessage?.text);
if (
lastMessage &&
isAIMessage(lastMessage) &&
lastMessage.tool_calls?.length
) {
console.log("Tool Calls:");
for (const call of lastMessage.tool_calls) {
console.log(` ${call.name} (${call.id})`);
console.log(` Args: ${JSON.stringify(call.args)}`);
}
}
}
}
```
:::
```
================================ Human Message =================================
@@ -126,12 +262,20 @@ Tool Calls:
birthday: 2023-01-01
```
:::python
We've hit the `interrupt` in the `human_assistance` tool again.
:::
:::js
We've hit the `interrupt` in the `humanAssistance` tool again.
:::
## 4. Add human assistance
The chatbot failed to identify the correct date, so supply it with information:
:::python
```python
human_command = Command(
resume={
@@ -146,6 +290,53 @@ for event in events:
event["messages"][-1].pretty_print()
```
:::
:::js
```typescript
import { Command } from "@langchain/langgraph";
const humanCommand = new Command({
resume: {
name: "LangGraph",
birthday: "Jan 17, 2024",
},
});
const resumeEvents = await graph.stream(humanCommand, {
configurable: { thread_id: "1" },
streamMode: "values",
});
for await (const event of resumeEvents) {
if ("messages" in event) {
const lastMessage = event.messages.at(-1);
console.log(
"=".repeat(32),
`${lastMessage?.getType()} Message`,
"=".repeat(32)
);
console.log(lastMessage?.text);
if (
lastMessage &&
isAIMessage(lastMessage) &&
lastMessage.tool_calls?.length
) {
console.log("Tool Calls:");
for (const call of lastMessage.tool_calls) {
console.log(` ${call.name} (${call.id})`);
console.log(` Args: ${JSON.stringify(call.args)}`);
}
}
}
}
```
:::
```
================================== Ai Message ==================================
@@ -175,6 +366,8 @@ It's worth noting that LangGraph had been in development and use for some time b
Note that these fields are now reflected in the state:
:::python
```python
snapshot = graph.get_state(config)
@@ -185,13 +378,34 @@ snapshot = graph.get_state(config)
{'name': 'LangGraph', 'birthday': 'Jan 17, 2024'}
```
:::
:::js
```typescript
const snapshot = await graph.getState(config);
const relevantState = Object.fromEntries(
Object.entries(snapshot.values).filter(([k]) =>
["name", "birthday"].includes(k)
)
);
```
```
{ name: 'LangGraph', birthday: 'Jan 17, 2024' }
```
:::
This makes them easily accessible to downstream nodes (e.g., a node that further processes or stores the information).
## 5. Manually update the state
:::python
LangGraph gives a high degree of control over the application state. For instance, at any point (including when interrupted), you can manually override a key using `graph.update_state`:
``` python
```python
graph.update_state(config, {"name": "LangGraph (library)"})
```
@@ -201,11 +415,36 @@ graph.update_state(config, {"name": "LangGraph (library)"})
'checkpoint_id': '1efd4ec5-cf69-6352-8006-9278f1730162'}}
```
:::
:::js
LangGraph gives a high degree of control over the application state. For instance, at any point (including when interrupted), you can manually override a key using `graph.updateState`:
```typescript
await graph.updateState(
{ configurable: { thread_id: "1" } },
{ name: "LangGraph (library)" }
);
```
```typescript
{
configurable: {
thread_id: '1',
checkpoint_ns: '',
checkpoint_id: '1efd4ec5-cf69-6352-8006-9278f1730162'
}
}
```
:::
## 6. View the new value
:::python
If you call `graph.get_state`, you can see the new value is reflected:
``` python
```python
snapshot = graph.get_state(config)
{k: v for k, v in snapshot.values.items() if k in ("name", "birthday")}
@@ -215,12 +454,35 @@ snapshot = graph.get_state(config)
{'name': 'LangGraph (library)', 'birthday': 'Jan 17, 2024'}
```
:::
:::js
If you call `graph.getState`, you can see the new value is reflected:
```typescript
const updatedSnapshot = await graph.getState(config);
const updatedRelevantState = Object.fromEntries(
Object.entries(updatedSnapshot.values).filter(([k]) =>
["name", "birthday"].includes(k)
)
);
```
```typescript
{ name: 'LangGraph (library)', birthday: 'Jan 17, 2024' }
```
:::
Manual state updates will [generate a trace](https://smith.langchain.com/public/7ebb7827-378d-49fe-9f6c-5df0e90086c8/r) in LangSmith. If desired, they can also be used to [control human-in-the-loop workflows](../../how-tos/human_in_the_loop/add-human-in-the-loop.md). Use of the `interrupt` function is generally recommended instead, as it allows data to be transmitted in a human-in-the-loop interaction independently of state updates.
**Congratulations!** You've added custom keys to the state to facilitate a more complex workflow, and learned how to generate state updates from inside tools.
Check out the code snippet below to review the graph from this tutorial:
:::python
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
<!---
@@ -305,7 +567,111 @@ memory = InMemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
:::
:::js
```typescript
import {
Command,
interrupt,
MessagesZodState,
MemorySaver,
StateGraph,
END,
START,
} from "@langchain/langgraph";
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
import { ChatAnthropic } from "@langchain/anthropic";
import { TavilySearch } from "@langchain/tavily";
import { ToolMessage } from "@langchain/core/messages";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const State = z.object({
messages: MessagesZodState.shape.messages,
name: z.string(),
birthday: z.string(),
});
const humanAssistance = tool(
async (input, config) => {
// Note that because we are generating a ToolMessage for a state update, we
// generally require the ID of the corresponding tool call. This is available
// in the tool's config.
const toolCallId = config?.toolCall?.id as string | undefined;
if (!toolCallId) throw new Error("Tool call ID is required");
const humanResponse = await interrupt({
question: "Is this correct?",
name: input.name,
birthday: input.birthday,
});
// We explicitly update the state with a ToolMessage inside the tool.
const stateUpdate = (() => {
// If the information is correct, update the state as-is.
if (humanResponse.correct?.toLowerCase().startsWith("y")) {
return {
name: input.name,
birthday: input.birthday,
messages: [
new ToolMessage({ content: "Correct", tool_call_id: toolCallId }),
],
};
}
// Otherwise, receive information from the human reviewer.
return {
name: humanResponse.name || input.name,
birthday: humanResponse.birthday || input.birthday,
messages: [
new ToolMessage({
content: `Made a correction: ${JSON.stringify(humanResponse)}`,
tool_call_id: toolCallId,
}),
],
};
})();
// We return a Command object in the tool to update our state.
return new Command({ update: stateUpdate });
},
{
name: "humanAssistance",
description: "Request assistance from a human.",
schema: z.object({
name: z.string().describe("The name of the entity"),
birthday: z.string().describe("The birthday/release date of the entity"),
}),
}
);
const searchTool = new TavilySearch({ maxResults: 2 });
const tools = [searchTool, humanAssistance];
const llmWithTools = new ChatAnthropic({
model: "claude-3-5-sonnet-latest",
}).bindTools(tools);
const memory = new MemorySaver();
const chatbot = async (state: z.infer<typeof State>) => {
const message = await llmWithTools.invoke(state.messages);
return { messages: message };
};
const graph = new StateGraph(State)
.addNode("chatbot", chatbot)
.addNode("tools", new ToolNode(tools))
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
.addEdge("tools", "chatbot")
.addEdge(START, "chatbot")
.compile({ checkpointer: memory });
```
:::
## Next steps
There's one more concept to review before finishing the LangGraph basics tutorials: connecting `checkpointing` and `state updates` to [time travel](./6-time-travel.md).
There's one more concept to review before finishing the LangGraph basics tutorials: connecting `checkpointing` and `state updates` to [time travel](./6-time-travel.md).
+341 -17
View File
@@ -4,7 +4,7 @@ In a typical chatbot workflow, the user interacts with the bot one or more times
What if you want a user to be able to start from a previous response and explore a different outcome? Or what if you want users to be able to rewind your chatbot's work to fix mistakes or try a different strategy, something that is common in applications like autonomous software engineers?
You can create these types of experiences using LangGraph's built-in **time travel** functionality.
You can create these types of experiences using LangGraph's built-in **time travel** functionality.
!!! note
@@ -12,7 +12,15 @@ You can create these types of experiences using LangGraph's built-in **time trav
## 1. Rewind your graph
:::python
Rewind your graph by fetching a checkpoint using the graph's `get_state_history` method. You can then resume execution at this previous point in time.
:::
:::js
Rewind your graph by fetching a checkpoint using the graph's `getStateHistory` method. You can then resume execution at this previous point in time.
:::
:::python
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
@@ -64,11 +72,49 @@ memory = InMemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
:::
:::js
```typescript
import {
StateGraph,
START,
END,
MessagesZodState,
MemorySaver,
} from "@langchain/langgraph";
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
import { TavilySearch } from "@langchain/tavily";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
const State = z.object({ messages: MessagesZodState.shape.messages });
const tools = [new TavilySearch({ maxResults: 2 })];
const llmWithTools = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools(tools);
const memory = new MemorySaver();
const graph = new StateGraph(State)
.addNode("chatbot", async (state) => ({
messages: [await llmWithTools.invoke(state.messages)],
}))
.addNode("tools", new ToolNode(tools))
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
.addEdge("tools", "chatbot")
.addEdge(START, "chatbot")
.compile({ checkpointer: memory });
```
:::
## 2. Add steps
Add steps to your graph. Every step will be checkpointed in its state history:
``` python
:::python
```python
config = {"configurable": {"thread_id": "1"}}
events = graph.stream(
{
@@ -159,7 +205,7 @@ Tool Calls:
================================= Tool Message =================================
Name: tavily_search_results_json
[{"url": "https://towardsdatascience.com/building-autonomous-multi-tool-agents-with-gemini-2-0-and-langgraph-ad3d7bd5e79d", "content": "Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph | by Youness Mansar | Jan, 2025 | Towards Data Science Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph A practical tutorial with full code examples for building and running multi-tool agents Towards Data Science LLMs are remarkable — they can memorize vast amounts of information, answer general knowledge questions, write code, generate stories, and even fix your grammar. In this tutorial, we are going to build a simple LLM agent that is equipped with four tools that it can use to answer a user’s question. This Agent will have the following specifications: Follow Published in Towards Data Science --------------------------------- Your home for data science and AI. Follow Follow Follow"}, {"url": "https://github.com/anmolaman20/Tools_and_Agents", "content": "GitHub - anmolaman20/Tools_and_Agents: This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository serves as a comprehensive guide for building AI-powered agents using Langchain and Langgraph. It provides hands-on examples, practical tutorials, and resources for developers and AI enthusiasts to master building intelligent systems and workflows. AI Agent Development: Gain insights into creating intelligent systems that think, reason, and adapt in real time. This repository is ideal for AI practitioners, developers exploring language models, or anyone interested in building intelligent systems. This repository provides resources for building AI agents using Langchain and Langgraph."}]
[{"url": "https://towardsdatascience.com/building-autonomous-multi-tool-agents-with-gemini-2-0-and-langgraph-ad3d7bd5e79d", "content": "Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph | by Youness Mansar | Jan, 2025 | Towards Data Science Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph A practical tutorial with full code examples for building and running multi-tool agents Towards Data Science LLMs are remarkable — they can memorize vast amounts of information, answer general knowledge questions, write code, generate stories, and even fix your grammar. In this tutorial, we are going to build a simple LLM agent that is equipped with four tools that it can use to answer a user's question. This Agent will have the following specifications: Follow Published in Towards Data Science --------------------------------- Your home for data science and AI. Follow Follow Follow"}, {"url": "https://github.com/anmolaman20/Tools_and_Agents", "content": "GitHub - anmolaman20/Tools_and_Agents: This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository serves as a comprehensive guide for building AI-powered agents using Langchain and Langgraph. It provides hands-on examples, practical tutorials, and resources for developers and AI enthusiasts to master building intelligent systems and workflows. AI Agent Development: Gain insights into creating intelligent systems that think, reason, and adapt in real time. This repository is ideal for AI practitioners, developers exploring language models, or anyone interested in building intelligent systems. This repository provides resources for building AI agents using Langchain and Langgraph."}]
================================== Ai Message ==================================
Great idea! Building an autonomous agent with LangGraph is definitely an exciting project. Based on the latest information I've found, here are some insights and tips for building autonomous agents with LangGraph:
@@ -177,11 +223,140 @@ Building an autonomous agent is an iterative process, so be prepared to refine a
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
```
:::
:::js
```typescript
import { randomUUID } from "node:crypto";
const threadId = randomUUID();
let iter = 0;
for (const userInput of [
"I'm learning LangGraph. Could you do some research on it for me?",
"Ya that's helpful. Maybe I'll build an autonomous agent with it!",
]) {
iter += 1;
console.log(`\n--- Conversation Turn ${iter} ---\n`);
const events = await graph.stream(
{ messages: [{ role: "user", content: userInput }] },
{ configurable: { thread_id: threadId }, streamMode: "values" }
);
for await (const event of events) {
if ("messages" in event) {
const lastMessage = event.messages.at(-1);
console.log(
"=".repeat(32),
`${lastMessage?.getType()} Message`,
"=".repeat(32)
);
console.log(lastMessage?.text);
}
}
}
```
```
--- Conversation Turn 1 ---
================================ human Message ================================
I'm learning LangGraph.js. Could you do some research on it for me?
================================ ai Message ================================
I'll search for information about LangGraph.js for you.
================================ tool Message ================================
{
"query": "LangGraph.js framework TypeScript langchain what is it tutorial guide",
"follow_up_questions": null,
"answer": null,
"images": [],
"results": [
{
"url": "https://techcommunity.microsoft.com/blog/educatordeveloperblog/an-absolute-beginners-guide-to-langgraph-js/4212496",
"title": "An Absolute Beginner's Guide to LangGraph.js",
"content": "(...)",
"score": 0.79369855,
"raw_content": null
},
{
"url": "https://langchain-ai.github.io/langgraphjs/",
"title": "LangGraph.js",
"content": "(...)",
"score": 0.78154784,
"raw_content": null
}
],
"response_time": 2.37
}
================================ ai Message ================================
Let me provide you with an overview of LangGraph.js based on the search results:
LangGraph.js is a JavaScript/TypeScript library that's part of the LangChain ecosystem, specifically designed for creating and managing complex LLM (Large Language Model) based workflows. Here are the key points about LangGraph.js:
1. Purpose:
- It's a low-level orchestration framework for building controllable agents
- Particularly useful for creating agentic workflows where LLMs decide the course of action based on current state
- Helps model workflows as graphs with nodes and edges
(...)
--- Conversation Turn 2 ---
================================ human Message ================================
Ya that's helpful. Maybe I'll build an autonomous agent with it!
================================ ai Message ================================
Let me search for specific information about building autonomous agents with LangGraph.js.
================================ tool Message ================================
{
"query": "how to build autonomous agents with LangGraph.js examples tutorial react agent",
"follow_up_questions": null,
"answer": null,
"images": [],
"results": [
{
"url": "https://ai.google.dev/gemini-api/docs/langgraph-example",
"title": "ReAct agent from scratch with Gemini 2.5 and LangGraph",
"content": "(...)",
"score": 0.7602419,
"raw_content": null
},
{
"url": "https://www.youtube.com/watch?v=ZfjaIshGkmk",
"title": "Build Autonomous AI Agents with ReAct and LangGraph Tools",
"content": "(...)",
"score": 0.7471924,
"raw_content": null
}
],
"response_time": 1.98
}
================================ ai Message ================================
Based on the search results, I can provide you with a practical overview of how to build an autonomous agent with LangGraph.js. Here's what you need to know:
1. Basic Structure for Building an Agent:
- LangGraph.js provides a ReAct (Reason + Act) pattern implementation
- The basic components include:
- State management for conversation history
- Nodes for different actions
- Edges for decision-making flow
- Tools for specific functionalities
(...)
```
:::
## 3. Replay the full state history
Now that you have added steps to the chatbot, you can `replay` the full state history to see everything that occurred.
``` python
:::python
```python
to_replay = None
for state in graph.get_state_history(config):
print("Num Messages: ", len(state.values["messages"]), "Next: ", state.next)
@@ -214,10 +389,61 @@ Num Messages: 0 Next: ('__start__',)
--------------------------------------------------------------------------------
```
Checkpoints are saved for every step of the graph. This __spans invocations__ so you can rewind across a full thread's history.
:::
:::js
```typescript
import type { StateSnapshot } from "@langchain/langgraph";
let toReplay: StateSnapshot | undefined;
for await (const state of graph.getStateHistory({
configurable: { thread_id: threadId },
})) {
console.log(
`Num Messages: ${state.values.messages.length}, Next: ${JSON.stringify(
state.next
)}`
);
console.log("-".repeat(80));
if (state.values.messages.length === 6) {
// We are somewhat arbitrarily selecting a specific state based on the number of chat messages in the state.
toReplay = state;
}
}
```
```
Num Messages: 8, Next: []
--------------------------------------------------------------------------------
Num Messages: 7, Next: ["chatbot"]
--------------------------------------------------------------------------------
Num Messages: 6, Next: ["tools"]
--------------------------------------------------------------------------------
Num Messages: 5, Next: ["chatbot"]
--------------------------------------------------------------------------------
Num Messages: 4, Next: ["__start__"]
--------------------------------------------------------------------------------
Num Messages: 4, Next: []
--------------------------------------------------------------------------------
Num Messages: 3, Next: ["chatbot"]
--------------------------------------------------------------------------------
Num Messages: 2, Next: ["tools"]
--------------------------------------------------------------------------------
Num Messages: 1, Next: ["chatbot"]
--------------------------------------------------------------------------------
Num Messages: 0, Next: ["__start__"]
--------------------------------------------------------------------------------
```
:::
Checkpoints are saved for every step of the graph. This **spans invocations** so you can rewind across a full thread's history.
## Resume from a checkpoint
:::python
Resume from the `to_replay` state, which is after the `chatbot` node in the second graph invocation. Resuming from this point will call the **action** node next.
```python
@@ -230,12 +456,37 @@ print(to_replay.config)
{'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1efd43e3-0c1f-6c4e-8006-891877d65740'}}
```
:::
:::js
Resume from the `toReplay` state, which is after the `chatbot` node in one of the graph invocations. Resuming from this point will call the next scheduled node.
```typescript
console.log(toReplay.next);
console.log(toReplay.config);
```
```
["tools"]
{
configurable: {
thread_id: "007708b8-ea9b-4ff7-a7ad-3843364dbf75",
checkpoint_ns: "",
checkpoint_id: "1efd43e3-0c1f-6c4e-8006-891877d65740"
}
}
```
:::
## 4. Load a state from a moment-in-time
:::python
The checkpoint's `to_replay.config` contains a `checkpoint_id` timestamp. Providing this `checkpoint_id` value tells LangGraph's checkpointer to **load** the state from that moment in time.
``` python
```python
# The `checkpoint_id` in the `to_replay.config` corresponds to a state we've persisted to our checkpointer.
for event in graph.stream(None, to_replay.config, stream_mode="values"):
if "messages" in event:
@@ -254,19 +505,16 @@ Tool Calls:
================================= Tool Message =================================
Name: tavily_search_results_json
[{"url": "https://towardsdatascience.com/building-autonomous-multi-tool-agents-with-gemini-2-0-and-langgraph-ad3d7bd5e79d", "content": "Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph | by Youness Mansar | Jan, 2025 | Towards Data Science Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph A practical tutorial with full code examples for building and running multi-tool agents Towards Data Science LLMs are remarkable — they can memorize vast amounts of information, answer general knowledge questions, write code, generate stories, and even fix your grammar. In this tutorial, we are going to build a simple LLM agent that is equipped with four tools that it can use to answer a user’s question. This Agent will have the following specifications: Follow Published in Towards Data Science --------------------------------- Your home for data science and AI. Follow Follow Follow"}, {"url": "https://github.com/anmolaman20/Tools_and_Agents", "content": "GitHub - anmolaman20/Tools_and_Agents: This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository serves as a comprehensive guide for building AI-powered agents using Langchain and Langgraph. It provides hands-on examples, practical tutorials, and resources for developers and AI enthusiasts to master building intelligent systems and workflows. AI Agent Development: Gain insights into creating intelligent systems that think, reason, and adapt in real time. This repository is ideal for AI practitioners, developers exploring language models, or anyone interested in building intelligent systems. This repository provides resources for building AI agents using Langchain and Langgraph."}]
[{"url": "https://towardsdatascience.com/building-autonomous-multi-tool-agents-with-gemini-2-0-and-langgraph-ad3d7bd5e79d", "content": "Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph | by Youness Mansar | Jan, 2025 | Towards Data Science Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph A practical tutorial with full code examples for building and running multi-tool agents Towards Data Science LLMs are remarkable — they can memorize vast amounts of information, answer general knowledge questions, write code, generate stories, and even fix your grammar. In this tutorial, we are going to build a simple LLM agent that is equipped with four tools that it can use to answer a user's question. This Agent will have the following specifications: Follow Published in Towards Data Science --------------------------------- Your home for data science and AI. Follow Follow Follow"}, {"url": "https://github.com/anmolaman20/Tools_and_Agents", "content": "GitHub - anmolaman20/Tools_and_Agents: This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository serves as a comprehensive guide for building AI-powered agents using Langchain and Langgraph. It provides hands-on examples, practical tutorials, and resources for developers and AI enthusiasts to master building intelligent systems and workflows. AI Agent Development: Gain insights into creating intelligent systems that think, reason, and adapt in real time. This repository is ideal for AI practitioners, developers exploring language models, or anyone interested in building intelligent systems. This repository provides resources for building AI agents using Langchain and Langgraph."}]
================================== Ai Message ==================================
Great idea! Building an autonomous agent with LangGraph is indeed an excellent way to apply and deepen your understanding of the technology. Based on the search results, I can provide you with some insights and resources to help you get started:
Great idea! Building an autonomous agent with LangGraph is definitely an exciting project. Based on the latest information I've found, here are some insights and tips for building autonomous agents with LangGraph:
1. Multi-Tool Agents:
LangGraph is well-suited for building autonomous agents that can use multiple tools. This allows your agent to have a variety of capabilities and choose the appropriate tool based on the task at hand.
1. Multi-Tool Agents: LangGraph is particularly well-suited for creating autonomous agents that can use multiple tools. This allows your agent to have a diverse set of capabilities and choose the right tool for each task.
2. Integration with Large Language Models (LLMs):
There's a tutorial that specifically mentions using Gemini 2.0 (Google's LLM) with LangGraph to build autonomous agents. This suggests that LangGraph can be integrated with various LLMs, giving you flexibility in choosing the language model that best fits your needs.
2. Integration with Large Language Models (LLMs): You can combine LangGraph with powerful LLMs like Gemini 2.0 to create more intelligent and capable agents. The LLM can serve as the "brain" of your agent, making decisions and generating responses.
3. Practical Tutorials:
There are tutorials available that provide full code examples for building and running multi-tool agents. These can be invaluable as you start your project, giving you a concrete starting point and demonstrating best practices.
3. Workflow Management: LangGraph excels at managing complex, multi-step AI workflows. This is crucial for autonomous agents that need to break down tasks into smaller steps and execute them in the right order.
...
Remember, building an autonomous agent is an iterative process. Start simple and gradually increase complexity as you become more comfortable with LangGraph and its capabilities.
@@ -275,7 +523,83 @@ Would you like more information on any specific aspect of building your autonomo
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
```
The graph resumed execution from the `action` node. You can tell this is the case since the first value printed above is the response from our search engine tool.
The graph resumed execution from the `tools` node. You can tell this is the case since the first value printed above is the response from our search engine tool.
:::
:::js
The checkpoint's `toReplay.config` contains a `checkpoint_id` timestamp. Providing this `checkpoint_id` value tells LangGraph's checkpointer to **load** the state from that moment in time.
```typescript
// The `checkpoint_id` in the `toReplay.config` corresponds to a state we've persisted to our checkpointer.
for await (const event of await graph.stream(null, {
...toReplay?.config,
streamMode: "values",
})) {
if ("messages" in event) {
const lastMessage = event.messages.at(-1);
console.log(
"=".repeat(32),
`${lastMessage?.getType()} Message`,
"=".repeat(32)
);
console.log(lastMessage?.text);
}
}
```
```
================================ ai Message ================================
Let me search for specific information about building autonomous agents with LangGraph.js.
================================ tool Message ================================
{
"query": "how to build autonomous agents with LangGraph.js examples tutorial",
"follow_up_questions": null,
"answer": null,
"images": [],
"results": [
{
"url": "https://www.mongodb.com/developer/languages/typescript/build-javascript-ai-agent-langgraphjs-mongodb/",
"title": "Build a JavaScript AI Agent With LangGraph.js and MongoDB",
"content": "(...)",
"score": 0.7672197,
"raw_content": null
},
{
"url": "https://medium.com/@lorevanoudenhove/how-to-build-ai-agents-with-langgraph-a-step-by-step-guide-5d84d9c7e832",
"title": "How to Build AI Agents with LangGraph: A Step-by-Step Guide",
"content": "(...)",
"score": 0.7407191,
"raw_content": null
}
],
"response_time": 0.82
}
================================ ai Message ================================
Based on the search results, I can share some practical information about building autonomous agents with LangGraph.js. Here are some concrete examples and approaches:
1. Example HR Assistant Agent:
- Can handle HR-related queries using employee information
- Features include:
- Starting and continuing conversations
- Looking up information using vector search
- Persisting conversation state using checkpoints
- Managing threaded conversations
2. Energy Savings Calculator Agent:
- Functions as a lead generation tool for solar panel sales
- Capabilities include:
- Calculating potential energy savings
- Handling multi-step conversations
- Processing user inputs for personalized estimates
- Managing conversation state
(...)
```
The graph resumed execution from the `tools` node. You can tell this is the case since the first value printed above is the response from our search engine tool.
:::
**Congratulations!** You've now used time-travel checkpoint traversal in LangGraph. Being able to rewind and explore alternative paths opens up a world of possibilities for debugging, experimentation, and interactive applications.
@@ -285,4 +609,4 @@ Take your LangGraph journey further by exploring deployment and advanced feature
- **[LangGraph Server quickstart](../../tutorials/langgraph-platform/local-server.md)**: Launch a LangGraph server locally and interact with it using the REST API and LangGraph Studio Web UI.
- **[LangGraph Platform quickstart](../../cloud/quick_start.md)**: Deploy your LangGraph app using LangGraph Platform.
- **[LangGraph Platform concepts](../../concepts/langgraph_platform.md)**: Understand the foundational concepts of the LangGraph Platform.
- **[LangGraph Platform concepts](../../concepts/langgraph_platform.md)**: Understand the foundational concepts of the LangGraph Platform.
-216
View File
@@ -1,216 +0,0 @@
"""Unit tests for cross-reference preprocessing functionality."""
from unittest.mock import patch
import pytest
from _scripts.handle_auto_links import _transform_link, _replace_autolinks
@pytest.fixture
def mock_link_maps():
"""Fixture providing mock link maps for testing."""
mock_scope_maps = {
"python": {"py-link": "https://example.com/python"},
"js": {"js-link": "https://example.com/js"},
}
with patch("_scripts.handle_auto_links.SCOPE_LINK_MAPS", mock_scope_maps):
yield mock_scope_maps
def test_transform_link_basic(mock_link_maps) -> None:
"""Test basic link transformation."""
# Test with a known link
result = _transform_link("py-link", "python", "test.md", 1)
assert result == "[py-link](https://example.com/python)"
# Test with an unknown link (returns None)
result = _transform_link("unknown-link", "global", "test.md", 1)
assert result is None
def test_transform_link_with_custom_title(mock_link_maps) -> None:
"""Test link transformation with custom title."""
# Test with a known link and custom title
result = _transform_link("py-link", "python", "test.md", 1, "Custom Python Link")
assert result == "[Custom Python Link](https://example.com/python)"
# Test with unknown link and custom title (should still return None)
result = _transform_link("unknown-link", "python", "test.md", 1, "Custom Title")
assert result is None
def test_no_cross_refs(mock_link_maps) -> None:
"""Test markdown with no @[references]."""
lines = ["# Title\n", "Regular text.\n"]
markdown = "".join(lines)
result = _replace_autolinks(markdown, "test.md")
expected = "".join(["# Title\n", "Regular text.\n"])
assert result == expected
def test_global_cross_refs(mock_link_maps) -> None:
"""Test @[references] in global scope (no conditional blocks)."""
lines = ["@[global-link]\n", "Text with @[unknown-link].\n"]
markdown = "".join(lines)
result = _replace_autolinks(markdown, "test.md")
expected = "".join(["@[global-link]\n", "Text with @[unknown-link].\n"])
assert result == expected
def test_python_conditional_block(mock_link_maps) -> None:
"""Test @[references] inside Python conditional block."""
lines = [":::python\n", "@[py-link]\n", ":::\n"]
markdown = "".join(lines)
result = _replace_autolinks(markdown, "test.md")
expected = "".join(
[":::python\n", "[py-link](https://example.com/python)\n", ":::\n"]
)
assert result == expected
def test_js_conditional_block(mock_link_maps) -> None:
"""Test @[references] inside JavaScript conditional block."""
lines = [":::js\n", "@[js-link]\n", ":::\n"]
markdown = "".join(lines)
result = _replace_autolinks(markdown, "test.md")
expected = "".join([":::js\n", "[js-link](https://example.com/js)\n", ":::\n"])
assert result == expected
def test_all_scopes(mock_link_maps) -> None:
"""Test @[references] in global, Python, and JavaScript scopes."""
lines = [
"@[global-link]\n",
":::python\n",
"@[py-link]\n",
":::\n",
"@[global-link]\n",
":::js\n",
"@[js-link]\n",
":::\n",
"@[global-link]\n",
]
markdown = "".join(lines)
result = _replace_autolinks(markdown, "test.md")
expected = "".join(
[
"@[global-link]\n",
":::python\n",
"[py-link](https://example.com/python)\n",
":::\n",
"@[global-link]\n",
":::js\n",
"[js-link](https://example.com/js)\n",
":::\n",
"@[global-link]\n",
]
)
assert result == expected
def test_fence_resets_to_global(mock_link_maps) -> None:
"""Test that closing fence resets scope to global."""
lines = [":::python\n", "@[py-link]\n", ":::\n", "@[global-link]\n"]
markdown = "".join(lines)
result = _replace_autolinks(markdown, "test.md")
expected = "".join(
[
":::python\n",
"[py-link](https://example.com/python)\n",
":::\n",
"@[global-link]\n",
]
)
assert result == expected
def test_indented_conditional_fences(mock_link_maps) -> None:
"""Test @[references] inside indented conditional fences (e.g., in tabs or admonitions)."""
lines = [
"@[global-link]\n",
" :::python\n",
" @[py-link]\n",
" :::\n",
"@[global-link]\n",
"\t\t:::js\n",
"\t\t@[js-link]\n",
"\t\t:::\n",
"@[global-link]\n",
]
markdown = "".join(lines)
result = _replace_autolinks(markdown, "test.md")
expected = "".join(
[
"@[global-link]\n",
" :::python\n",
" [py-link](https://example.com/python)\n",
" :::\n",
"@[global-link]\n",
"\t\t:::js\n",
"\t\t[js-link](https://example.com/js)\n",
"\t\t:::\n",
"@[global-link]\n",
]
)
assert result == expected
def test_custom_title_syntax(mock_link_maps) -> None:
"""Test @[title][ref] syntax with custom titles."""
lines = [
":::python\n",
"@[Custom Python Title][py-link]\n",
":::\n",
":::js\n",
"@[Custom JS Title][js-link]\n",
":::\n"
]
markdown = "".join(lines)
result = _replace_autolinks(markdown, "test.md")
expected = "".join([
":::python\n",
"[Custom Python Title](https://example.com/python)\n",
":::\n",
":::js\n",
"[Custom JS Title](https://example.com/js)\n",
":::\n"
])
assert result == expected
def test_mixed_syntax_compatibility(mock_link_maps) -> None:
"""Test that both @[ref] and @[title][ref] syntax work together."""
lines = [
":::python\n",
"@[py-link]\n", # Old syntax
"@[Custom Title][py-link]\n", # New syntax
":::\n"
]
markdown = "".join(lines)
result = _replace_autolinks(markdown, "test.md")
expected = "".join([
":::python\n",
"[py-link](https://example.com/python)\n",
"[Custom Title](https://example.com/python)\n",
":::\n"
])
assert result == expected
def test_custom_title_with_unknown_link(mock_link_maps) -> None:
"""Test @[title][ref] syntax with unknown reference."""
lines = [
":::python\n",
"@[Custom Title][unknown-link]\n",
":::\n"
]
markdown = "".join(lines)
result = _replace_autolinks(markdown, "test.md")
expected = "".join([
":::python\n",
"@[Custom Title][unknown-link]\n", # Should remain unchanged
":::\n"
])
assert result == expected
@@ -165,7 +165,7 @@ def patch_config(
Defaults to None.
recursion_limit: The recursion limit to set.
Defaults to None.
max_concurrency: The max number of concurrent steps to run, which also applies to parallelized steps.
max_concurrency: The max concurrency to set.
Defaults to None.
run_name: The run name to set. Defaults to None.
configurable: The configurable to set.
+64 -62
View File
@@ -22,7 +22,6 @@ from typing import (
Protocol,
Union,
cast,
get_type_hints,
)
from langchain_core.runnables.base import (
@@ -128,27 +127,71 @@ ANY_TYPE = object()
ASYNCIO_ACCEPTS_CONTEXT = sys.version_info >= (3, 11)
# Configuration for keyword arguments that can be injected at runtime
KWARGS_CONFIG_KEYS: tuple[tuple[str, str, Any], ...] = (
("config", "N/A", inspect.Parameter.empty),
("writer", "stream_writer", lambda _: None),
("store", "store", inspect.Parameter.empty),
("previous", "previous", inspect.Parameter.empty),
("runtime", "N/A", inspect.Parameter.empty),
# List of keyword arguments that can be injected into nodes / tasks / tools at runtime.
# A named argument may appear multiple times if it appears with distinct types.
KWARGS_CONFIG_KEYS: tuple[tuple[str, tuple[Any, ...], str, Any], ...] = (
(
"config",
(RunnableConfig, "RunnableConfig", inspect.Parameter.empty),
# for now, use config directly, eventually, will pop off of Runtime
"N/A",
inspect.Parameter.empty,
),
(
"writer",
(StreamWriter, "StreamWriter", inspect.Parameter.empty),
"stream_writer",
lambda _: None,
),
(
"store",
(
BaseStore,
"BaseStore",
inspect.Parameter.empty,
),
"store",
inspect.Parameter.empty,
),
(
"store",
(
Optional[BaseStore],
"Optional[BaseStore]",
),
"store",
None,
),
(
"previous",
(ANY_TYPE,),
"previous",
inspect.Parameter.empty,
),
(
"runtime",
(ANY_TYPE,),
# we never hit this block, we just inject runtime directly
"N/A",
inspect.Parameter.empty,
),
)
"""List of kwargs that can be passed to functions, and their corresponding
runtime keys and default values.
config keys, default values and type annotations.
Used to configure keyword arguments that can be injected at runtime
from the `Runtime` object as kwargs to `invoke`, `ainvoke`, `stream` and `astream`.
For a keyword to be injected from the config object, the function signature
must contain a kwarg with the same name and a compatible type annotation.
must contain a kwarg with the same name and a matching type annotation.
Each tuple contains:
- the name of the kwarg in the function signature
- the `Runtime` attribute for fetching the value (N/A if not applicable)
- the default value to use if the runtime value is missing
- the type annotation(s) for the kwarg
- the `Runtime` attribute for fetching the value (N/A if not applicable)
This is fully internal and should be further refactored to use `get_type_hints`
to resolve forward references and optional types formatted like BaseStore | None.
"""
VALID_KINDS = (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
@@ -242,63 +285,22 @@ class RunnableCallable(Runnable):
raise ValueError("At least one of func or afunc must be provided.")
self.func_accepts: dict[str, tuple[str, Any]] = {}
func_or_afunc = cast(Callable, func or afunc)
params = inspect.signature(func_or_afunc).parameters
# Get resolved type hints to properly handle forward references and unions
try:
type_hints = get_type_hints(func_or_afunc)
except (NameError, AttributeError):
# Fallback to raw annotations if type resolution fails
type_hints = getattr(func_or_afunc, '__annotations__', {})
params = inspect.signature(cast(Callable, func or afunc)).parameters
for kw, runtime_key, default in KWARGS_CONFIG_KEYS:
for kw, typ, runtime_key, default in KWARGS_CONFIG_KEYS:
p = params.get(kw)
if p is None or p.kind not in VALID_KINDS:
# If parameter is not found or is not a valid kind, skip
continue
# Get the resolved type hint for this parameter
param_type = type_hints.get(kw, p.annotation)
# Check if this parameter should be injected based on its type
if self._should_inject_param(kw, param_type):
self.func_accepts[kw] = (runtime_key, default)
def _should_inject_param(self, param_name: str, param_type: Any) -> bool:
"""Determine if a parameter should be injected based on its name and type."""
if param_name == "config":
# Accept RunnableConfig, Optional[RunnableConfig], or no annotation
return (
param_type is inspect.Parameter.empty
or param_type is RunnableConfig
or param_type == Optional[RunnableConfig]
or (hasattr(param_type, '__origin__') and param_type.__origin__ is Union
and RunnableConfig in param_type.__args__
and type(None) in param_type.__args__)
)
elif param_name == "writer":
# Accept StreamWriter or no annotation
return (
param_type is inspect.Parameter.empty
or param_type is StreamWriter
)
elif param_name == "store":
# Accept BaseStore, Optional[BaseStore], or no annotation
return (
param_type is inspect.Parameter.empty
or param_type is BaseStore
or param_type == Optional[BaseStore]
or (hasattr(param_type, '__origin__') and param_type.__origin__ is Union
and BaseStore in param_type.__args__
and type(None) in param_type.__args__)
)
elif param_name in ("previous", "runtime"):
# Accept any type for previous and runtime
return True
return False
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.
continue
# If the kwarg is accepted by the function, store the key / runtime attribute to inject
self.func_accepts[kw] = (runtime_key, default)
def __repr__(self) -> str:
repr_args = {
+3 -11
View File
@@ -116,7 +116,7 @@ from langgraph.pregel._validate import validate_graph, validate_keys
from langgraph.pregel._write import ChannelWrite, ChannelWriteEntry
from langgraph.pregel.debug import get_bolded_text, get_colored_text, tasks_w_writes
from langgraph.pregel.protocol import PregelProtocol, StreamChunk, StreamProtocol
from langgraph.runtime import DEFAULT_RUNTIME, Runtime
from langgraph.runtime import Runtime
from langgraph.store.base import BaseStore
from langgraph.types import (
All,
@@ -2570,16 +2570,12 @@ class Pregel(
if durability is not None or deprecated_checkpoint_during is not None:
config[CONF][CONFIG_KEY_DURABILITY] = durability_
runtime = Runtime(
config[CONF][CONFIG_KEY_RUNTIME] = Runtime(
context=context,
store=store,
stream_writer=stream_writer,
previous=None,
)
parent_runtime = config[CONF].get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME)
runtime = parent_runtime.merge(runtime)
config[CONF][CONFIG_KEY_RUNTIME] = runtime
with SyncPregelLoop(
input,
stream=StreamProtocol(stream.put, stream_modes),
@@ -2865,16 +2861,12 @@ class Pregel(
if durability is not None or deprecated_checkpoint_during is not None:
config[CONF][CONFIG_KEY_DURABILITY] = durability_
runtime = Runtime(
config[CONF][CONFIG_KEY_RUNTIME] = Runtime(
context=context,
store=store,
stream_writer=stream_writer,
previous=None,
)
parent_runtime = config[CONF].get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME)
runtime = parent_runtime.merge(runtime)
config[CONF][CONFIG_KEY_RUNTIME] = runtime
async with AsyncPregelLoop(
input,
stream=StreamProtocol(stream.put_nowait, stream_modes),
-24
View File
@@ -3,7 +3,6 @@ from __future__ import annotations
from typing import Any, Optional
import pytest
from langchain_core.runnables.config import RunnableConfig
from langgraph._internal._runnable import RunnableCallable
from langgraph.runtime import Runtime
@@ -371,26 +370,3 @@ async def test_runnable_callable_injectable_arguments_async() -> None:
)
== "success"
)
def test_config_injection() -> None:
def func(x: Any, config: RunnableConfig) -> list[str]:
return config.get("tags", [])
assert RunnableCallable(func).invoke(
"test", config={"tags": ["test"], "configurable": {}}
) == ["test"]
def func_optional(x: Any, config: Optional[RunnableConfig]) -> list[str]:
return config.get("tags", []) if config else []
assert RunnableCallable(func_optional).invoke(
"test", config={"tags": ["test"], "configurable": {}}
) == ["test"]
def func_untyped(x: Any, config) -> list[str]:
return config.get("tags", [])
assert RunnableCallable(func_untyped).invoke(
"test", config={"tags": ["test"], "configurable": {}}
) == ["test"]
+9 -70
View File
@@ -7,14 +7,16 @@ from langgraph.graph import END, START, StateGraph
from langgraph.runtime import Runtime, get_runtime
@dataclass
class Context:
api_key: str
class State(TypedDict):
message: str
def test_injected_runtime() -> None:
@dataclass
class Context:
api_key: str
class State(TypedDict):
message: str
def injected_runtime(state: State, runtime: Runtime[Context]) -> dict[str, Any]:
return {"message": f"api key: {runtime.context.api_key}"}
@@ -30,13 +32,6 @@ def test_injected_runtime() -> None:
def test_context_runtime() -> None:
@dataclass
class Context:
api_key: str
class State(TypedDict):
message: str
def context_runtime(state: State) -> dict[str, Any]:
runtime = get_runtime(Context)
return {"message": f"api key: {runtime.context.api_key}"}
@@ -50,59 +45,3 @@ def test_context_runtime() -> None:
{"message": "hello world"}, context=Context(api_key="sk_123456")
)
assert result == {"message": "api key: sk_123456"}
def test_override_runtime() -> None:
@dataclass
class Context:
api_key: str
prev = Runtime(context=Context(api_key="abc"))
new = prev.override(context=Context(api_key="def"))
assert new.override(context=Context(api_key="def")).context.api_key == "def"
def test_merge_runtime() -> None:
@dataclass
class Context:
api_key: str
runtime1 = Runtime(context=Context(api_key="abc"))
runtime2 = Runtime(context=Context(api_key="def"))
runtime3 = Runtime(context=None)
assert runtime1.merge(runtime2).context.api_key == "def"
# override only applies to non-falsy values
assert runtime1.merge(runtime3).context.api_key == "abc" # type: ignore
def test_runtime_propogated_to_subgraph() -> None:
@dataclass
class Context:
username: str
class State(TypedDict, total=False):
subgraph: str
main: str
def subgraph_node_1(state: State, runtime: Runtime[Context]):
return {"subgraph": f"{runtime.context.username}!"}
subgraph_builder = StateGraph(State, context_schema=Context)
subgraph_builder.add_node(subgraph_node_1)
subgraph_builder.set_entry_point("subgraph_node_1")
subgraph = subgraph_builder.compile()
def main_node(state: State, runtime: Runtime[Context]):
return {"main": f"{runtime.context.username}!"}
builder = StateGraph(State, context_schema=Context)
builder.add_node(main_node)
builder.add_node("node_1", subgraph)
builder.set_entry_point("main_node")
builder.add_edge("main_node", "node_1")
graph = builder.compile()
context = Context(username="Alice")
result = graph.invoke({}, context=context)
assert result == {"subgraph": "Alice!", "main": "Alice!"}