mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-18 21:55:46 +02:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcd7332d88 | ||
|
|
1eb0c2cdb8 | ||
|
|
e777244c77 | ||
|
|
4a3854b09e | ||
|
|
191a2238db | ||
|
|
70097bb254 | ||
|
|
48b6e5cf7c | ||
|
|
e67f164ef6 | ||
|
|
89b565bc23 | ||
|
|
f18b880559 | ||
|
|
427e9e8061 | ||
|
|
09b10b5b3a | ||
|
|
c1343601d9 | ||
|
|
4496c86d28 | ||
|
|
39d6bdf236 | ||
|
|
6b59ab410d | ||
|
|
e46338af46 |
+119
-122
@@ -1,157 +1,154 @@
|
||||
"""Add typescript translation to a given markdown file."""
|
||||
"""Translate Python markdown to TypeScript and/or consolidate Python-JS markdown into a single document."""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
|
||||
import requests
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
|
||||
# Load reference TypeScript snippets
|
||||
URL = "https://gist.githubusercontent.com/eyurtsev/e7486731415463a9bc5b4682358859c8/raw/b5a5fda9c7e3387cfcb781f25082814d43675d50/gistfile1.txt"
|
||||
response = requests.get(URL)
|
||||
response.raise_for_status()
|
||||
reference_snippets = response.text
|
||||
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-latest")
|
||||
# Initialize model
|
||||
model = ChatAnthropic(model="claude-sonnet-4-0", max_tokens=64_000)
|
||||
|
||||
TRANSLATION_PROMPT = (
|
||||
"You are a helpful assistant that translates Python-based technical "
|
||||
"documentation written in Markdown to equivalent TypeScript-based documentation. "
|
||||
"The input is a Markdown file written in mkdocs format. It contains "
|
||||
"Python code snippets embedded in prose. "
|
||||
"Your task is to rewrite the content by translating the Python code to "
|
||||
"idiomatic TypeScript, using the provided TypeScript reference snippets "
|
||||
"to ensure accurate and consistent usage (e.g., correct imports, function "
|
||||
"names, and patterns). "
|
||||
"Remove the original Python code and replace it with the corresponding "
|
||||
"TypeScript version. "
|
||||
"Do not alter the surrounding prose unless a change is necessary to "
|
||||
"reflect differences between Python and TypeScript. "
|
||||
"Preserve the structure and formatting of the original Markdown document. "
|
||||
"Do not make stylistic or structural changes unless they directly support "
|
||||
"the translation. "
|
||||
"Use the reference TypeScript snippets as guidance whenever possible to "
|
||||
"maintain alignment with existing conventions.\n\n"
|
||||
f"Here are the reference TypeScript snippets:\n\n{reference_snippets}\n\n"
|
||||
)
|
||||
|
||||
CONSOLIDATION_PROMPT = (
|
||||
"You are a helpful assistant that consolidates parallel Python and JavaScript (TypeScript) technical documentation "
|
||||
"written in Markdown into a single unified Markdown document. "
|
||||
"The input consists of two documents: the first is for Python users, and the second is for JavaScript/TypeScript users. "
|
||||
"Your task is to merge these into one Markdown file using language-specific fenced blocks to separate the content where needed. "
|
||||
"Use the following syntax to distinguish content for each language:\n\n"
|
||||
":::python\n"
|
||||
"# Python-specific content\n"
|
||||
":::\n\n"
|
||||
":::js\n"
|
||||
"# JavaScript/TypeScript-specific content\n"
|
||||
":::\n\n"
|
||||
"Follow these consolidation rules:\n"
|
||||
"- When content (prose or code) is the same or nearly identical in both versions, include it only once—outside of any fenced block.\n"
|
||||
"- When content differs between the Python and JS versions, wrap each version in its corresponding fenced block.\n"
|
||||
"- Prefer **paragraph-level separation** of language-specific content. Do not combine Python and JS snippets or terminology in the same sentence or paragraph using conditional phrases.\n"
|
||||
" For example, avoid inline constructs like:\n"
|
||||
" `The :::python add_messages ::: :::js reducer ::: function...`\n"
|
||||
" Instead, write two distinct paragraphs:\n\n"
|
||||
" :::python\n"
|
||||
" The `add_messages` function in our `State` will append the LLM's response messages to whatever messages are already in the state.\n"
|
||||
" ::: \n\n"
|
||||
" :::js\n"
|
||||
" The `reducer` function in our `StateAnnotation` will append the LLM's response messages to whatever messages are already in the state.\n"
|
||||
" :::\n\n"
|
||||
"- Preserve the overall structure, ordering, and formatting of the original Markdown documents.\n"
|
||||
"- Do not rephrase or unify content unless it is logically and semantically identical.\n"
|
||||
"- Use the fenced blocks for both prose and code as needed, and ensure output is clean, readable Markdown suitable for tools that parse these directives.\n"
|
||||
"Your goal is to produce a cleanly merged documentation file that serves both Python and JavaScript users without redundancy, while maximizing clarity and separation of language-specific details."
|
||||
)
|
||||
|
||||
|
||||
def _get_tqdm():
|
||||
try:
|
||||
from tqdm import tqdm
|
||||
except ImportError:
|
||||
# If not available return a simple identity function
|
||||
def tqdm(iterable, *args, **kwargs):
|
||||
return iterable
|
||||
|
||||
return tqdm
|
||||
|
||||
|
||||
_tqdm = _get_tqdm()
|
||||
|
||||
opening_pattern = re.compile(r"^\s*```python(?:\s+.*)?\s*$")
|
||||
closing_pattern = re.compile(r"^\s*```\s*$")
|
||||
|
||||
|
||||
def extract_python_snippets(markdown: str) -> list[str]:
|
||||
"""
|
||||
Extract all python code blocks (including their fence lines) from the markdown content.
|
||||
A python block is defined as any block that starts with a line containing an opening fence
|
||||
with '```python' (optionally with extra parameters) and ends with a closing fence '```'.
|
||||
"""
|
||||
snippets = []
|
||||
inside_block = False
|
||||
current_snippet = []
|
||||
|
||||
for line in markdown.splitlines(keepends=True):
|
||||
if not inside_block:
|
||||
if opening_pattern.match(line):
|
||||
inside_block = True
|
||||
current_snippet = [line]
|
||||
else:
|
||||
current_snippet.append(line)
|
||||
if closing_pattern.match(line):
|
||||
inside_block = False
|
||||
snippets.append("".join(current_snippet))
|
||||
current_snippet = []
|
||||
return snippets
|
||||
|
||||
|
||||
def translate_snippet(python_snippet: str) -> str:
|
||||
"""Translate a python code block into a TypeScript code block using Langchain.
|
||||
The response is expected to be a properly fenced TypeScript code block (i.e.
|
||||
starting with ```typescript and ending with ```).
|
||||
"""
|
||||
ai_message = model.invoke(
|
||||
def translate_python_to_ts(markdown_content: str) -> str:
|
||||
response = model.invoke(
|
||||
[
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
f"You have access to the following up-to-date example TypeScript code "
|
||||
f"snippets that show examples of building with langgraph "
|
||||
f"and langchain:\n\n{reference_snippets}\n\n"
|
||||
"Use this context to translate the following Python code to equivalent "
|
||||
"TypeScript. Ensure that your output is a valid fenced TypeScript "
|
||||
"code block (i.e. starts with ```typescript and ends with ```)."
|
||||
),
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Translate this Python snippet to TypeScript:\n\n{python_snippet}",
|
||||
"content": TRANSLATION_PROMPT,
|
||||
"cache_control": {"type": "ephemeral"},
|
||||
},
|
||||
{"role": "user", "content": markdown_content},
|
||||
]
|
||||
)
|
||||
|
||||
# Use a regular expression to search for a TypeScript code block in the response.
|
||||
pattern = r"```typescript\s*(.*?)\s*```"
|
||||
match = re.search(pattern, ai_message.content, re.DOTALL)
|
||||
if match:
|
||||
# Reconstruct the code block with proper fences.
|
||||
typescript_code = match.group(1).strip()
|
||||
return f"```typescript\n{typescript_code}\n```"
|
||||
else:
|
||||
raise ValueError("No TypeScript code block found in the model's response.")
|
||||
return response.content
|
||||
|
||||
|
||||
def insert_translations_into_markdown(
|
||||
markdown: str, typescript_snippets: list[str]
|
||||
) -> str:
|
||||
"""Walks through the original markdown content and, after each
|
||||
Python snippet block, inserts the corresponding translated TypeScript snippet.
|
||||
It assumes that the ordering of the Python snippets
|
||||
(from extract_python_snippets) matches the order they appear in the markdown.
|
||||
"""
|
||||
output_lines = []
|
||||
lines = markdown.splitlines(keepends=True)
|
||||
inside_block = False
|
||||
snippet_index = 0
|
||||
|
||||
for line in lines:
|
||||
output_lines.append(line)
|
||||
if not inside_block and opening_pattern.match(line):
|
||||
# We've encountered the start of a python code block.
|
||||
inside_block = True
|
||||
elif inside_block:
|
||||
if closing_pattern.match(line):
|
||||
# End of a python snippet block.
|
||||
inside_block = False
|
||||
if snippet_index < len(typescript_snippets):
|
||||
# Insert an extra newline for clarity, then the translated TypeScript snippet.
|
||||
output_lines.append("\n")
|
||||
output_lines.append(typescript_snippets[snippet_index])
|
||||
output_lines.append("\n")
|
||||
snippet_index += 1
|
||||
return "".join(output_lines)
|
||||
def consolidate_python_and_ts(combined_content: str) -> str:
|
||||
response = model.invoke(
|
||||
[
|
||||
{
|
||||
"role": "system",
|
||||
"content": CONSOLIDATION_PROMPT,
|
||||
"cache_control": {"type": "ephemeral"},
|
||||
},
|
||||
{"role": "user", "content": combined_content},
|
||||
]
|
||||
)
|
||||
return response.content
|
||||
|
||||
|
||||
def main(file_path: str) -> None:
|
||||
# Read the markdown file.
|
||||
with open(file_path, "r") as f:
|
||||
def main(file_path: str, translate_only: bool, consolidate_only: bool) -> None:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
markdown_content = f.read()
|
||||
|
||||
# 1. Extract all Python snippets.
|
||||
python_snippets = extract_python_snippets(markdown_content)[:1]
|
||||
if translate_only:
|
||||
translated = translate_python_to_ts(markdown_content)
|
||||
output_path = file_path.replace(".md", ".translated.md")
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write(translated)
|
||||
print(f"Translated JS/TS version written to: {output_path}")
|
||||
|
||||
# 2. Translate each Python snippet to TypeScript.
|
||||
typescript_snippets = []
|
||||
# Replace with .batch() for faster translation
|
||||
for python_snippet in _tqdm(python_snippets):
|
||||
ts_snippet = translate_snippet(python_snippet)
|
||||
typescript_snippets.append(ts_snippet)
|
||||
elif consolidate_only:
|
||||
consolidated = consolidate_python_and_ts(markdown_content)
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(consolidated)
|
||||
print(f"Consolidated content written to: {file_path}")
|
||||
|
||||
# 3. Insert the TypeScript translations after their respective Python snippets.
|
||||
updated_markdown = insert_translations_into_markdown(
|
||||
markdown_content, typescript_snippets
|
||||
)
|
||||
|
||||
# Overwrite the original markdown file with the updated content.
|
||||
with open(file_path, "w") as f:
|
||||
f.write(updated_markdown)
|
||||
else:
|
||||
# Default behavior: translate first, then consolidate both
|
||||
translated = translate_python_to_ts(markdown_content)
|
||||
combined = f"{markdown_content.strip()}\n\n\n{translated.strip()}"
|
||||
consolidated = consolidate_python_and_ts(combined)
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(consolidated)
|
||||
print(f"Translated and consolidated content written to: {file_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Translate Python snippets in a markdown file to TypeScript and insert them after each Python snippet."
|
||||
description=(
|
||||
"Translate Python markdown to TypeScript and/or consolidate "
|
||||
"Python-JS markdown into one file."
|
||||
)
|
||||
)
|
||||
parser.add_argument("file_path", type=str, help="Path to the markdown file.")
|
||||
parser.add_argument(
|
||||
"--translate-only",
|
||||
action="store_true",
|
||||
help="Only generate the JS translation.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--consolidate-only",
|
||||
action="store_true",
|
||||
help="Only consolidate pre-paired Python and JS content.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args.file_path)
|
||||
if args.translate_only and args.consolidate_only:
|
||||
raise ValueError(
|
||||
"Cannot use both --translate-only and --consolidate-only at the same time."
|
||||
)
|
||||
|
||||
main(
|
||||
args.file_path,
|
||||
translate_only=args.translate_only,
|
||||
consolidate_only=args.consolidate_only,
|
||||
)
|
||||
|
||||
@@ -3,19 +3,21 @@
|
||||
import asyncio
|
||||
import glob
|
||||
import os
|
||||
from typing import TypedDict, List, Optional
|
||||
import pydantic
|
||||
import re
|
||||
from pydantic import BaseModel, Field
|
||||
from langchain_core.rate_limiters import InMemoryRateLimiter
|
||||
from typing import TypedDict, List, Optional
|
||||
|
||||
import yaml
|
||||
from langchain.chat_models import init_chat_model
|
||||
from langchain_core.rate_limiters import InMemoryRateLimiter
|
||||
from mkdocs.structure.files import File
|
||||
from mkdocs.structure.pages import Page
|
||||
from pydantic import BaseModel, Field
|
||||
from yaml import SafeLoader
|
||||
|
||||
from _scripts.notebook_hooks import _on_page_markdown_with_config
|
||||
from _scripts.notebook_hooks import (
|
||||
_on_page_markdown_with_config,
|
||||
_apply_conditional_rendering,
|
||||
)
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
# Get source directory (parent of HERE / docs)
|
||||
@@ -211,7 +213,9 @@ async def process_nav_items(nav_items: list[NavItem]) -> list[NavItem]:
|
||||
# Remove any items that start with http:// or https:// looking only for
|
||||
# local file at this stages.
|
||||
nav_items = [
|
||||
item for item in nav_items if not item["url"].startswith(("http://", "https://"))
|
||||
item
|
||||
for item in nav_items
|
||||
if not item["url"].startswith(("http://", "https://"))
|
||||
]
|
||||
# Process items in parallel
|
||||
tasks = [process_single_item(item) for item in nav_items]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
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",
|
||||
|
||||
}
|
||||
@@ -16,6 +16,7 @@ from mkdocs.structure.pages import Page
|
||||
|
||||
from _scripts.generate_api_reference_links import update_markdown_with_imports
|
||||
from _scripts.notebook_convert import convert_notebook
|
||||
from _scripts.link_map import JS_LINK_MAP
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig()
|
||||
@@ -158,6 +159,62 @@ def _add_path_to_code_blocks(markdown: str, page: Page) -> str:
|
||||
return code_block_pattern.sub(replace_code_block_header, markdown)
|
||||
|
||||
|
||||
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:
|
||||
if target_language not in {"python", "js"}:
|
||||
raise ValueError("target_language must be 'python' or 'js'")
|
||||
|
||||
pattern = re.compile(
|
||||
r"(?P<indent>[ \t]*):::(?P<language>\w+)\s*\n"
|
||||
r"(?P<content>((?:.*\n)*?))" # Capture the content inside the block
|
||||
r"(?P=indent):::" # Match closing with the same indentation
|
||||
)
|
||||
|
||||
def replace_conditional_blocks(match: re.Match) -> str:
|
||||
"""Keep active conditionals."""
|
||||
language = match.group("language")
|
||||
content = match.group("content")
|
||||
|
||||
if language not in {"python", "js"}:
|
||||
# If the language is not supported, return the original block
|
||||
return match.group(0)
|
||||
|
||||
if language == target_language:
|
||||
return content
|
||||
|
||||
# If the language does not match, return an empty string
|
||||
return ""
|
||||
|
||||
processed = pattern.sub(replace_conditional_blocks, md_text)
|
||||
return processed
|
||||
|
||||
|
||||
def _highlight_code_blocks(markdown: str) -> str:
|
||||
"""Find code blocks with highlight comments and add hl_lines attribute.
|
||||
|
||||
@@ -257,6 +314,20 @@ def _on_page_markdown_with_config(
|
||||
# Apply highlight comments to code blocks
|
||||
markdown = _highlight_code_blocks(markdown)
|
||||
|
||||
# Apply conditional rendering for code blocks
|
||||
target_language = kwargs.get("target_language", "js")
|
||||
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
|
||||
# which can be used in CI to test the docs without making network requests.
|
||||
|
||||
@@ -11,6 +11,8 @@ hide:
|
||||
|
||||
This guide shows you how to set up and use LangGraph's **prebuilt**, **reusable** components, which are designed to help you construct agentic systems quickly and reliably.
|
||||
|
||||
:::python
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you start this tutorial, ensure you have the following:
|
||||
@@ -228,3 +230,244 @@ response["structured_response"]
|
||||
- [Deploy your agent locally](../tutorials/langgraph-platform/local-server.md)
|
||||
- [Learn more about prebuilt agents](../agents/overview.md)
|
||||
- [LangGraph Platform quickstart](../cloud/quick_start.md)
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you start this tutorial, ensure you have the following:
|
||||
|
||||
- An [Anthropic](https://console.anthropic.com/settings/keys) API key
|
||||
|
||||
## 1. Install dependencies
|
||||
|
||||
If you haven't already, install LangGraph and LangChain:
|
||||
|
||||
```
|
||||
npm install langchain @langchain/langgraph @langchain/anthropic
|
||||
```
|
||||
|
||||
## 2. Create an agent
|
||||
|
||||
Use [`createReactAgent`][create_react_agent] to instantiate an agent:
|
||||
|
||||
```ts
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
const getWeather = tool( // (1)!
|
||||
async (input: { city: string }) => {
|
||||
return `It's always sunny in ${input.city}!`;
|
||||
},
|
||||
{
|
||||
name: "getWeather",
|
||||
schema: z.object({
|
||||
city: z.string().describe("The city to get the weather for"),
|
||||
}),
|
||||
description: "Get weather for a given city.",
|
||||
}
|
||||
);
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest"); // (2)!
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getWeather], // (3)!
|
||||
prompt: "You are a helpful assistant", // (4)!
|
||||
});
|
||||
|
||||
// Run the agent
|
||||
await agent.invoke({
|
||||
messages: [{ role: "user", content: "what is the weather in sf" }],
|
||||
});
|
||||
```
|
||||
|
||||
1. Define a tool for the agent to use. For more advanced tool usage and customization, check the [tools](./tools.md) page.
|
||||
2. Provide a language model for the agent to use. To learn more about configuring language models for the agents, check the [models](./models.md) page.
|
||||
3. Provide a list of tools for the model to use.
|
||||
4. Provide a system prompt (instructions) to the language model used by the agent.
|
||||
|
||||
## 3. Configure an LLM
|
||||
|
||||
Use [`initChatModel`](https://api.js.langchain.com/functions/langchain.chat_models_universal.initChatModel.html) to configure an LLM with specific parameters, such as temperature:
|
||||
|
||||
```ts
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
|
||||
// highlight-next-line
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest", {
|
||||
// highlight-next-line
|
||||
temperature: 0,
|
||||
});
|
||||
|
||||
const agent = createReactAgent({
|
||||
// highlight-next-line
|
||||
llm,
|
||||
tools: [getWeather],
|
||||
});
|
||||
```
|
||||
|
||||
See the [models](./models.md) page for more information on how to configure LLMs.
|
||||
|
||||
## 4. Add a custom prompt
|
||||
|
||||
Prompts instruct the LLM how to behave. They can be:
|
||||
|
||||
- **Static**: A string is interpreted as a **system message**
|
||||
- **Dynamic**: a list of messages generated at **runtime** based on input or configuration
|
||||
|
||||
=== "Static prompt"
|
||||
|
||||
Define a fixed prompt string or list of messages.
|
||||
|
||||
```ts
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getWeather],
|
||||
// A static prompt that never changes
|
||||
// highlight-next-line
|
||||
prompt: "Never answer questions about the weather.",
|
||||
});
|
||||
|
||||
await agent.invoke({
|
||||
messages: "what is the weather in sf",
|
||||
});
|
||||
```
|
||||
|
||||
=== "Dynamic prompt"
|
||||
|
||||
Define a function that returns a message list based on the agent's state and configuration:
|
||||
|
||||
```ts
|
||||
import { BaseMessageLike } from "@langchain/core/messages";
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { MessagesAnnotation } from "@langchain/langgraph";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
const prompt = (
|
||||
state: typeof MessagesAnnotation.State,
|
||||
config: RunnableConfig
|
||||
): BaseMessageLike[] => { // (1)!
|
||||
const userName = config.configurable?.userName;
|
||||
const systemMsg = `You are a helpful assistant. Address the user as ${userName}.`;
|
||||
return [{ role: "system", content: systemMsg }, ...state.messages];
|
||||
};
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getWeather],
|
||||
// highlight-next-line
|
||||
prompt,
|
||||
});
|
||||
|
||||
await agent.invoke(
|
||||
{ messages: [{ role: "user", content: "what is the weather in sf" }] },
|
||||
// highlight-next-line
|
||||
{ configurable: { userName: "John Smith" } }
|
||||
);
|
||||
```
|
||||
|
||||
1. Dynamic prompts allow including non-message [context](./context.md) when constructing an input to the LLM, such as:
|
||||
|
||||
- Information passed at runtime, like a `userId` or API credentials (using `config`).
|
||||
- Internal agent state updated during a multi-step reasoning process (using `state`).
|
||||
|
||||
Dynamic prompts can be defined as functions that take `state` and `config` and return a list of messages to send to the LLM.
|
||||
|
||||
For more information, see [Context](./context.md).
|
||||
|
||||
## 5. Add memory
|
||||
|
||||
To allow multi-turn conversations with an agent, you need to enable [persistence](../concepts/persistence.md) by providing a `checkpointer` when creating an agent. At runtime you need to provide a config containing `thread_id` — a unique identifier for the conversation (session):
|
||||
|
||||
```ts
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { MemorySaver } from "@langchain/langgraph-checkpoint";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
|
||||
// highlight-next-line
|
||||
const checkpointer = new MemorySaver();
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getWeather],
|
||||
// highlight-next-line
|
||||
checkpointer, // (1)!
|
||||
});
|
||||
|
||||
// Run the agent
|
||||
// highlight-next-line
|
||||
const config = { configurable: { thread_id: "1" } };
|
||||
const sfResponse = await agent.invoke(
|
||||
{ messages: [{ role: "user", content: "what is the weather in sf" }] },
|
||||
config // (2)!
|
||||
);
|
||||
const nyResponse = await agent.invoke(
|
||||
{ messages: [{ role: "user", content: "what about new york?" }] },
|
||||
config
|
||||
);
|
||||
```
|
||||
|
||||
1. `checkpointer` allows the agent to store its state at every step in the tool calling loop. This enables [short-term memory](./memory.md#short-term-memory) and [human-in-the-loop](./human-in-the-loop.md) capabilities.
|
||||
2. Pass configuration with `thread_id` to be able to resume the same conversation on future agent invocations.
|
||||
|
||||
When you enable the checkpointer, it stores agent state at every step in the provided checkpointer database (or in memory, if using `InMemorySaver`).
|
||||
|
||||
Note that in the above example, when the agent is invoked the second time with the same `thread_id`, the original message history from the first conversation is automatically included, together with the new user input.
|
||||
|
||||
For more information, see [Memory](./memory.md).
|
||||
|
||||
## 6. Configure structured output
|
||||
|
||||
To produce structured responses conforming to a schema, use the `responseFormat` parameter. The schema can be defined with a `zod` schema. The result will be accessible via the `structuredResponse` field.
|
||||
|
||||
```ts
|
||||
import { z } from "zod";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
|
||||
const WeatherResponse = z.object({
|
||||
conditions: z.string(),
|
||||
});
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getWeather],
|
||||
// highlight-next-line
|
||||
responseFormat: WeatherResponse, // (1)!
|
||||
});
|
||||
|
||||
const response = await agent.invoke({
|
||||
messages: [{ role: "user", content: "what is the weather in sf" }],
|
||||
});
|
||||
// highlight-next-line
|
||||
response.structuredResponse;
|
||||
```
|
||||
|
||||
1. When `responseFormat` is provided, a separate step is added at the end of the agent loop: agent message history is passed to an LLM with structured output to generate a structured response.
|
||||
|
||||
To provide a system prompt to this LLM, use an object `{ prompt, schema }`, e.g., `responseFormat: { prompt, schema: WeatherResponse }`.
|
||||
|
||||
!!! Note "LLM post-processing"
|
||||
|
||||
Structured output requires an additional call to the LLM to format the response according to the schema.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Deploy your agent locally](../tutorials/langgraph-platform/local-server.md)
|
||||
- [Learn more about prebuilt agents](../agents/overview.md)
|
||||
- [LangGraph Platform quickstart](../cloud/quick_start.md)
|
||||
|
||||
:::
|
||||
+282
-1
@@ -43,6 +43,8 @@ when you have values that don't change mid-run.
|
||||
Specify configuration using a key called **"configurable"** which is reserved
|
||||
for this purpose:
|
||||
|
||||
|
||||
:::python
|
||||
```python
|
||||
agent.invoke(
|
||||
{"messages": [{"role": "user", "content": "hi!"}]},
|
||||
@@ -50,11 +52,23 @@ agent.invoke(
|
||||
config={"configurable": {"user_id": "user_123"}}
|
||||
)
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```ts
|
||||
await agent.invoke(
|
||||
{ messages: "hi!" },
|
||||
// highlight-next-line
|
||||
{ configurable: { userId: "user_123" } }
|
||||
)
|
||||
```
|
||||
:::
|
||||
|
||||
### State (mutable context)
|
||||
|
||||
State acts as short-term memory during a run. It holds dynamic data that can evolve during execution, such as values derived from tools or LLM outputs.
|
||||
|
||||
:::python
|
||||
```python
|
||||
class CustomState(AgentState):
|
||||
# highlight-next-line
|
||||
@@ -71,6 +85,29 @@ agent.invoke({
|
||||
"user_name": "Jane"
|
||||
})
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```ts
|
||||
const CustomState = Annotation.Root({
|
||||
...MessagesAnnotation.spec,
|
||||
userName: Annotation<string>,
|
||||
});
|
||||
|
||||
const agent = createReactAgent({
|
||||
// Other agent parameters...
|
||||
// highlight-next-line
|
||||
stateSchema: CustomState,
|
||||
})
|
||||
|
||||
await agent.invoke(
|
||||
// highlight-next-line
|
||||
{ messages: "hi!", userName: "Jane" }
|
||||
)
|
||||
```
|
||||
:::
|
||||
|
||||
|
||||
|
||||
!!! tip "Turning on memory"
|
||||
|
||||
@@ -93,6 +130,8 @@ Common use cases:
|
||||
- Role or goal customization
|
||||
- Conditional behavior (e.g., user is admin)
|
||||
|
||||
:::python
|
||||
|
||||
=== "Using config"
|
||||
|
||||
```python
|
||||
@@ -162,8 +201,90 @@ Common use cases:
|
||||
})
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
=== "Using config"
|
||||
|
||||
```ts
|
||||
import { BaseMessageLike } from "@langchain/core/messages";
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { MessagesAnnotation } from "@langchain/langgraph";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
const prompt = (
|
||||
state: typeof MessagesAnnotation.State,
|
||||
// highlight-next-line
|
||||
config: RunnableConfig
|
||||
): BaseMessageLike[] => {
|
||||
// highlight-next-line
|
||||
const userName = config.configurable?.userName;
|
||||
const systemMsg = `You are a helpful assistant. Address the user as ${userName}.`;
|
||||
return [{ role: "system", content: systemMsg }, ...state.messages];
|
||||
};
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getWeather],
|
||||
// highlight-next-line
|
||||
prompt
|
||||
});
|
||||
|
||||
await agent.invoke(
|
||||
{ messages: "hi!" },
|
||||
// highlight-next-line
|
||||
{ configurable: { userName: "John Smith" } }
|
||||
);
|
||||
```
|
||||
|
||||
=== "Using state"
|
||||
|
||||
```ts
|
||||
import { BaseMessageLike } from "@langchain/core/messages";
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { Annotation, MessagesAnnotation } from "@langchain/langgraph";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
const CustomState = Annotation.Root({
|
||||
...MessagesAnnotation.spec,
|
||||
// highlight-next-line
|
||||
userName: Annotation<string>,
|
||||
});
|
||||
|
||||
const prompt = (
|
||||
// highlight-next-line
|
||||
state: typeof CustomState.State,
|
||||
): BaseMessageLike[] => {
|
||||
// highlight-next-line
|
||||
const userName = state.userName;
|
||||
const systemMsg = `You are a helpful assistant. Address the user as ${userName}.`;
|
||||
return [{ role: "system", content: systemMsg }, ...state.messages];
|
||||
};
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getWeather],
|
||||
// highlight-next-line
|
||||
prompt,
|
||||
// highlight-next-line
|
||||
stateSchema: CustomState,
|
||||
});
|
||||
|
||||
await agent.invoke(
|
||||
// highlight-next-line
|
||||
{ messages: "hi!", userName: "John Smith" },
|
||||
);
|
||||
```
|
||||
:::
|
||||
|
||||
|
||||
## Accessing Context in Tools { #tools }
|
||||
|
||||
:::python
|
||||
Tools can access context through special parameter **annotations**.
|
||||
|
||||
* Use `RunnableConfig` for config access
|
||||
@@ -230,7 +351,167 @@ Tools can access context through special parameter **annotations**.
|
||||
"user_id": "user_123"
|
||||
})
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
Tools can access context through:
|
||||
|
||||
* Use `RunnableConfig` for config access
|
||||
* Use `getCurrentTaskInput()` for agent state
|
||||
|
||||
=== "Using config"
|
||||
|
||||
```ts
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
const getUserInfo = tool(
|
||||
async (input: Record<string, any>, config: RunnableConfig) => {
|
||||
// highlight-next-line
|
||||
const userId = config.configurable?.userId;
|
||||
return userId === "user_123" ? "User is John Smith" : "Unknown user";
|
||||
},
|
||||
{
|
||||
name: "get_user_info",
|
||||
description: "Look up user info.",
|
||||
schema: z.object({}),
|
||||
}
|
||||
);
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getUserInfo],
|
||||
});
|
||||
|
||||
await agent.invoke(
|
||||
{ messages: "look up user information" },
|
||||
// highlight-next-line
|
||||
{ configurable: { userId: "user_123" } }
|
||||
);
|
||||
```
|
||||
|
||||
=== "Using state"
|
||||
|
||||
```ts
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { Annotation, MessagesAnnotation, getCurrentTaskInput } from "@langchain/langgraph";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
const CustomState = Annotation.Root({
|
||||
...MessagesAnnotation.spec,
|
||||
// highlight-next-line
|
||||
userId: Annotation<string>(),
|
||||
});
|
||||
|
||||
const getUserInfo = tool(
|
||||
async (
|
||||
input: Record<string, any>,
|
||||
) => {
|
||||
// highlight-next-line
|
||||
const state = getCurrentTaskInput() as typeof CustomState.State;
|
||||
// highlight-next-line
|
||||
const userId = state.userId;
|
||||
return userId === "user_123" ? "User is John Smith" : "Unknown user";
|
||||
},
|
||||
{
|
||||
name: "get_user_info",
|
||||
description: "Look up user info.",
|
||||
schema: z.object({})
|
||||
}
|
||||
);
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getUserInfo],
|
||||
// highlight-next-line
|
||||
stateSchema: CustomState,
|
||||
});
|
||||
|
||||
await agent.invoke(
|
||||
// highlight-next-line
|
||||
{ messages: "look up user information", userId: "user_123" }
|
||||
);
|
||||
```
|
||||
:::
|
||||
|
||||
### Update Context from Tools
|
||||
|
||||
Tools can update agent's context (state and long-term memory) during execution. This is useful for persisting intermediate results or making information accessible to subsequent tools or prompts. See [Memory](./memory.md#read-short-term) guide for more information.
|
||||
:::python
|
||||
Tools can update agent's context (state and long-term memory) during execution. This is useful for persisting intermediate results or making information accessible to subsequent tools or prompts. See [Memory](./memory.md#read-short-term) guide for more information.
|
||||
:::
|
||||
|
||||
:::js
|
||||
Tools can modify the agent's state during execution. This is useful for persisting intermediate results or making information accessible to subsequent tools or prompts.
|
||||
|
||||
```ts
|
||||
import { Annotation, MessagesAnnotation, LangGraphRunnableConfig, Command } from "@langchain/langgraph";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
import { ToolMessage } from "@langchain/core/messages";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
const CustomState = Annotation.Root({
|
||||
...MessagesAnnotation.spec,
|
||||
// highlight-next-line
|
||||
userName: Annotation<string>(), // Will be updated by the tool
|
||||
});
|
||||
|
||||
const getUserInfo = tool(
|
||||
async (
|
||||
_input: Record<string, never>,
|
||||
config: LangGraphRunnableConfig
|
||||
): Promise<Command> => {
|
||||
const userId = config.configurable?.userId;
|
||||
if (!userId) {
|
||||
throw new Error("Please provide a user id in config.configurable");
|
||||
}
|
||||
|
||||
const toolCallId = config.toolCall?.id;
|
||||
|
||||
const name = userId === "user_123" ? "John Smith" : "Unknown user";
|
||||
// Return command to update state
|
||||
return new Command({
|
||||
update: {
|
||||
// highlight-next-line
|
||||
userName: name,
|
||||
// Update the message history
|
||||
// highlight-next-line
|
||||
messages: [
|
||||
new ToolMessage({
|
||||
content: "Successfully looked up user information",
|
||||
tool_call_id: toolCallId,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
name: "get_user_info",
|
||||
description: "Look up user information.",
|
||||
schema: z.object({}),
|
||||
}
|
||||
);
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getUserInfo],
|
||||
// highlight-next-line
|
||||
stateSchema: CustomState,
|
||||
});
|
||||
|
||||
await agent.invoke(
|
||||
{ messages: "look up user information" },
|
||||
// highlight-next-line
|
||||
{ configurable: { userId: "user_123" } }
|
||||
);
|
||||
```
|
||||
:::
|
||||
@@ -27,6 +27,8 @@ A human can review and edit the output from the agent before proceeding. This is
|
||||
</figure>
|
||||
|
||||
|
||||
:::python
|
||||
|
||||
## Review tool calls
|
||||
|
||||
To add a human approval step to a tool:
|
||||
@@ -34,6 +36,7 @@ To add a human approval step to a tool:
|
||||
1. Use `interrupt()` in the tool to pause execution.
|
||||
2. Resume with a `Command(resume=...)` to continue based on human input.
|
||||
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.types import interrupt
|
||||
@@ -233,6 +236,110 @@ for chunk in agent.stream(
|
||||
print("\n")
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
## Review tool calls
|
||||
|
||||
To add a human approval step to a tool:
|
||||
|
||||
1. Use `interrupt()` in the tool to pause execution.
|
||||
2. Resume with a `Command({ resume: ... })` to continue based on human input.
|
||||
|
||||
```ts
|
||||
import { MemorySaver } from "@langchain/langgraph-checkpoint";
|
||||
import { interrupt } from "@langchain/langgraph";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
// An example of a sensitive tool that requires human review / approval
|
||||
const bookHotel = tool(
|
||||
async (input: { hotelName: string; }) => {
|
||||
let hotelName = input.hotelName;
|
||||
// highlight-next-line
|
||||
const response = interrupt( // (1)!
|
||||
`Trying to call \`book_hotel\` with args {'hotel_name': ${hotelName}}. ` +
|
||||
`Please approve or suggest edits.`
|
||||
)
|
||||
if (response.type === "accept") {
|
||||
// proceed to execute the tool logic
|
||||
} else if (response.type === "edit") {
|
||||
hotelName = response.args["hotel_name"]
|
||||
} else {
|
||||
throw new Error(`Unknown response type: ${response.type}`)
|
||||
}
|
||||
return `Successfully booked a stay at ${hotelName}.`;
|
||||
},
|
||||
{
|
||||
name: "bookHotel",
|
||||
schema: z.object({
|
||||
hotelName: z.string().describe("Hotel to book"),
|
||||
}),
|
||||
description: "Book a hotel.",
|
||||
}
|
||||
);
|
||||
|
||||
// highlight-next-line
|
||||
const checkpointer = new MemorySaver(); // (2)!
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [bookHotel],
|
||||
// highlight-next-line
|
||||
checkpointer // (3)!
|
||||
});
|
||||
```
|
||||
|
||||
1. The [`interrupt` function][langgraph.types.interrupt] pauses the agent graph at a specific node. In this case, we call `interrupt()` at the beginning of the tool function, which pauses the graph at the node that executes the tool. The information inside `interrupt()` (e.g., tool calls) can be presented to a human, and the graph can be resumed with the user input (tool call approval, edit or feedback).
|
||||
2. The `InMemorySaver` is used to store the agent state at every step in the tool calling loop. This enables [short-term memory](./memory.md#short-term-memory) and [human-in-the-loop](./human-in-the-loop.md) capabilities. In this example, we use `InMemorySaver` to store the agent state in memory. In a production application, the agent state will be stored in a database.
|
||||
3. Initialize the agent with the `checkpointer`.
|
||||
|
||||
Run the agent with the `stream()` method, passing the `config` object to specify the thread ID. This allows the agent to resume the same conversation on future invocations.
|
||||
|
||||
```ts
|
||||
const config = {
|
||||
configurable: {
|
||||
// highlight-next-line
|
||||
"thread_id": "1"
|
||||
}
|
||||
}
|
||||
|
||||
for await (const chunk of await agent.stream(
|
||||
{ messages: "book a stay at McKittrick hotel" },
|
||||
// highlight-next-line
|
||||
config
|
||||
)) {
|
||||
console.log(chunk);
|
||||
console.log("\n");
|
||||
};
|
||||
```
|
||||
|
||||
> You should see that the agent runs until it reaches the `interrupt()` call, at which point it pauses and waits for human input.
|
||||
|
||||
Resume the agent with a `Command({ resume: ... })` to continue based on human input.
|
||||
|
||||
```ts
|
||||
import { Command } from "@langchain/langgraph";
|
||||
|
||||
for await (const chunk of await agent.stream(
|
||||
new Command({ resume: { type: "accept" } }), // (1)!
|
||||
// new Command({ resume: { type: "edit", args: { "hotel_name": "McKittrick Hotel" } } }),
|
||||
// highlight-next-line
|
||||
config
|
||||
)) {
|
||||
console.log(chunk);
|
||||
console.log("\n");
|
||||
};
|
||||
```
|
||||
|
||||
1. The [`interrupt` function][langgraph.types.interrupt] is used in conjunction with the [`Command`][langgraph.types.Command] object to resume the graph with a value provided by the human.
|
||||
|
||||
:::
|
||||
|
||||
## Additional resources
|
||||
|
||||
* [Human-in-the-loop in LangGraph](../concepts/human_in_the_loop.md)
|
||||
|
||||
@@ -13,6 +13,8 @@ hide:
|
||||
|
||||

|
||||
|
||||
:::python
|
||||
|
||||
Install the `langchain-mcp-adapters` library to use MCP tools in LangGraph:
|
||||
|
||||
```bash
|
||||
@@ -58,6 +60,57 @@ weather_response = await agent.ainvoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in nyc?"}]}
|
||||
)
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
Install the `@langchain/mcp-adapters` library to use MCP tools in LangGraph:
|
||||
```bash
|
||||
npm install @langchain/mcp-adapters
|
||||
```
|
||||
|
||||
## Use MCP tools
|
||||
|
||||
The `@langchain/mcp-adapters` package enables agents to use tools defined across one or more MCP servers.
|
||||
|
||||
```ts
|
||||
// highlight-next-line
|
||||
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
// highlight-next-line
|
||||
const client = new MultiServerMCPClient({
|
||||
mcpServers: {
|
||||
"math": {
|
||||
command: "python",
|
||||
// Replace with absolute path to your math_server.py file
|
||||
args: ["/path/to/math_server.py"],
|
||||
transport: "stdio",
|
||||
},
|
||||
"weather": {
|
||||
// Ensure your start your weather server on port 8000
|
||||
url: "http://localhost:8000/sse",
|
||||
transport: "sse",
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
// highlight-next-line
|
||||
tools: await client.getTools()
|
||||
});
|
||||
|
||||
const mathResponse = await agent.invoke(
|
||||
{ messages: [ { role: "user", content: "what's (3 + 5) x 12?" } ] }
|
||||
);
|
||||
const weatherResponse = await agent.invoke(
|
||||
{ messages: [ { role: "user", content: "what is the weather in nyc?" } ] }
|
||||
);
|
||||
await client.close();
|
||||
```
|
||||
:::
|
||||
|
||||
## Custom MCP servers
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ LangGraph comes with a set of prebuilt components that implement common agent be
|
||||
|
||||
Using LangGraph for agent development allows you to focus on your application's logic and behavior, instead of building and maintaining the supporting infrastructure for state, memory, and human feedback.
|
||||
|
||||
|
||||
:::python
|
||||
## Package ecosystem
|
||||
|
||||
The high-level components are organized into several packages, each with a specific focus.
|
||||
@@ -189,3 +191,161 @@ function initializeWidget() {
|
||||
window.addEventListener("DOMContentLoaded", initializeWidget);
|
||||
document$.subscribe(initializeWidget);
|
||||
</script>
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
## Package ecosystem
|
||||
|
||||
The high-level components are organized into several packages, each with a specific focus.
|
||||
|
||||
| Package | Description | Installation |
|
||||
|--------------------------|-----------------------------------------------------------------------------|----------------------------------------------------|
|
||||
| `langgraph` | Prebuilt components to [**create agents**](./agents.md) | `npm install @langchain/langgraph @langchain/core` |
|
||||
| `langgraph-supervisor` | Tools for building [**supervisor**](./multi-agent.md#supervisor) agents | `npm install @langchain/langgraph-supervisor` |
|
||||
| `langgraph-swarm` | Tools for building a [**swarm**](./multi-agent.md#swarm) multi-agent system | `npm install @langchain/langgraph-swarm` |
|
||||
| `langchain-mcp-adapters` | Interfaces to [**MCP servers**](./mcp.md) for tool and resource integration | `npm install @langchain/mcp-adapters` |
|
||||
| `agentevals` | Utilities to [**evaluate agent performance**](./evals.md) | `npm install agentevals` |
|
||||
|
||||
## Visualize an agent graph
|
||||
|
||||
Use the following tool to visualize the graph generated by [`createReactAgent`][create_react_agent] and to view an outline of the corresponding code. It allows you to explore the infrastructure of the agent as defined by the presence of:
|
||||
|
||||
- [`tools`](./tools.md): A list of tools (functions, APIs, or other callable objects) that the agent can use to perform tasks.
|
||||
- `preModelHook`: A function that is called before the model is invoked. It can be used to condense messages or perform other preprocessing tasks.
|
||||
- `postModelHook`: A function that is called after the model is invoked. It can be used to implement guardrails, human-in-the-loop flows, or other postprocessing tasks.
|
||||
- [`responseFormat`](./agents.md#6-configure-structured-output): A data structure used to constrain the type of the final output (via Zod schemas).
|
||||
|
||||
<div class="agent-layout">
|
||||
<div class="agent-graph-features-container">
|
||||
<div class="agent-graph-features">
|
||||
<h3 class="agent-section-title">Features</h3>
|
||||
<label><input type="checkbox" id="tools" checked> <code>tools</code></label>
|
||||
<label><input type="checkbox" id="preModelHook"> <code>preModelHook</code></label>
|
||||
<label><input type="checkbox" id="postModelHook"> <code>postModelHook</code></label>
|
||||
<label><input type="checkbox" id="responseFormat"> <code>responseFormat</code></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="agent-graph-container">
|
||||
<h3 class="agent-section-title">Graph</h3>
|
||||
<img id="agent-graph-img" src="../assets/react_agent_graphs/0001.svg" alt="graph image" style="max-width: 100%;"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
The following code snippet shows how to create the above agent (and underlying graph) with [`createReactAgent`][create_react_agent]:
|
||||
|
||||
```typescript
|
||||
|
||||
<div class="language-typescript">
|
||||
<pre><code id="agent-code" class="language-typescript"></code></pre>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function getCheckedValue(id) {
|
||||
return document.getElementById(id).checked ? "1" : "0";
|
||||
}
|
||||
|
||||
function getKey() {
|
||||
return [
|
||||
getCheckedValue("responseFormat"),
|
||||
getCheckedValue("postModelHook"),
|
||||
getCheckedValue("preModelHook"),
|
||||
getCheckedValue("tools")
|
||||
].join("");
|
||||
}
|
||||
|
||||
function dedent(strings, ...values) {
|
||||
const str = String.raw({ raw: strings }, ...values)
|
||||
const [space] = str.split("\n").filter(Boolean).at(0).match(/^(\s*)/)
|
||||
const spaceLen = space.length
|
||||
return str.split("\n").map(line => line.slice(spaceLen)).join("\n").trim()
|
||||
}
|
||||
|
||||
Object.assign(dedent, {
|
||||
offset: (size) => (strings, ...values) => {
|
||||
return dedent(strings, ...values).split("\n").map(line => " ".repeat(size) + line).join("\n")
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
function generateCodeSnippet({ tools, pre, post, response }) {
|
||||
const lines = []
|
||||
|
||||
lines.push(dedent`
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
`)
|
||||
|
||||
if (tools) lines.push(`import { tool } from "@langchain/core/tools";`);
|
||||
if (response || tools) lines.push(`import { z } from "zod";`);
|
||||
|
||||
lines.push("", dedent`
|
||||
const agent = createReactAgent({
|
||||
llm: new ChatOpenAI({ model: "o4-mini" }),
|
||||
`)
|
||||
|
||||
if (tools) {
|
||||
lines.push(dedent.offset(2)`
|
||||
tools: [
|
||||
tool(() => "Sample tool output", {
|
||||
name: "sampleTool",
|
||||
schema: z.object({}),
|
||||
}),
|
||||
],
|
||||
`)
|
||||
}
|
||||
|
||||
if (pre) {
|
||||
lines.push(dedent.offset(2)`
|
||||
preModelHook: (state) => ({ llmInputMessages: state.messages }),
|
||||
`)
|
||||
}
|
||||
|
||||
if (post) {
|
||||
lines.push(dedent.offset(2)`
|
||||
postModelHook: (state) => state,
|
||||
`)
|
||||
}
|
||||
|
||||
if (response) {
|
||||
lines.push(dedent.offset(2)`
|
||||
responseFormat: z.object({ result: z.string() }),
|
||||
`)
|
||||
}
|
||||
|
||||
lines.push(`});`);
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function render() {
|
||||
const key = getKey();
|
||||
document.getElementById("agent-graph-img").src = `../assets/react_agent_graphs/${key}.svg`;
|
||||
|
||||
const state = {
|
||||
tools: document.getElementById("tools").checked,
|
||||
pre: document.getElementById("preModelHook").checked,
|
||||
post: document.getElementById("postModelHook").checked,
|
||||
response: document.getElementById("responseFormat").checked
|
||||
};
|
||||
|
||||
document.getElementById("agent-code").textContent = generateCodeSnippet(state);
|
||||
}
|
||||
|
||||
function initializeWidget() {
|
||||
render(); // no need for `await` here
|
||||
document.querySelectorAll(".agent-graph-features input").forEach((input) => {
|
||||
input.addEventListener("change", render);
|
||||
});
|
||||
}
|
||||
|
||||
// Init for both full reload and SPA nav (used by MkDocs Material)
|
||||
window.addEventListener("DOMContentLoaded", initializeWidget);
|
||||
document$.subscribe(initializeWidget);
|
||||
</script>
|
||||
|
||||
:::
|
||||
@@ -23,6 +23,7 @@ Then, navigate to [Agent Chat UI](https://agentchat.vercel.app), or clone the re
|
||||
|
||||
UI has out-of-box support for rendering tool calls, and tool result messages. To customize what messages are shown, see the [Hiding Messages in the Chat](https://github.com/langchain-ai/agent-chat-ui?tab=readme-ov-file#hiding-messages-in-the-chat) section in the Agent Chat UI documentation.
|
||||
|
||||
:::python
|
||||
## Add human-in-the-loop
|
||||
|
||||
Agent Chat UI has full support for [human-in-the-loop](../concepts/human_in_the_loop.md) workflows. To try it out, replace the agent code in `src/agent/graph.py` (from the [deployment](./deployment.md) guide) with this [agent implementation](./human-in-the-loop.md#using-with-agent-inbox):
|
||||
@@ -32,6 +33,7 @@ Agent Chat UI has full support for [human-in-the-loop](../concepts/human_in_the_
|
||||
!!! Important
|
||||
|
||||
Agent Chat UI works best if your LangGraph agent interrupts using the [`HumanInterrupt` schema][langgraph.prebuilt.interrupt.HumanInterrupt]. If you do not use that schema, the Agent Chat UI will be able to render the input passed to the `interrupt` function, but it will not have full support for resuming your graph.
|
||||
:::
|
||||
|
||||
## Generative UI
|
||||
|
||||
|
||||
@@ -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,9 +13,17 @@ tool-calling features, such as [OpenAI](https://platform.openai.com/api-keys),
|
||||
|
||||
Install the required packages:
|
||||
|
||||
:::python
|
||||
```bash
|
||||
pip install -U langgraph langsmith
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```bash
|
||||
npm install @langchain/langgraph @langchain/core langsmith
|
||||
```
|
||||
:::
|
||||
|
||||
!!! tip
|
||||
|
||||
@@ -27,6 +35,7 @@ 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
|
||||
|
||||
@@ -45,24 +54,53 @@ class State(TypedDict):
|
||||
|
||||
graph_builder = StateGraph(State)
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { Annotation } from "@langchain/langgraph";
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
import { StateGraph, START, END } from "@langchain/langgraph";
|
||||
|
||||
const StateAnnotation = Annotation.Root({
|
||||
// Messages have the type "BaseMessage[]". The messagesStateReducer function
|
||||
// defines how this state key should be updated
|
||||
// (in this case, it appends messages to the list, rather than overwriting them)
|
||||
messages: Annotation<BaseMessage[]>({
|
||||
reducer: (x, y) => x.concat(y),
|
||||
}),
|
||||
});
|
||||
|
||||
const graphBuilder = new StateGraph(StateAnnotation);
|
||||
```
|
||||
:::
|
||||
|
||||
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 function used with the annotation.
|
||||
|
||||
------
|
||||
|
||||
!!! 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. 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).
|
||||
|
||||
:::python
|
||||
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.
|
||||
:::
|
||||
|
||||
:::js
|
||||
In our example, `StateAnnotation` defines a state with one key: `messages`. The reducer function is used to append new messages to the list instead of overwriting it.
|
||||
:::
|
||||
|
||||
## 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:
|
||||
|
||||
:::python
|
||||
{!snippets/chat_model_tabs.md!}
|
||||
|
||||
<!---
|
||||
@@ -72,10 +110,21 @@ from langchain.chat_models import init_chat_model
|
||||
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 the chat model into a simple node:
|
||||
|
||||
:::python
|
||||
```python
|
||||
|
||||
def chatbot(state: State):
|
||||
@@ -87,26 +136,63 @@ def chatbot(state: State):
|
||||
# the node is used.
|
||||
graph_builder.add_node("chatbot", chatbot)
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
const chatbot = async (state: typeof StateAnnotation.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);
|
||||
```
|
||||
:::
|
||||
|
||||
**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 reducer function in our `StateAnnotation` 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
|
||||
graphBuilder.addEdge(START, "chatbot");
|
||||
```
|
||||
:::
|
||||
|
||||
## 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
|
||||
graphBuilder.addEdge("chatbot", END);
|
||||
```
|
||||
:::
|
||||
|
||||
This tells the graph to terminate after running the chatbot node.
|
||||
|
||||
## 6. Compile the graph
|
||||
@@ -114,14 +200,23 @@ This tells the graph to terminate after running the chatbot node.
|
||||
Before running the graph, we'll need to compile it. We can do so by calling `compile()`
|
||||
on the graph builder. This creates a `CompiledGraph` we can invoke on our state.
|
||||
|
||||
:::python
|
||||
```python
|
||||
graph = graph_builder.compile()
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
const graph = graphBuilder.compile();
|
||||
```
|
||||
:::
|
||||
|
||||
## 7. Visualize the graph (optional)
|
||||
|
||||
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
|
||||
```python
|
||||
from IPython.display import Image, display
|
||||
|
||||
@@ -131,14 +226,31 @@ except Exception:
|
||||
# This requires some extra dependencies and is optional
|
||||
pass
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import * as tslab from "tslab";
|
||||
|
||||
try {
|
||||
const drawableGraph = graph.getGraph();
|
||||
const image = await drawableGraph.drawMermaidPng();
|
||||
const arrayBuffer = await image.arrayBuffer();
|
||||
await tslab.display.png(new Uint8Array(arrayBuffer));
|
||||
} catch (error) {
|
||||
// This requires some extra dependencies and is optional
|
||||
console.log("Graph visualization not available");
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||

|
||||
|
||||
|
||||
## 8. Run the chatbot
|
||||
|
||||
Now run the chatbot!
|
||||
|
||||
:::python
|
||||
!!! tip
|
||||
|
||||
You can exit the chat loop at any time by typing `quit`, `exit`, or `q`.
|
||||
@@ -169,11 +281,41 @@ while True:
|
||||
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.
|
||||
Goodbye!
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { HumanMessage } from "@langchain/core/messages";
|
||||
|
||||
async function streamGraphUpdates(userInput: string) {
|
||||
const stream = await graph.stream({
|
||||
messages: [new HumanMessage(userInput)]
|
||||
});
|
||||
|
||||
for await (const event of stream) {
|
||||
for (const value of Object.values(event)) {
|
||||
console.log("Assistant:", value.messages[value.messages.length - 1].content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage
|
||||
const userInput = "What do you know about LangGraph?";
|
||||
console.log("User:", userInput);
|
||||
await streamGraphUpdates(userInput);
|
||||
```
|
||||
|
||||
```
|
||||
User: What do you know about LangGraph?
|
||||
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.
|
||||
```
|
||||
:::
|
||||
|
||||
**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.
|
||||
|
||||
Below is the full code for this tutorial:
|
||||
|
||||
:::python
|
||||
```python
|
||||
from typing import Annotated
|
||||
|
||||
@@ -206,9 +348,41 @@ graph_builder.add_edge(START, "chatbot")
|
||||
graph_builder.add_edge("chatbot", END)
|
||||
graph = graph_builder.compile()
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { Annotation } from "@langchain/langgraph";
|
||||
import { BaseMessage, HumanMessage } from "@langchain/core/messages";
|
||||
import { StateGraph, START, END } from "@langchain/langgraph";
|
||||
import { ChatAnthropic } from "@langchain/anthropic";
|
||||
|
||||
const StateAnnotation = Annotation.Root({
|
||||
messages: Annotation<BaseMessage[]>({
|
||||
reducer: (x, y) => x.concat(y),
|
||||
}),
|
||||
});
|
||||
|
||||
const graphBuilder = new StateGraph(StateAnnotation);
|
||||
|
||||
const llm = new ChatAnthropic({
|
||||
model: "claude-3-5-sonnet-latest",
|
||||
});
|
||||
|
||||
const chatbot = async (state: typeof StateAnnotation.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.
|
||||
|
||||
|
||||
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.
|
||||
@@ -8,19 +8,39 @@ To handle queries that your chatbot can't answer "from memory", integrate a web
|
||||
|
||||
## Prerequisites
|
||||
|
||||
:::python
|
||||
Before you start this tutorial, ensure you have the following:
|
||||
|
||||
- An API key for the [Tavily Search Engine](https://python.langchain.com/docs/integrations/tools/tavily_search/).
|
||||
:::
|
||||
|
||||
:::js
|
||||
Before you start this tutorial, ensure you have the following:
|
||||
|
||||
- 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://js.langchain.com/docs/integrations/tools/tavily_search/):
|
||||
|
||||
```bash
|
||||
npm install @langchain/community
|
||||
```
|
||||
:::
|
||||
|
||||
## 2. Configure your environment
|
||||
|
||||
:::python
|
||||
Configure your environment with your search engine API key:
|
||||
|
||||
```bash
|
||||
@@ -30,11 +50,21 @@ _set_env("TAVILY_API_KEY")
|
||||
```
|
||||
TAVILY_API_KEY: ········
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
Configure your environment with your search engine API key:
|
||||
|
||||
```typescript
|
||||
process.env.TAVILY_API_KEY = "tvly-...";
|
||||
```
|
||||
:::
|
||||
|
||||
## 3. Define the tool
|
||||
|
||||
Define the web search tool:
|
||||
|
||||
:::python
|
||||
```python
|
||||
from langchain_tavily import TavilySearch
|
||||
|
||||
@@ -42,9 +72,21 @@ tool = TavilySearch(max_results=2)
|
||||
tools = [tool]
|
||||
tool.invoke("What's a 'node' in LangGraph?")
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
|
||||
|
||||
const tool = new TavilySearchResults({ maxResults: 2 });
|
||||
const tools = [tool];
|
||||
await tool.invoke("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,
|
||||
@@ -62,9 +104,17 @@ The results are page summaries our chat bot can use to answer questions:
|
||||
'raw_content': None}],
|
||||
'response_time': 1.38}
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```
|
||||
'[{"title":"Introduction to LangGraph: A Beginner\'s Guide - Medium","url":"https://medium.com/@cplog/introduction-to-langgraph-a-beginners-guide-14f9be027141","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. We define nodes for classifying the input, handling greetings, and handling search queries. def classify_input_node(state): LangGraph is a versatile tool for building complex, stateful applications with LLMs. By understanding its core concepts and working through simple examples, beginners can start to leverage its power for their projects. Remember to pay attention to state management, conditional edges, and ensuring there are no dead-end nodes in your graph.","score":0.7065353,"raw_content":null},{"title":"LangGraph Tutorial: What Is LangGraph and How to Use It?","url":"https://www.datacamp.com/tutorial/langgraph-tutorial","content":"LangGraph is a library within the LangChain ecosystem that provides a framework for defining, coordinating, and executing multiple LLM agents (or chains) in a structured and efficient manner. By managing the flow of data and the sequence of operations, LangGraph allows developers to focus on the high-level logic of their applications rather than the intricacies of agent coordination. Whether you need a chatbot that can handle various types of user requests or a multi-agent system that performs complex tasks, LangGraph provides the tools to build exactly what you need. LangGraph significantly simplifies the development of complex LLM applications by providing a structured framework for managing state and coordinating agent interactions.","score":0.5008063,"raw_content":null}]'
|
||||
```
|
||||
:::
|
||||
|
||||
## 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.
|
||||
|
||||
Let's first select our LLM:
|
||||
@@ -103,9 +153,52 @@ def chatbot(state: State):
|
||||
|
||||
graph_builder.add_node("chatbot", chatbot)
|
||||
```
|
||||
:::
|
||||
|
||||
:::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:
|
||||
|
||||
```typescript
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
|
||||
const llm = new ChatOpenAI({
|
||||
model: "gpt-4o",
|
||||
temperature: 0,
|
||||
});
|
||||
```
|
||||
|
||||
We can now incorporate it into a `StateGraph`:
|
||||
|
||||
```typescript hl_lines="15"
|
||||
import { Annotation } from "@langchain/langgraph";
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
|
||||
const StateAnnotation = Annotation.Root({
|
||||
messages: Annotation<BaseMessage[]>({
|
||||
reducer: (x, y) => x.concat(y),
|
||||
}),
|
||||
});
|
||||
|
||||
import { StateGraph, START, END } from "@langchain/langgraph";
|
||||
|
||||
const graphBuilder = new StateGraph(StateAnnotation);
|
||||
|
||||
// Modification: tell the LLM which tools it can call
|
||||
const llmWithTools = llm.bindTools(tools);
|
||||
|
||||
const chatbot = async (state: typeof StateAnnotation.State) => {
|
||||
return { messages: [await llmWithTools.invoke(state.messages)] };
|
||||
};
|
||||
|
||||
graphBuilder.addNode("chatbot", chatbot);
|
||||
```
|
||||
:::
|
||||
|
||||
## 5. Create a function to run the tools
|
||||
|
||||
:::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
|
||||
@@ -143,6 +236,50 @@ class BasicToolNode:
|
||||
tool_node = BasicToolNode(tools=[tool])
|
||||
graph_builder.add_node("tools", tool_node)
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
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.
|
||||
|
||||
```typescript
|
||||
import { ToolMessage } from "@langchain/core/messages";
|
||||
|
||||
class BasicToolNode {
|
||||
private toolsByName: Record<string, any>;
|
||||
|
||||
constructor(tools: any[]) {
|
||||
this.toolsByName = {};
|
||||
for (const tool of tools) {
|
||||
this.toolsByName[tool.name] = tool;
|
||||
}
|
||||
}
|
||||
|
||||
async __call__(inputs: Record<string, any>): Promise<{ messages: ToolMessage[] }> {
|
||||
const messages = inputs.messages || [];
|
||||
if (messages.length === 0) {
|
||||
throw new Error("No message found in input");
|
||||
}
|
||||
const message = messages[messages.length - 1];
|
||||
const outputs: ToolMessage[] = [];
|
||||
|
||||
for (const toolCall of message.tool_calls || []) {
|
||||
const toolResult = await this.toolsByName[toolCall.name].invoke(toolCall.args);
|
||||
outputs.push(
|
||||
new ToolMessage({
|
||||
content: JSON.stringify(toolResult),
|
||||
name: toolCall.name,
|
||||
tool_call_id: toolCall.id,
|
||||
})
|
||||
);
|
||||
}
|
||||
return { messages: outputs };
|
||||
}
|
||||
}
|
||||
|
||||
const toolNode = new BasicToolNode([tool]);
|
||||
graphBuilder.addNode("tools", async (state) => toolNode.__call__(state));
|
||||
```
|
||||
:::
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -154,6 +291,7 @@ 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.
|
||||
|
||||
:::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.
|
||||
|
||||
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.
|
||||
@@ -194,6 +332,51 @@ graph_builder.add_edge("tools", "chatbot")
|
||||
graph_builder.add_edge(START, "chatbot")
|
||||
graph = graph_builder.compile()
|
||||
```
|
||||
:::
|
||||
|
||||
:::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.
|
||||
|
||||
```typescript
|
||||
import { AIMessage } from "@langchain/core/messages";
|
||||
|
||||
const routeTools = (state: typeof StateAnnotation.State) => {
|
||||
/**
|
||||
* Use in the conditional_edge to route to the ToolNode if the last message
|
||||
* has tool calls. Otherwise, route to the end.
|
||||
*/
|
||||
const messages = state.messages;
|
||||
const lastMessage = messages[messages.length - 1] as AIMessage;
|
||||
|
||||
if (lastMessage.tool_calls && lastMessage.tool_calls.length > 0) {
|
||||
return "tools";
|
||||
}
|
||||
return END;
|
||||
};
|
||||
|
||||
// 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.
|
||||
graphBuilder.addConditionalEdges(
|
||||
"chatbot",
|
||||
routeTools,
|
||||
// The following dictionary lets you tell the graph to interpret the condition's outputs as a specific node
|
||||
// It defaults to the identity function, but if you
|
||||
// want to use a node named something else apart from "tools",
|
||||
// You can update the value of the dictionary to something else
|
||||
// e.g., "tools": "my_tools"
|
||||
{
|
||||
tools: "tools",
|
||||
[END]: END,
|
||||
}
|
||||
);
|
||||
// Any time a tool is called, we return to the chatbot to decide the next step
|
||||
graphBuilder.addEdge("tools", "chatbot");
|
||||
graphBuilder.addEdge(START, "chatbot");
|
||||
const graph = graphBuilder.compile();
|
||||
```
|
||||
:::
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -201,6 +384,7 @@ graph = graph_builder.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
|
||||
@@ -212,6 +396,26 @@ except Exception:
|
||||
# This requires some extra dependencies and is optional
|
||||
pass
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
You can visualize the graph using the `getGraph` method and one of the "draw" methods, like `drawAscii` or `drawMermaidPng`. The `draw` methods each require additional dependencies.
|
||||
|
||||
```typescript
|
||||
import * as tslab from "tslab";
|
||||
|
||||
try {
|
||||
const representation = graph.getGraph();
|
||||
const image = await representation.drawMermaidPng();
|
||||
const arrayBuffer = await image.arrayBuffer();
|
||||
|
||||
await tslab.display.png(new Uint8Array(arrayBuffer));
|
||||
} catch (error) {
|
||||
// This requires some extra dependencies and is optional
|
||||
console.log("Graph visualization not available");
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||

|
||||
|
||||
@@ -219,6 +423,7 @@ except Exception:
|
||||
|
||||
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}]}):
|
||||
@@ -274,11 +479,71 @@ LangGraph appears to be a significant tool in the evolving landscape of LLM-base
|
||||
Goodbye!
|
||||
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { HumanMessage } from "@langchain/core/messages";
|
||||
|
||||
const streamGraphUpdates = async (userInput: string) => {
|
||||
const stream = await graph.stream(
|
||||
{ messages: [new HumanMessage(userInput)] },
|
||||
{ streamMode: "values" }
|
||||
);
|
||||
|
||||
for await (const event of stream) {
|
||||
const messages = event.messages;
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
console.log("Assistant:", lastMessage.content);
|
||||
}
|
||||
};
|
||||
|
||||
// Example usage
|
||||
const userInput = "What do you know about LangGraph?";
|
||||
console.log("User:", userInput);
|
||||
await streamGraphUpdates(userInput);
|
||||
```
|
||||
|
||||
```
|
||||
Assistant: I'll search for information about LangGraph to provide you with accurate details.
|
||||
Assistant: [{"title": "Introduction to LangGraph: A Beginner's Guide - Medium", "url": "https://medium.com/@cplog/introduction-to-langgraph-a-beginners-guide-14f9be027141", "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. We define nodes for classifying the input, handling greetings, and handling search queries. def classify_input_node(state): LangGraph is a versatile tool for building complex, stateful applications with LLMs. By understanding its core concepts and working through simple examples, beginners can start to leverage its power for their projects. Remember to pay attention to state management, conditional edges, and ensuring there are no dead-end nodes in your graph.", "score": 0.7065353, "raw_content": null}, {"title": "LangGraph Tutorial: What Is LangGraph and How to Use It?", "url": "https://www.datacamp.com/tutorial/langgraph-tutorial", "content": "LangGraph is a library within the LangChain ecosystem that provides a framework for defining, coordinating, and executing multiple LLM agents or chains in a structured and efficient manner. By managing the flow of data and the sequence of operations, LangGraph allows developers to focus on the high-level logic of their applications rather than the intricacies of agent coordination. Whether you need a chatbot that can handle various types of user requests or a multi-agent system that performs complex tasks, LangGraph provides the tools to build exactly what you need. LangGraph significantly simplifies the development of complex LLM applications by providing a structured framework for managing state and coordinating agent interactions.", "score": 0.5008063, "raw_content": null}]
|
||||
Assistant: Based on the search results, I can provide you with comprehensive information about LangGraph:
|
||||
|
||||
## What is LangGraph?
|
||||
|
||||
LangGraph is a library within the LangChain ecosystem designed for building stateful, multi-actor applications with Large Language Models (LLMs). It provides a framework for defining, coordinating, and executing multiple LLM agents or chains in a structured and efficient manner.
|
||||
|
||||
## Key Features:
|
||||
|
||||
1. **Stateful Graph Architecture**: LangGraph revolves around the concept of a stateful graph where each node represents a step in your 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. **Multi-Agent Coordination**: LangGraph manages the flow of data and sequence of operations, allowing developers to focus on high-level logic rather than the intricacies of agent coordination.
|
||||
|
||||
## Use Cases:
|
||||
|
||||
- Building conversational agents
|
||||
- Creating chatbots that can handle various types of user requests
|
||||
- Developing multi-agent systems that perform complex tasks
|
||||
- Complex task automation
|
||||
- Custom LLM-backed experiences
|
||||
|
||||
## Benefits:
|
||||
|
||||
- **Simplified Development**: LangGraph significantly simplifies the development of complex LLM applications by providing a structured framework for managing state and coordinating agent interactions.
|
||||
- **Flexibility**: It's a versatile tool for building complex, stateful applications with LLMs.
|
||||
- **Focus on Logic**: Developers can focus on the high-level logic of their applications rather than coordination details.
|
||||
|
||||
LangGraph is particularly valuable for projects that require sophisticated AI workflows with multiple steps, decision points, and state management across different components.
|
||||
```
|
||||
:::
|
||||
|
||||
## 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)
|
||||
|
||||
@@ -322,9 +587,56 @@ graph_builder.add_edge("tools", "chatbot")
|
||||
graph_builder.add_edge(START, "chatbot")
|
||||
graph = graph_builder.compile()
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
- `BasicToolNode` is replaced with the prebuilt [ToolNode](https://langchain-ai.github.io/langgraph/reference/prebuilt/#toolnode)
|
||||
- `routeTools` is replaced with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition)
|
||||
|
||||
```typescript hl_lines="25 30"
|
||||
import { Annotation } from "@langchain/langgraph";
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
|
||||
import { StateGraph, START, END } from "@langchain/langgraph";
|
||||
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
const StateAnnotation = Annotation.Root({
|
||||
messages: Annotation<BaseMessage[]>({
|
||||
reducer: (x, y) => x.concat(y),
|
||||
}),
|
||||
});
|
||||
|
||||
const graphBuilder = new StateGraph(StateAnnotation);
|
||||
|
||||
const tool = new TavilySearchResults({ maxResults: 2 });
|
||||
const tools = [tool];
|
||||
const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0 });
|
||||
const llmWithTools = llm.bindTools(tools);
|
||||
|
||||
const chatbot = async (state: typeof StateAnnotation.State) => {
|
||||
return { messages: [await llmWithTools.invoke(state.messages)] };
|
||||
};
|
||||
|
||||
graphBuilder.addNode("chatbot", chatbot);
|
||||
|
||||
const toolNode = new ToolNode(tools);
|
||||
graphBuilder.addNode("tools", toolNode);
|
||||
|
||||
graphBuilder.addConditionalEdges(
|
||||
"chatbot",
|
||||
toolsCondition,
|
||||
);
|
||||
// Any time a tool is called, we return to the chatbot to decide the next step
|
||||
graphBuilder.addEdge("tools", "chatbot");
|
||||
graphBuilder.addEdge(START, "chatbot");
|
||||
const graph = graphBuilder.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).
|
||||
|
||||
## Next steps
|
||||
|
||||
The chatbot cannot remember past interactions on its own, which limits its ability to have coherent, multi-turn conversations. In the next part, you will [add **memory**](./3-add-memory.md) to address this.
|
||||
The chatbot cannot remember past interactions on its own, which limits its ability to have coherent, multi-turn conversations. In the next part, you will [add **memory**](./3-add-memory.md) to address this.
|
||||
@@ -14,11 +14,21 @@ We will see later that **checkpointing** is _much_ more powerful than simple cha
|
||||
|
||||
Create a `MemorySaver` checkpointer:
|
||||
|
||||
:::python
|
||||
``` python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
memory = MemorySaver()
|
||||
```
|
||||
:::
|
||||
|
||||
:::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.
|
||||
|
||||
@@ -26,6 +36,7 @@ This is in-memory checkpointer, which is convenient for the tutorial. However, i
|
||||
|
||||
Compile the graph with the provided checkpointer, which will checkpoint the `State` as the graph works through each node:
|
||||
|
||||
:::python
|
||||
``` python
|
||||
graph = graph_builder.compile(checkpointer=memory)
|
||||
```
|
||||
@@ -39,6 +50,27 @@ except Exception:
|
||||
# This requires some extra dependencies and is optional
|
||||
pass
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
const graph = graphBuilder.compile({ checkpointer: memory });
|
||||
```
|
||||
|
||||
```typescript
|
||||
import * as tslab from "tslab";
|
||||
|
||||
try {
|
||||
const representation = graph.getGraph();
|
||||
const image = await representation.drawMermaidPng();
|
||||
const arrayBuffer = await image.arrayBuffer();
|
||||
|
||||
await tslab.display.png(new Uint8Array(arrayBuffer));
|
||||
} catch (e) {
|
||||
// This requires some extra dependencies and is optional
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
## 3. Interact with your chatbot
|
||||
|
||||
@@ -46,12 +78,21 @@ Now you can interact with your bot!
|
||||
|
||||
1. Pick a thread to use as the key for this conversation.
|
||||
|
||||
:::python
|
||||
```python
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
const config = { configurable: { thread_id: "1" } };
|
||||
```
|
||||
:::
|
||||
|
||||
2. Call your chatbot:
|
||||
|
||||
:::python
|
||||
```python
|
||||
user_input = "Hi there! My name is Will."
|
||||
|
||||
@@ -64,6 +105,24 @@ Now you can interact with your bot!
|
||||
for event in events:
|
||||
event["messages"][-1].pretty_print()
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
const userInput = "Hi there! My name is Will.";
|
||||
|
||||
// The config is the **second positional argument** to stream() or invoke()!
|
||||
const events = await graph.stream(
|
||||
{ messages: [{ role: "user", content: userInput }] },
|
||||
{ ...config, streamMode: "values" }
|
||||
);
|
||||
|
||||
for await (const event of events) {
|
||||
const messages = event.messages;
|
||||
console.log(messages[messages.length - 1]);
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
```
|
||||
================================ Human Message =================================
|
||||
@@ -74,14 +133,23 @@ 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?
|
||||
```
|
||||
|
||||
:::python
|
||||
!!! 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
|
||||
!!! note
|
||||
|
||||
The config was provided as the **second positional argument** 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?"
|
||||
|
||||
@@ -94,6 +162,24 @@ events = graph.stream(
|
||||
for event in events:
|
||||
event["messages"][-1].pretty_print()
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
const userInput2 = "Remember my name?";
|
||||
|
||||
// The config is the **second positional argument** to stream() or invoke()!
|
||||
const events2 = await graph.stream(
|
||||
{ messages: [{ role: "user", content: userInput2 }] },
|
||||
{ ...config, streamMode: "values" }
|
||||
);
|
||||
|
||||
for await (const event of events2) {
|
||||
const messages = event.messages;
|
||||
console.log(messages[messages.length - 1]);
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
```
|
||||
================================ Human Message =================================
|
||||
@@ -108,6 +194,7 @@ Of course, I remember your name, Will. I always try to pay attention to importan
|
||||
|
||||
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(
|
||||
@@ -119,6 +206,23 @@ events = graph.stream(
|
||||
for event in events:
|
||||
event["messages"][-1].pretty_print()
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
// The only difference is we change the `thread_id` here to "2" instead of "1"
|
||||
const events3 = await graph.stream(
|
||||
{ messages: [{ role: "user", content: userInput2 }] },
|
||||
// highlight-next-line
|
||||
{ configurable: { thread_id: "2" }, streamMode: "values" }
|
||||
);
|
||||
|
||||
for await (const event of events3) {
|
||||
const messages = event.messages;
|
||||
console.log(messages[messages.length - 1]);
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
```
|
||||
================================ Human Message =================================
|
||||
@@ -133,8 +237,15 @@ I apologize, but I don't have any previous context or memory of your name. As an
|
||||
|
||||
## 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)`.
|
||||
:::
|
||||
|
||||
:::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)`.
|
||||
:::
|
||||
|
||||
:::python
|
||||
```python
|
||||
snapshot = graph.get_state(config)
|
||||
snapshot
|
||||
@@ -147,6 +258,75 @@ 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
|
||||
```typescript
|
||||
const snapshot = await graph.getState(config);
|
||||
console.log(snapshot);
|
||||
```
|
||||
|
||||
```
|
||||
StateSnapshot {
|
||||
values: {
|
||||
messages: [
|
||||
HumanMessage {
|
||||
content: 'Hi there! My name is Will.',
|
||||
id: '8c1ca919-c553-4ebf-95d4-b59a2d61e078'
|
||||
},
|
||||
AIMessage {
|
||||
content: "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?",
|
||||
id: 'run-58587b77-8c82-41e6-8a90-d62c444a261d-0'
|
||||
},
|
||||
HumanMessage {
|
||||
content: 'Remember my name?',
|
||||
id: 'daba7df6-ad75-4d6b-8057-745881cea1ca'
|
||||
},
|
||||
AIMessage {
|
||||
content: "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.",
|
||||
id: 'run-ffeaae5c-4d2d-4ddb-bd59-5d5cbf2a5af8-0'
|
||||
}
|
||||
]
|
||||
},
|
||||
next: [],
|
||||
config: {
|
||||
configurable: {
|
||||
thread_id: '1',
|
||||
checkpoint_ns: '',
|
||||
checkpoint_id: '1ef7d06e-93e0-6acc-8004-f2ac846575d2'
|
||||
}
|
||||
},
|
||||
metadata: {
|
||||
source: 'loop',
|
||||
writes: {
|
||||
chatbot: {
|
||||
messages: [
|
||||
AIMessage {
|
||||
content: "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.",
|
||||
id: 'run-ffeaae5c-4d2d-4ddb-bd59-5d5cbf2a5af8-0'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
step: 4,
|
||||
parents: {}
|
||||
},
|
||||
createdAt: '2024-09-27T19:30:10.820758+00:00',
|
||||
parentConfig: {
|
||||
configurable: {
|
||||
thread_id: '1',
|
||||
checkpoint_ns: '',
|
||||
checkpoint_id: '1ef7d06e-859f-6206-8003-e1bd3c264b8f'
|
||||
}
|
||||
},
|
||||
tasks: []
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
console.log(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)
|
||||
```
|
||||
:::
|
||||
|
||||
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.
|
||||
|
||||
@@ -157,13 +337,24 @@ Check out the code snippet below to review the graph from this tutorial:
|
||||
{!snippets/chat_model_tabs.md!}
|
||||
|
||||
<!---
|
||||
:::python
|
||||
```python
|
||||
from langchain.chat_models import init_chat_model
|
||||
|
||||
llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
|
||||
const llm = new ChatOpenAI({ model: "gpt-4" });
|
||||
```
|
||||
:::
|
||||
-->
|
||||
|
||||
:::python
|
||||
```python hl_lines="36 37"
|
||||
from typing import Annotated
|
||||
|
||||
@@ -203,7 +394,51 @@ graph_builder.set_entry_point("chatbot")
|
||||
memory = MemorySaver()
|
||||
graph = graph_builder.compile(checkpointer=memory)
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript hl_lines="36 37"
|
||||
import { Annotation } from "@langchain/langgraph";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
import { MemorySaver, StateGraph } from "@langchain/langgraph";
|
||||
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
const StateAnnotation = Annotation.Root({
|
||||
messages: Annotation<BaseMessage[]>({
|
||||
reducer: (x, y) => x.concat(y),
|
||||
}),
|
||||
});
|
||||
|
||||
const graphBuilder = new StateGraph(StateAnnotation);
|
||||
|
||||
const tool = new TavilySearchResults({ maxResults: 2 });
|
||||
const tools = [tool];
|
||||
const llm = new ChatOpenAI({ model: "gpt-4" });
|
||||
const llmWithTools = llm.bindTools(tools);
|
||||
|
||||
function chatbot(state: typeof StateAnnotation.State) {
|
||||
return { messages: [llmWithTools.invoke(state.messages)] };
|
||||
}
|
||||
|
||||
graphBuilder.addNode("chatbot", chatbot);
|
||||
|
||||
const toolNode = new ToolNode(tools);
|
||||
graphBuilder.addNode("tools", toolNode);
|
||||
|
||||
graphBuilder.addConditionalEdges(
|
||||
"chatbot",
|
||||
toolsCondition,
|
||||
);
|
||||
graphBuilder.addEdge("tools", "chatbot");
|
||||
graphBuilder.addEdge("__start__", "chatbot");
|
||||
|
||||
const memory = new MemorySaver();
|
||||
const graph = graphBuilder.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.
|
||||
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.
|
||||
@@ -14,6 +14,7 @@ Starting with the existing code from the [Add memory to the chatbot](./3-add-mem
|
||||
|
||||
Let's first select a chat model:
|
||||
|
||||
:::python
|
||||
{!snippets/chat_model_tabs.md!}
|
||||
|
||||
<!---
|
||||
@@ -23,9 +24,21 @@ from langchain.chat_models import init_chat_model
|
||||
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 our `StateGraph` with an additional tool:
|
||||
|
||||
:::python
|
||||
``` python hl_lines="12 19 20 21 22 23"
|
||||
from typing import Annotated
|
||||
|
||||
@@ -75,6 +88,60 @@ 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 { tool } from "@langchain/core/tools";
|
||||
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
|
||||
import { z } from "zod";
|
||||
|
||||
import { MemorySaver } from "@langchain/langgraph";
|
||||
import { StateGraph, START, END, MessagesAnnotation } from "@langchain/langgraph";
|
||||
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
import { interrupt, Command } from "@langchain/langgraph";
|
||||
|
||||
const humanAssistance = tool(async ({ query }) => {
|
||||
const humanResponse = interrupt({ query });
|
||||
return humanResponse.data;
|
||||
}, {
|
||||
name: "human_assistance",
|
||||
description: "Request assistance from a human.",
|
||||
schema: z.object({
|
||||
query: z.string().describe("Human readable question for the human")
|
||||
})
|
||||
});
|
||||
|
||||
const searchTool = new TavilySearchResults({ maxResults: 2 });
|
||||
const tools = [searchTool, humanAssistance];
|
||||
const llmWithTools = llm.bindTools(tools);
|
||||
|
||||
const chatbot = async (state: typeof MessagesAnnotation.State) => {
|
||||
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 for this example");
|
||||
}
|
||||
return { messages: [message] };
|
||||
};
|
||||
|
||||
const graphBuilder = new StateGraph(MessagesAnnotation)
|
||||
.addNode("chatbot", chatbot);
|
||||
|
||||
const toolNode = new ToolNode(tools);
|
||||
graphBuilder.addNode("tools", toolNode);
|
||||
|
||||
graphBuilder.addConditionalEdges(
|
||||
"chatbot",
|
||||
toolsCondition,
|
||||
);
|
||||
graphBuilder.addEdge("tools", "chatbot");
|
||||
graphBuilder.addEdge(START, "chatbot");
|
||||
```
|
||||
:::
|
||||
|
||||
!!! tip
|
||||
|
||||
@@ -84,16 +151,27 @@ graph_builder.add_edge(START, "chatbot")
|
||||
|
||||
We compile the graph with a checkpointer, as before:
|
||||
|
||||
:::python
|
||||
```python
|
||||
memory = MemorySaver()
|
||||
|
||||
graph = graph_builder.compile(checkpointer=memory)
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
const memory = new MemorySaver();
|
||||
|
||||
const graph = graphBuilder.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
|
||||
from IPython.display import Image, display
|
||||
|
||||
@@ -103,6 +181,19 @@ except Exception:
|
||||
# This requires some extra dependencies and is optional
|
||||
pass
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import * as tslab from "tslab";
|
||||
|
||||
const drawableGraph = graph.getGraph();
|
||||
const image = await drawableGraph.drawMermaidPng();
|
||||
const arrayBuffer = await image.arrayBuffer();
|
||||
|
||||
await tslab.display.png(new Uint8Array(arrayBuffer));
|
||||
```
|
||||
:::
|
||||
|
||||

|
||||
|
||||
@@ -110,6 +201,7 @@ except Exception:
|
||||
|
||||
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"}}
|
||||
@@ -137,9 +229,49 @@ Tool Calls:
|
||||
Args:
|
||||
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 = graph.stream(
|
||||
{ messages: [{ role: "user", content: userInput }] },
|
||||
config,
|
||||
);
|
||||
|
||||
for await (const event of events) {
|
||||
if (event.messages) {
|
||||
const lastMessage = event.messages[event.messages.length - 1];
|
||||
console.log(`================================ ${lastMessage.getType()} Message =================================`);
|
||||
console.log(lastMessage.content);
|
||||
if (lastMessage.tool_calls?.length) {
|
||||
console.log("Tool Calls:");
|
||||
lastMessage.tool_calls.forEach((call) => {
|
||||
console.log(` ${call.name} (${call.id})`);
|
||||
console.log(` Args: ${JSON.stringify(call.args)}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
================================ Human Message =================================
|
||||
I need some expert guidance for building an AI agent. Could you request assistance for me?
|
||||
================================== Ai Message ==================================
|
||||
I'd be happy to request expert assistance for you regarding building an AI agent. Let me use the human assistance function to get you some expert guidance.
|
||||
|
||||
Tool Calls:
|
||||
human_assistance (toolu_01ABUqneqnuHNuo1vhfDFQCW)
|
||||
Args: {"query":"A user is requesting expert guidance for building an AI agent. Could you please provide some expert advice or resources on this topic?"}
|
||||
```
|
||||
:::
|
||||
|
||||
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
|
||||
@@ -148,7 +280,20 @@ snapshot.next
|
||||
```
|
||||
('tools',)
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
const snapshot = await graph.getState(config);
|
||||
console.log(snapshot.next);
|
||||
```
|
||||
|
||||
```
|
||||
['tools']
|
||||
```
|
||||
:::
|
||||
|
||||
:::python
|
||||
!!! info Additional information
|
||||
|
||||
Take a closer look at the `human_assistance` tool:
|
||||
@@ -162,11 +307,34 @@ 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
|
||||
!!! info Additional information
|
||||
|
||||
Take a closer look at the `human_assistance` tool:
|
||||
|
||||
```typescript
|
||||
const humanAssistance = tool(async ({ query }) => {
|
||||
const humanResponse = interrupt({ query });
|
||||
return humanResponse.data;
|
||||
}, {
|
||||
name: "human_assistance",
|
||||
description: "Request assistance from a human.",
|
||||
schema: z.object({
|
||||
query: z.string().describe("Human readable question for the human")
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
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 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"`:
|
||||
|
||||
:::python
|
||||
``` python
|
||||
human_response = (
|
||||
"We, the experts are here to help! We'd recommend you check out LangGraph to build your agent."
|
||||
@@ -214,6 +382,47 @@ LangGraph is likely a framework or library designed specifically for creating AI
|
||||
If you'd like more specific information about LangGraph or have any questions about this recommendation, please feel free to ask, and I can request further assistance from the experts.
|
||||
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```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 = graph.stream(humanCommand, config);
|
||||
|
||||
for await (const event of resumeEvents) {
|
||||
if (event.messages) {
|
||||
const lastMessage = event.messages[event.messages.length - 1];
|
||||
console.log(`================================ ${lastMessage.getType()} Message =================================`);
|
||||
console.log(lastMessage.content);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
================================== Ai Message ==================================
|
||||
I'd be happy to request expert assistance for you regarding building an AI agent. Let me use the human assistance function to get you some expert guidance.
|
||||
================================= Tool Message =================================
|
||||
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 Message ==================================
|
||||
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.
|
||||
|
||||
@@ -221,6 +430,7 @@ The input has been received and processed as a tool message. Review this call's
|
||||
|
||||
Check out the code snippet below to review the graph from this tutorial:
|
||||
|
||||
:::python
|
||||
{!snippets/chat_model_tabs.md!}
|
||||
|
||||
```python
|
||||
@@ -271,6 +481,64 @@ graph_builder.add_edge(START, "chatbot")
|
||||
memory = MemorySaver()
|
||||
graph = graph_builder.compile(checkpointer=memory)
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
|
||||
import { z } from "zod";
|
||||
import { ChatAnthropic } from "@langchain/anthropic";
|
||||
|
||||
import { MemorySaver } from "@langchain/langgraph";
|
||||
import { StateGraph, START, END, MessagesAnnotation } from "@langchain/langgraph";
|
||||
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
|
||||
import { interrupt, Command } from "@langchain/langgraph";
|
||||
|
||||
const llm = new ChatAnthropic({
|
||||
model: "claude-3-5-sonnet-latest",
|
||||
});
|
||||
|
||||
const humanAssistance = tool(async ({ query }) => {
|
||||
const humanResponse = interrupt({ query });
|
||||
return humanResponse.data;
|
||||
}, {
|
||||
name: "human_assistance",
|
||||
description: "Request assistance from a human.",
|
||||
schema: z.object({
|
||||
query: z.string().describe("Human readable question for the human")
|
||||
})
|
||||
});
|
||||
|
||||
const searchTool = new TavilySearchResults({ maxResults: 2 });
|
||||
const tools = [searchTool, humanAssistance];
|
||||
const llmWithTools = llm.bindTools(tools);
|
||||
|
||||
const chatbot = async (state: typeof MessagesAnnotation.State) => {
|
||||
const message = await llmWithTools.invoke(state.messages);
|
||||
if (message.tool_calls && message.tool_calls.length > 1) {
|
||||
throw new Error("Multiple tool calls not supported for this example");
|
||||
}
|
||||
return { messages: [message] };
|
||||
};
|
||||
|
||||
const graphBuilder = new StateGraph(MessagesAnnotation)
|
||||
.addNode("chatbot", chatbot);
|
||||
|
||||
const toolNode = new ToolNode(tools);
|
||||
graphBuilder.addNode("tools", toolNode);
|
||||
|
||||
graphBuilder.addConditionalEdges(
|
||||
"chatbot",
|
||||
toolsCondition,
|
||||
);
|
||||
graphBuilder.addEdge("tools", "chatbot");
|
||||
graphBuilder.addEdge(START, "chatbot");
|
||||
|
||||
const memory = new MemorySaver();
|
||||
const graph = graphBuilder.compile({ checkpointer: memory });
|
||||
```
|
||||
:::
|
||||
|
||||
## Next steps
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ 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
|
||||
|
||||
@@ -25,11 +26,30 @@ class State(TypedDict):
|
||||
# highlight-next-line
|
||||
birthday: str
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { Annotation } from "@langchain/langgraph";
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
|
||||
const StateAnnotation = Annotation.Root({
|
||||
messages: Annotation<BaseMessage[]>({
|
||||
reducer: (x, y) => x.concat(y),
|
||||
}),
|
||||
// highlight-next-line
|
||||
name: Annotation<string>,
|
||||
// highlight-next-line
|
||||
birthday: Annotation<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
|
||||
@@ -75,11 +95,73 @@ def human_assistance(
|
||||
# We return a Command object in the tool to update our state.
|
||||
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 { z } from "zod";
|
||||
import { Command, interrupt } from "@langchain/langgraph";
|
||||
|
||||
const humanAssistance = tool(async (input, config) => {
|
||||
const { name, birthday } = input;
|
||||
// Note that because we are generating a ToolMessage for a state update, we
|
||||
// generally require the ID of the corresponding tool call. We can access this
|
||||
// from the tool's config when it's called by a model.
|
||||
const toolCallId = config?.toolCall?.id;
|
||||
|
||||
const humanResponse = interrupt({
|
||||
question: "Is this correct?",
|
||||
name: name,
|
||||
birthday: birthday,
|
||||
});
|
||||
|
||||
let verifiedName, verifiedBirthday, response;
|
||||
|
||||
// If the information is correct, update the state as-is.
|
||||
if (humanResponse?.correct?.toLowerCase().startsWith("y")) {
|
||||
verifiedName = name;
|
||||
verifiedBirthday = birthday;
|
||||
response = "Correct";
|
||||
} else {
|
||||
// Otherwise, receive information from the human reviewer.
|
||||
verifiedName = humanResponse?.name || name;
|
||||
verifiedBirthday = humanResponse?.birthday || birthday;
|
||||
response = `Made a correction: ${JSON.stringify(humanResponse)}`;
|
||||
}
|
||||
|
||||
// This time we explicitly update the state with a ToolMessage inside
|
||||
// the tool.
|
||||
const stateUpdate = {
|
||||
name: verifiedName,
|
||||
birthday: verifiedBirthday,
|
||||
messages: [new ToolMessage({
|
||||
content: response,
|
||||
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(),
|
||||
birthday: z.string(),
|
||||
}),
|
||||
});
|
||||
```
|
||||
:::
|
||||
|
||||
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
|
||||
@@ -98,6 +180,30 @@ for event in events:
|
||||
if "messages" in event:
|
||||
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
|
||||
const userInput = "Can you look up when LangGraph was released? " +
|
||||
"When you have the answer, use the humanAssistance tool for review.";
|
||||
const config = { configurable: { thread_id: "1" } };
|
||||
|
||||
const events = graph.stream(
|
||||
{ messages: [{ role: "user", content: userInput }] },
|
||||
{ ...config, streamMode: "values" }
|
||||
);
|
||||
|
||||
for await (const event of events) {
|
||||
if (event.messages) {
|
||||
const lastMessage = event.messages[event.messages.length - 1];
|
||||
console.log(`================================ ${lastMessage._getType()} Message =================================`);
|
||||
console.log(lastMessage.content);
|
||||
}
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
```
|
||||
================================ Human Message =================================
|
||||
@@ -130,6 +236,7 @@ We've hit the `interrupt` in the `human_assistance` tool again.
|
||||
|
||||
## 4. Add human assistance
|
||||
|
||||
:::python
|
||||
The chatbot failed to identify the correct date, so supply it with information:
|
||||
|
||||
```python
|
||||
@@ -145,6 +252,32 @@ for event in events:
|
||||
if "messages" in event:
|
||||
event["messages"][-1].pretty_print()
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
The chatbot failed to identify the correct date, so supply it with information:
|
||||
|
||||
```typescript
|
||||
import { Command } from "@langchain/langgraph";
|
||||
|
||||
const humanCommand = new Command({
|
||||
resume: {
|
||||
name: "LangGraph",
|
||||
birthday: "Jan 17, 2024",
|
||||
},
|
||||
});
|
||||
|
||||
const resumeEvents = graph.stream(humanCommand, { ...config, streamMode: "values" });
|
||||
|
||||
for await (const event of resumeEvents) {
|
||||
if (event.messages) {
|
||||
const lastMessage = event.messages[event.messages.length - 1];
|
||||
console.log(`================================ ${lastMessage._getType()} Message =================================`);
|
||||
console.log(lastMessage.content);
|
||||
}
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
```
|
||||
================================== Ai Message ==================================
|
||||
@@ -175,11 +308,25 @@ 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)
|
||||
|
||||
{k: v for k, v in snapshot.values.items() if k in ("name", "birthday")}
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
const snapshot = await graph.getState(config);
|
||||
|
||||
const relevantState = {
|
||||
name: snapshot.values.name,
|
||||
birthday: snapshot.values.birthday
|
||||
};
|
||||
console.log(relevantState);
|
||||
```
|
||||
:::
|
||||
|
||||
```
|
||||
{'name': 'LangGraph', 'birthday': 'Jan 17, 2024'}
|
||||
@@ -189,11 +336,21 @@ This makes them easily accessible to downstream nodes (e.g., a node that further
|
||||
|
||||
## 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
|
||||
graph.update_state(config, {"name": "LangGraph (library)"})
|
||||
```
|
||||
:::
|
||||
|
||||
:::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(config, { name: "LangGraph (library)" });
|
||||
```
|
||||
:::
|
||||
|
||||
```
|
||||
{'configurable': {'thread_id': '1',
|
||||
@@ -203,6 +360,7 @@ graph.update_state(config, {"name": "LangGraph (library)"})
|
||||
|
||||
## 6. View the new value
|
||||
|
||||
:::python
|
||||
If you call `graph.get_state`, you can see the new value is reflected:
|
||||
|
||||
``` python
|
||||
@@ -210,6 +368,21 @@ snapshot = graph.get_state(config)
|
||||
|
||||
{k: v for k, v in snapshot.values.items() if k in ("name", "birthday")}
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
If you call `graph.getState`, you can see the new value is reflected:
|
||||
|
||||
```typescript
|
||||
const updatedSnapshot = await graph.getState(config);
|
||||
|
||||
const updatedState = {
|
||||
name: updatedSnapshot.values.name,
|
||||
birthday: updatedSnapshot.values.birthday
|
||||
};
|
||||
console.log(updatedState);
|
||||
```
|
||||
:::
|
||||
|
||||
```
|
||||
{'name': 'LangGraph (library)', 'birthday': 'Jan 17, 2024'}
|
||||
@@ -231,6 +404,7 @@ llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
|
||||
```
|
||||
-->
|
||||
|
||||
:::python
|
||||
```python
|
||||
from typing import Annotated
|
||||
|
||||
@@ -304,8 +478,106 @@ graph_builder.add_edge(START, "chatbot")
|
||||
memory = MemorySaver()
|
||||
graph = graph_builder.compile(checkpointer=memory)
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { ChatAnthropic } from "@langchain/anthropic";
|
||||
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { ToolMessage, BaseMessage } from "@langchain/core/messages";
|
||||
import { z } from "zod";
|
||||
|
||||
import { MemorySaver } from "@langchain/langgraph";
|
||||
import { StateGraph, START, Annotation } from "@langchain/langgraph";
|
||||
import { ToolNode } from "@langchain/langgraph/prebuilt";
|
||||
import { Command, interrupt } from "@langchain/langgraph";
|
||||
|
||||
const llm = new ChatAnthropic({
|
||||
model: "claude-3-5-sonnet-latest",
|
||||
});
|
||||
|
||||
const StateAnnotation = Annotation.Root({
|
||||
messages: Annotation<BaseMessage[]>({
|
||||
reducer: (x, y) => x.concat(y),
|
||||
}),
|
||||
name: Annotation<string>,
|
||||
birthday: Annotation<string>,
|
||||
});
|
||||
|
||||
const humanAssistance = tool(async (input, config) => {
|
||||
const { name, birthday } = input;
|
||||
const toolCallId = config?.toolCall?.id;
|
||||
|
||||
const humanResponse = interrupt({
|
||||
question: "Is this correct?",
|
||||
name: name,
|
||||
birthday: birthday,
|
||||
});
|
||||
|
||||
let verifiedName, verifiedBirthday, response;
|
||||
|
||||
if (humanResponse?.correct?.toLowerCase().startsWith("y")) {
|
||||
verifiedName = name;
|
||||
verifiedBirthday = birthday;
|
||||
response = "Correct";
|
||||
} else {
|
||||
verifiedName = humanResponse?.name || name;
|
||||
verifiedBirthday = humanResponse?.birthday || birthday;
|
||||
response = `Made a correction: ${JSON.stringify(humanResponse)}`;
|
||||
}
|
||||
|
||||
const stateUpdate = {
|
||||
name: verifiedName,
|
||||
birthday: verifiedBirthday,
|
||||
messages: [new ToolMessage({
|
||||
content: response,
|
||||
tool_call_id: toolCallId!
|
||||
})],
|
||||
};
|
||||
|
||||
return new Command({ update: stateUpdate });
|
||||
}, {
|
||||
name: "humanAssistance",
|
||||
description: "Request assistance from a human.",
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
birthday: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
const searchTool = new TavilySearchResults({ maxResults: 2 });
|
||||
const tools = [searchTool, humanAssistance];
|
||||
const llmWithTools = llm.bindTools(tools);
|
||||
|
||||
const chatbot = async (state: typeof StateAnnotation.State) => {
|
||||
const message = await llmWithTools.invoke(state.messages);
|
||||
return { messages: [message] };
|
||||
};
|
||||
|
||||
const shouldContinue = (state: typeof StateAnnotation.State) => {
|
||||
const lastMessage = state.messages[state.messages.length - 1];
|
||||
if ("tool_calls" in lastMessage && lastMessage.tool_calls?.length) {
|
||||
return "tools";
|
||||
}
|
||||
return "__end__";
|
||||
};
|
||||
|
||||
const graphBuilder = new StateGraph(StateAnnotation);
|
||||
graphBuilder.addNode("chatbot", chatbot);
|
||||
|
||||
const toolNode = new ToolNode(tools);
|
||||
graphBuilder.addNode("tools", toolNode);
|
||||
|
||||
graphBuilder.addConditionalEdges("chatbot", shouldContinue);
|
||||
graphBuilder.addEdge("tools", "chatbot");
|
||||
graphBuilder.addEdge(START, "chatbot");
|
||||
|
||||
const memory = new MemorySaver();
|
||||
const graph = graphBuilder.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).
|
||||
@@ -12,18 +12,35 @@ 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.
|
||||
:::
|
||||
|
||||
{!snippets/chat_model_tabs.md!}
|
||||
|
||||
<!---
|
||||
:::python
|
||||
```python
|
||||
from langchain.chat_models import init_chat_model
|
||||
|
||||
llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { initChatModel } from "langchain/chat_models/init";
|
||||
|
||||
const llm = initChatModel("anthropic:claude-3-5-sonnet-latest");
|
||||
```
|
||||
:::
|
||||
-->
|
||||
|
||||
:::python
|
||||
```python
|
||||
from typing import Annotated
|
||||
|
||||
@@ -63,11 +80,62 @@ graph_builder.add_edge(START, "chatbot")
|
||||
memory = MemorySaver()
|
||||
graph = graph_builder.compile(checkpointer=memory)
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
|
||||
import { ChatAnthropic } from "@langchain/anthropic";
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
import { Annotation, StateGraph, START, END } from "@langchain/langgraph";
|
||||
import { MemorySaver } from "@langchain/langgraph";
|
||||
import { ToolNode } from "@langchain/langgraph/prebuilt";
|
||||
import { messagesStateReducer } from "@langchain/langgraph";
|
||||
|
||||
const StateAnnotation = Annotation.Root({
|
||||
messages: Annotation<BaseMessage[]>({
|
||||
reducer: messagesStateReducer,
|
||||
}),
|
||||
});
|
||||
|
||||
const graphBuilder = new StateGraph(StateAnnotation);
|
||||
|
||||
const tool = new TavilySearchResults({ maxResults: 2 });
|
||||
const tools = [tool];
|
||||
const llm = new ChatAnthropic({ model: "claude-3-5-sonnet-latest" });
|
||||
const llmWithTools = llm.bindTools(tools);
|
||||
|
||||
const chatbot = async (state: typeof StateAnnotation.State) => {
|
||||
return { messages: [await llmWithTools.invoke(state.messages)] };
|
||||
};
|
||||
|
||||
graphBuilder.addNode("chatbot", chatbot);
|
||||
|
||||
const toolNode = new ToolNode(tools);
|
||||
graphBuilder.addNode("tools", toolNode);
|
||||
|
||||
const toolsCondition = (state: typeof StateAnnotation.State) => {
|
||||
const lastMessage = state.messages[state.messages.length - 1];
|
||||
if ("tool_calls" in lastMessage && lastMessage.tool_calls?.length) {
|
||||
return "tools";
|
||||
}
|
||||
return END;
|
||||
};
|
||||
|
||||
graphBuilder.addConditionalEdges("chatbot", toolsCondition);
|
||||
graphBuilder.addEdge("tools", "chatbot");
|
||||
graphBuilder.addEdge(START, "chatbot");
|
||||
|
||||
const memory = new MemorySaver();
|
||||
const graph = graphBuilder.compile({ checkpointer: memory });
|
||||
```
|
||||
:::
|
||||
|
||||
## 2. Add steps
|
||||
|
||||
Add steps to your graph. Every step will be checkpointed in its state history:
|
||||
|
||||
:::python
|
||||
``` python
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
events = graph.stream(
|
||||
@@ -89,6 +157,42 @@ for event in events:
|
||||
if "messages" in event:
|
||||
event["messages"][-1].pretty_print()
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
const config = { configurable: { thread_id: "1" } };
|
||||
const events = await graph.stream(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: (
|
||||
"I'm learning LangGraph. " +
|
||||
"Could you do some research on it for me?"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{ ...config, streamMode: "values" }
|
||||
);
|
||||
|
||||
for await (const event of events) {
|
||||
if ("messages" in event) {
|
||||
const lastMessage = event.messages[event.messages.length - 1];
|
||||
console.log(`================================ ${lastMessage._getType()} Message =================================`);
|
||||
console.log(lastMessage.content);
|
||||
if ("tool_calls" in lastMessage && lastMessage.tool_calls?.length) {
|
||||
console.log("Tool Calls:");
|
||||
for (const toolCall of lastMessage.tool_calls) {
|
||||
console.log(` ${toolCall.name} (${toolCall.id})`);
|
||||
console.log(` Args: ${JSON.stringify(toolCall.args)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
```
|
||||
================================ Human Message =================================
|
||||
@@ -123,6 +227,7 @@ Is there any specific aspect of LangGraph you'd like to know more about? I'd be
|
||||
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
|
||||
```
|
||||
|
||||
:::python
|
||||
```python
|
||||
events = graph.stream(
|
||||
{
|
||||
@@ -143,6 +248,41 @@ for event in events:
|
||||
if "messages" in event:
|
||||
event["messages"][-1].pretty_print()
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
const events2 = await graph.stream(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: (
|
||||
"Ya that's helpful. Maybe I'll " +
|
||||
"build an autonomous agent with it!"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{ ...config, streamMode: "values" }
|
||||
);
|
||||
|
||||
for await (const event of events2) {
|
||||
if ("messages" in event) {
|
||||
const lastMessage = event.messages[event.messages.length - 1];
|
||||
console.log(`================================ ${lastMessage._getType()} Message =================================`);
|
||||
console.log(lastMessage.content);
|
||||
if ("tool_calls" in lastMessage && lastMessage.tool_calls?.length) {
|
||||
console.log("Tool Calls:");
|
||||
for (const toolCall of lastMessage.tool_calls) {
|
||||
console.log(` ${toolCall.name} (${toolCall.id})`);
|
||||
console.log(` Args: ${JSON.stringify(toolCall.args)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
```
|
||||
================================ Human Message =================================
|
||||
@@ -159,7 +299,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:
|
||||
@@ -181,6 +321,7 @@ Output is truncated. View as a scrollable element or open in a text editor. Adju
|
||||
|
||||
Now that you have added steps to the chatbot, you can `replay` the full state history to see everything that occurred.
|
||||
|
||||
:::python
|
||||
``` python
|
||||
to_replay = None
|
||||
for state in graph.get_state_history(config):
|
||||
@@ -190,7 +331,24 @@ for state in graph.get_state_history(config):
|
||||
# We are somewhat arbitrarily selecting a specific state based on the number of chat messages in the state.
|
||||
to_replay = state
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
let toReplay = null;
|
||||
const stateHistory = await graph.getStateHistory(config);
|
||||
for await (const state of stateHistory) {
|
||||
console.log("Num Messages: ", state.values.messages.length, "Next: ", 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;
|
||||
}
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
:::python
|
||||
```
|
||||
Num Messages: 8 Next: ()
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -213,6 +371,32 @@ Num Messages: 1 Next: ('chatbot',)
|
||||
Num Messages: 0 Next: ('__start__',)
|
||||
--------------------------------------------------------------------------------
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```
|
||||
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.
|
||||
|
||||
@@ -220,27 +404,74 @@ Checkpoints are saved for every step of the graph. This __spans invocations__ so
|
||||
|
||||
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
|
||||
```python
|
||||
print(to_replay.next)
|
||||
print(to_replay.config)
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
console.log(toReplay.next);
|
||||
console.log(toReplay.config);
|
||||
```
|
||||
:::
|
||||
|
||||
:::python
|
||||
```
|
||||
('tools',)
|
||||
{'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1efd43e3-0c1f-6c4e-8006-891877d65740'}}
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```
|
||||
["tools"]
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": "",
|
||||
"checkpoint_id": "1efd43e3-0c1f-6c4e-8006-891877d65740"
|
||||
}
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
## 4. Load a state from a moment-in-time
|
||||
|
||||
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:
|
||||
event["messages"][-1].pretty_print()
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```typescript
|
||||
// The `checkpoint_id` in the `toReplay.config` corresponds to a state we've persisted to our checkpointer.
|
||||
const timeTravel = await graph.stream(null, { ...toReplay.config, streamMode: "values" });
|
||||
|
||||
for await (const event of timeTravel) {
|
||||
if ("messages" in event) {
|
||||
const lastMessage = event.messages[event.messages.length - 1];
|
||||
console.log(`================================ ${lastMessage._getType()} Message =================================`);
|
||||
console.log(lastMessage.content);
|
||||
if ("tool_calls" in lastMessage && lastMessage.tool_calls?.length) {
|
||||
console.log("Tool Calls:");
|
||||
for (const toolCall of lastMessage.tool_calls) {
|
||||
console.log(` ${toolCall.name} (${toolCall.id})`);
|
||||
console.log(` Args: ${JSON.stringify(toolCall.args)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
```
|
||||
================================== Ai Message ==================================
|
||||
@@ -254,7 +485,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 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:
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
from _scripts.notebook_hooks import _apply_conditional_rendering
|
||||
|
||||
|
||||
CONDITIONAL_RENDERING = """
|
||||
above
|
||||
:::js
|
||||
js-content
|
||||
:::
|
||||
between
|
||||
:::python
|
||||
python-content
|
||||
:::
|
||||
below
|
||||
"""
|
||||
|
||||
|
||||
def test_conditional_rendering() -> None:
|
||||
"""Test logic for conditional rendering of content."""
|
||||
output = _apply_conditional_rendering(CONDITIONAL_RENDERING, "js")
|
||||
assert output.strip() == "above\njs-content\n\nbetween\n\nbelow"
|
||||
output = _apply_conditional_rendering(CONDITIONAL_RENDERING, "python")
|
||||
assert output.strip() == "above\n\nbetween\npython-content\n\nbelow"
|
||||
Reference in New Issue
Block a user