Compare commits

..
Author SHA1 Message Date
Sydney Runkle f1f4d8387f Merge branch 'sr/perms-for-pr' of https://github.com/langchain-ai/langgraph into sr/perms-for-pr 2025-07-02 11:05:19 -04:00
Sydney Runkle fab56ea75a added token 2025-07-02 11:05:13 -04:00
Sydney RunkleandGitHub 0af0462628 Merge branch 'main' into sr/perms-for-pr 2025-07-02 11:00:21 -04:00
Sydney Runkle 532340e6cb use new token 2025-07-02 10:39:41 -04:00
Sydney RunkleandGitHub 000f5c3043 fix[deps]: update lockfiles / deps bounds for internal tools (#5301)
update lockfiles / deps bounds
2025-07-02 10:30:55 -04:00
Sydney RunkleandGitHub b3708bd7f6 ci: add automated uv lock --upgrade workflow (#5307) 2025-07-02 10:10:01 -04:00
Sydney RunkleandGitHub 8271e39e00 dependabot: no kafka (#5306)
* fix list of dirs
* another patch
2025-07-02 13:15:00 +00:00
Sydney RunkleandGitHub 60560ea755 dependabot: fix list of dirs for pip updates (#5305)
fix list of dirs
2025-07-02 13:12:22 +00:00
waqarahmed6095andGitHub e2acfb24cc Update use_stream_react.md (#5304)
Problem of two times heading 
"How to integrate LangGraph into your React application"
2025-07-02 13:10:57 +00:00
Sydney RunkleandGitHub 191192b142 upgrade dependabot scope (#5303) 2025-07-02 09:09:11 -04:00
Josh RogersandGitHub df368bdd30 Updating message types to include all base message fields (#5298) 2025-07-01 15:40:13 -07:00
Josh RogersandGitHub 4ec897033f Update LGP api reference docs (#5297) 2025-07-01 11:45:30 -07:00
c16e42e6d5 fix broken link (#5291)
* fix broken link

* Apply suggestions from code review

Fix link

Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>

---------

Co-authored-by: Lauren Hirata Singh <lauren@langchain.dev>
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-07-01 13:52:26 +00:00
David DuongandGitHub 376469ea90 release(sdk-js): 0.0.88 (#5294) 2025-07-01 14:30:37 +02:00
Tat Dat Duong 7e2af0ce8d release(sdk-js): 0.0.88 2025-07-01 14:21:11 +02:00
Youssef Ahmed Mohamed AbdelrahmanandGitHub 1b205a99cb docs: fix typo in application_structure (#5289) 2025-07-01 12:09:14 +00:00
Sam CrowderandGitHub 0a8ba20f5f docs: remove beta flag on self hosted plane (#5288) 2025-06-30 22:41:05 -04:00
Sam CrowderandGitHub 048cb3584c self hosted control plane no longer in beta (#5286)
* self hosted control plane no longer in beta

* accidental changes
2025-06-30 17:34:18 -07:00
Sam CrowderandGitHub 22c35b7bc8 switch order of MCP methods in API spec (#5287) 2025-06-30 17:33:59 -07:00
David DuongandGitHub f3ed32e611 feat(react): enhance useStream with initialValues, newThreadId, and onStop callback for improved UX (#5111) 2025-07-01 01:53:28 +02:00
Tat Dat Duong 276675b618 Make sure to spread stream values 2025-07-01 01:39:53 +02:00
Tat Dat Duong 882de42996 Fix non-existent assistantId 2025-07-01 01:35:59 +02:00
Tat Dat Duong 70be50f37b Fix typo 2025-07-01 01:33:08 +02:00
Tat Dat Duong 1c7234e9c5 Update README.md 2025-07-01 01:32:27 +02:00
Tat Dat Duong 3d88f75254 Cleanup 2025-07-01 01:19:07 +02:00
Lauren Hirata SinghandGitHub 407abbe9ff Add forum links (#5282) 2025-06-30 16:11:46 -04:00
ccurmeandGitHub 6182cd1dcb prebuilt: release 0.5.2 (#5280) 2025-06-30 15:50:21 -04:00
Lauren Hirata SinghandGitHub 1d276dd753 docs: cronjob nav (#5281) 2025-06-30 15:34:29 -04:00
ccurmeandGitHub a48d8cb69b prebuilt[patch]: import recognized tool message content block types from langchain-core (#5275) 2025-06-30 15:22:19 -04:00
Lauren Hirata SinghandGitHub a05a251caf docs: Fix nav (#5279) 2025-06-30 15:18:43 -04:00
MauritsBrinkmanandTat Dat Duong c7bbb26ac0 test: add useStream onStop callback tests 2025-06-30 16:43:48 +02:00
MauritsBrinkmanandTat Dat Duong ac9b6c416e feat: add onStop callback to useStream for custom stop behavior
Add onStop callback to useStream hook enabling developers to customize
UI behavior when streams are stopped. This is especially useful for
UI messages with loading states that need to show "stopped" status
instead of remaining in infinite loading state.

The callback provides the same mutate function as onCustomEvent for
immediate local state updates, while users can optionally update
server thread state using the threads client.

Example usage:
```typescript
const stream = useStream({
  assistantId: "my-assistant",
  onStop: async ({ mutate }) => {
    // Immediate UI update - stop loading components
    mutate((prev) => ({
      ...prev,
      ui: prev.ui?.map(component =>
        component.props?.isLoading
          ? {
              ...component,
              props: {
                ...component.props,
                isLoading: false,
                isStopped: true
              }
            }
          : component
      )
    }));

    // Optional server thread state update
    if (stream.threadId) {
      await stream.client.threads.updateState(stream.threadId, {
        values: {
          ui: prev.ui // persist stopped state to server
        }
      });
    }
  }
});
```

This is especially useful for cases where gen UI components have loading states,
where we don't want the loading state to persist on cancellation.
2025-06-30 16:43:13 +02:00
MauritsBrinkmanandTat Dat Duong d4b4eebe4a fix(sdk-js): convert SSE classes to factory functions to resolve tree shaking
- Convert BytesLineDecoder and SSEDecoder from classes extending TransformStream to factory functions
- Fixes tree shaking failures that prevented build completion
- Maintains identical API functionality, just removes 'new' keyword usage
- All tests continue to pass

Resolves tree shaking side effect detection issues with TransformStream extension
2025-06-30 16:43:13 +02:00
MauritsBrinkmanandTat Dat Duong f8e1e803e1 docs(react): add documentation and tests for initialValues and newThreadId options
- Document initialValues for cached thread display
- Document newThreadId for optimistic thread creation
- Add comprehensive test coverage for both features
2025-06-30 16:43:12 +02:00
MauritsBrinkmanandTat Dat Duong 141a6af4f7 feat(react): add initialValues option to useStream for cached thread display
Add initialValues parameter to UseStreamOptions to enable immediate display
of cached thread data while official history is being fetched from the server.

This addresses the common use case where applications cache thread data
locally (IndexedDB, localStorage, etc.) and want to show it instantly when
users navigate to existing threads, providing better UX with faster loading.

Key changes:
- Add initialValues?: Partial<StateType> | null to UseStreamOptions interface
- Update values precedence: streamValues > initialValues > historyValues
- Ensure optimisticValues properly override initialValues during submission
- Maintain full backward compatibility with existing API

Example usage:
```typescript
const stream = useStream({
  threadId,
  assistantId: 'my-assistant',
  initialValues: cachedThreadData?.values // Show cached data immediately
});
```

The values flow now follows this priority:
1. Initial load: shows initialValues while history loads
2. During submit: optimisticValues take precedence
3. After server response: official history replaces all
2025-06-30 16:43:12 +02:00
MauritsBrinkmanandTat Dat Duong 8a763ad358 feat(react): add newThreadId option to useStream for optimistic UI
Add optional newThreadId parameter to useStream hook that allows specifying
a thread ID for new thread creation while keeping threadId null. This enables
optimistic UI patterns where developers need to know the thread ID beforehand
for routing/navigation without causing 404 errors from attempting to fetch
non-existent thread history.

Usage:
- Set threadId: null and newThreadId: "predetermined-id"
- Submit message to create thread with specified ID
- Use onThreadId callback to update threadId after creation

This solves the UX problem of having to await thread creation before
enabling optimistic navigation to e.g. /[threadId] routes.
2025-06-30 16:43:12 +02:00
David DuongandGitHub 16d02e63a1 fix: Allow configuring stream mode in useStream.joinStream() (#5146) 2025-06-30 16:37:56 +02:00
David DuongandGitHub 03421c2b04 chore(sdk-js): use embed LGP server for MSW mocking (#5174) 2025-06-30 16:33:47 +02:00
Tat Dat Duong 2508aa45ea Use published package 2025-06-30 13:51:32 +02:00
Sam CrowderandGitHub 9e035264f8 slightly more explanation when we say dont use in serverless (#5245)
slightly more explanation
2025-06-29 21:59:48 -04:00
joaquin-borggio-lcandGitHub 84e14f47bf docs: Add pre-req for egress to control plane (#5241)
added pre-req for egress
2025-06-27 14:18:39 -07:00
Lauren Hirata SinghandGitHub 83bbe42eab docs: Nav reorg (#5236)
* docs: Nav consolidation

* nav

* reorg

* fix links

* fix

* prebuilts

* fix spelling

* reorg

* reorg
2025-06-27 14:06:26 -04:00
David DuongandGitHub 70894af72c fix(sdk-js): avoid stale client when fetching history (#5240) 2025-06-27 20:04:41 +02:00
Tat Dat Duong e466327524 Bump to 0.0.87 2025-06-27 20:03:03 +02:00
Tat Dat Duong b6655fe083 Use the client hash only in useEffect 2025-06-27 19:59:21 +02:00
Tat Dat Duong 9b7cc1c82e fix(sdk-js): avoid stale client when fetching history 2025-06-27 19:51:49 +02:00
Nuno CamposandGitHub 80d6bddd1b Fix deadlock in SqliteStore (#5234) 2025-06-27 09:03:57 -07:00
Nuno Campos c406aede96 Fix deadlock in SqliteStore
- If setup wasnt called separately _cursor() and setup() would deadlock
- The call to setup() in _cursor() should be outside the lock block, as setup() also acquires the lock and re-checks the setup flag
2025-06-27 08:57:35 -07:00
Eugene YurtsevandGitHub 96bfc8bad9 docs: cross links for functional api (#5231)
add cross-links
2025-06-27 11:31:18 -04:00
Tat Dat Duong 9a0cee5cd0 chore(sdk-js): use embed LGP server for MSW mocking 2025-06-24 02:14:19 +02:00
bracesproul 78bef6bc0c formatting 2025-06-19 11:20:17 -07:00
bracesproul 7bd364c457 fix: Allow configuring stream mode in useStream.joinStream() 2025-06-19 11:14:59 -07:00
57 changed files with 3501 additions and 3945 deletions
+3 -3
View File
@@ -10,6 +10,6 @@ contact_links:
- name: Show and tell
about: Show what you built with LangChain
url: https://github.com/langchain-ai/langgraph/discussions/categories/show-and-tell
- name: Slack
url: https://www.langchain.com/join-community
about: General community discussions
- name: LangChain Forum
url: https://forum.langchain.com/
about: General community discussions and support
+12 -5
View File
@@ -1,11 +1,18 @@
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# and
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directories:
- "libs/checkpoint"
- "libs/checkpoint-postgres"
- "libs/checkpoint-sqlite"
- "libs/cli"
- "libs/langgraph"
- "libs/prebuilt"
- "libs/sdk-py"
schedule:
interval: "weekly"
+8 -4
View File
@@ -22,6 +22,7 @@ jobs:
outputs:
python: ${{ steps.filter.outputs.python }}
sdk-js: ${{ steps.filter.outputs.sdk-js }}
deps: ${{ steps.filter.outputs.deps }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
@@ -38,6 +39,9 @@ jobs:
- 'libs/prebuilt/**'
sdk-js:
- 'libs/sdk-js/**'
deps:
- '**/pyproject.toml'
- '**/uv.lock'
lint:
needs: changes
@@ -55,7 +59,7 @@ jobs:
"libs/prebuilt",
]
if: needs.changes.outputs.python == 'true'
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.deps == 'true'
uses: ./.github/workflows/_lint.yml
with:
working-directory: ${{ matrix.working-directory }}
@@ -74,7 +78,7 @@ jobs:
"libs/checkpoint-postgres",
"libs/prebuilt",
]
if: needs.changes.outputs.python == 'true'
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.deps == 'true'
uses: ./.github/workflows/_test.yml
with:
working-directory: ${{ matrix.working-directory }}
@@ -83,7 +87,7 @@ jobs:
# NOTE: we're testing langgraph separately because it requires a different matrix
test-langgraph:
needs: changes
if: needs.changes.outputs.python == 'true'
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.deps == 'true'
name: "cd libs/langgraph"
uses: ./.github/workflows/_test_langgraph.yml
secrets: inherit
@@ -140,7 +144,7 @@ jobs:
integration-test:
needs: changes
if: needs.changes.outputs.python == 'true'
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.deps == 'true'
name: CLI integration test
uses: ./.github/workflows/_integration_test.yml
secrets: inherit
+49
View File
@@ -0,0 +1,49 @@
name: lockfile upgrades
on:
schedule:
# run at midnight every Sunday
- cron: '0 0 * * 0'
# allow manual triggering
workflow_dispatch:
pull_request:
branches:
- main
permissions:
contents: write
pull-requests: write
jobs:
upgrade-dependencies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up uv
uses: astral-sh/setup-uv@v6
with:
# use minimum supported Python version
python-version: "3.9"
enable-cache: true
cache-suffix: "uv-lock-upgrade"
- name: Run uv lock --upgrade in all Python packages
run: make lock-upgrade
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.LANGGRAPH_WRITE_TOKEN }}
commit-message: "chore: upgrade dependencies with `uv lock --upgrade`"
title: "chore: upgrade dependencies with `uv lock --upgrade`"
body: |
This PR updates the dependencies in all Python packages using `uv lock --upgrade`.
This is an automated PR created by the "lockfile upgrades" workflow.
branch: deps/uv-lock-upgrade
base: main
delete-branch: true
labels: |
dependencies
+10
View File
@@ -47,6 +47,16 @@ lock:
fi; \
done
# Lock all projects and upgrade dependencies
.PHONY: lock-upgrade
lock-upgrade:
@for dir in $(LIBS_DIRS); do \
if [ -f $$dir/Makefile ]; then \
echo "Running lock-upgrade in $$dir"; \
(cd $$dir && uv lock --upgrade); \
fi; \
done
# Test all projects
.PHONY: test
test:
+1 -1
View File
@@ -63,7 +63,7 @@ LangGraph provides low-level supporting infrastructure for *any* long-running, s
While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
- [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
- [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
- [LangChain](https://python.langchain.com/docs/introduction/) Provides integrations and composable components to streamline LLM application development.
> [!NOTE]
+1 -1
View File
@@ -22,7 +22,7 @@ build-prebuilt:
uv run python -m _scripts.third_party_page.create_third_party_page stats.yml docs/agents/prebuilt.md --language python
build-docs: build-typedoc build-prebuilt
TARGET_LANGUAGE=js uv run python -m mkdocs build --clean -f mkdocs.yml --strict
uv run python -m mkdocs build --clean -f mkdocs.yml --strict
llms-text:
uv run python -m _scripts.generate_llms_text docs/llms-full.txt
+1 -83
View File
@@ -4,11 +4,9 @@ import argparse
import requests
from langchain_anthropic import ChatAnthropic
from textwrap import dedent
# Load reference TypeScript snippets
URL = "https://gist.githubusercontent.com/dqbd/b35d49e2ceec80e654fe1c5ab61ec477/raw/f4768aeedb67628190a4e06d063a938afc8e7672/snippets.md"
URL = "https://gist.githubusercontent.com/eyurtsev/e7486731415463a9bc5b4682358859c8/raw/b5a5fda9c7e3387cfcb781f25082814d43675d50/gistfile1.txt"
response = requests.get(URL)
response.raise_for_status()
reference_snippets = response.text
@@ -16,80 +14,6 @@ reference_snippets = response.text
# Initialize model
model = ChatAnthropic(model="claude-sonnet-4-0", max_tokens=64_000)
FLUENT_INTERFACE_PROMPT = (
"CRITICAL: Always use method chaining (fluent interface) for StateGraph operations in TypeScript. "
"Never create separate variables for the graph builder or call methods individually. "
"The fluent interface provides better type safety and is the preferred pattern.\n\n"
"CORRECT examples with fluent interface:\n"
+ dedent(
"""
```typescript
const graph = new StateGraph(MyState)
.addNode('node1', node1)
.addNode('node2', node2)
.addEdge(START, 'node1')
.addEdge('node1', 'node2')
.addEdge('node2', END)
.compile()
```
```typescript
const graph = new StateGraph(MyState)
.addNode('chatbot', chatbot)
.addEdge(START, 'chatbot')
.addEdge('chatbot', END)
.compile()
```
```typescript
const graph = new StateGraph(MyState)
.addNode('chatbot', chatbot)
.addEdge(START, 'chatbot')
.addEdge('chatbot', END)
.compile()
```
"""
)
+ "\n"
+ "INCORRECT examples to avoid:\n"
+ dedent(
"""
```typescript
// WRONG: Creating separate builder variable
const graphBuilder = new StateGraph(MyState)
graphBuilder.addNode('node1', node1)
graphBuilder.addEdge(START, 'node1')
const graph = graphBuilder.compile()
```
```typescript
// WRONG: Using Python-style method names
const workflow = new StateGraph(MyState)
workflow.add_node('node1', node1)
workflow.add_edge(START, 'node1')
const graph = workflow.compile()
```
```typescript
// WRONG: Calling methods individually
const graphBuilder = new StateGraph(MyState)
graphBuilder.addNode('chatbot', chatbot)
graphBuilder.addEdge(START, 'chatbot')
graphBuilder.addEdge('chatbot', END)
const graph = graphBuilder.compile()
```
"""
)
+ "\n"
+ "Key rules:\n"
+ "- Always chain methods directly on the StateGraph constructor\n"
+ "- Use camelCase method names (addNode, addEdge, not add_node, add_edge)\n"
+ "- Always end with .compile()\n"
+ "- Never store the builder in a separate variable\n"
)
TRANSLATION_PROMPT = (
"You are a helpful assistant that translates Python-based technical "
"documentation written in Markdown to equivalent TypeScript-based documentation. "
@@ -108,12 +32,6 @@ TRANSLATION_PROMPT = (
"the translation. "
"Use the reference TypeScript snippets as guidance whenever possible to "
"maintain alignment with existing conventions.\n\n"
"IMPORTANT REQUIREMENTS:\n"
"- Use Zod for state definition for StateGraph. Avoid using Annotation since it will be deprecated in the future.\n"
"- ALWAYS use fluent interface (method chaining) for StateGraph operations - this is CRITICAL\n"
"- Never create separate variables for graph builders\n"
"- Always chain methods directly on the StateGraph constructor and end with .compile()\n\n"
f"{FLUENT_INTERFACE_PROMPT}\n\n"
f"Here are the reference TypeScript snippets:\n\n{reference_snippets}\n\n"
)
-51
View File
@@ -1,51 +0,0 @@
import * as path from "node:path";
import * as fs from "node:fs/promises";
import * as url from "node:url";
const mdPath = url.fileURLToPath(
new URL("./add_translation_js_ref_updated.md", import.meta.url)
);
const extractedDir = url.fileURLToPath(
new URL(
"../../../oap-langgraphjs-tools-agent/src/add_transaction_js",
import.meta.url
)
);
const files = (await fs.readdir(extractedDir, { withFileTypes: true })).sort(
(a, b) => {
const aInt = Number.parseInt(a.name.split(".")[0], 10);
const bInt = Number.parseInt(b.name.split(".")[0], 10);
return aInt - bInt;
}
);
let count = 0;
let lines = [];
for (let file of files) {
if (file.isDirectory() || !file.name.endsWith(".mts")) continue;
count += 1;
const content = await fs.readFile(path.resolve(extractedDir, file.name), {
encoding: "utf-8",
});
lines = lines.concat(
content
.split("\n")
.reduce((acc, line) => {
if (line.trimStart().startsWith("// ```")) acc.push([]);
acc.at(-1)?.push(line);
return acc;
}, [])
.map((i) => {
const tag = i[0].trimStart().slice("// ```".length);
return ["```" + tag, ...i.slice(1), "```"].join("\n");
})
);
}
await fs.writeFile(mdPath, lines.join("\n\n"));
-46
View File
@@ -1,46 +0,0 @@
import * as fs from "node:fs/promises";
import * as path from "node:path";
import * as url from "node:url";
const mdPath = url.fileURLToPath(
new URL("./add_translation_js_ref.md", import.meta.url)
);
const extractedDir = url.fileURLToPath(
new URL(
"../../../oap-langgraphjs-tools-agent/src/add_transaction_js",
import.meta.url
)
);
await fs.mkdir(extractedDir, { recursive: true });
const md = (await fs.readFile(mdPath, { encoding: "utf-8" })).split("\n");
const chunks = [];
let current = [];
for (let line of md) {
if (line.trimStart().startsWith("```")) {
if (current.length > 0) {
chunks.push(current.join("\n"));
current = [];
} else {
current.push("// " + line.trimStart());
}
} else if (current.length > 0) {
current.push(line);
}
}
if (current.length > 0) {
chunks.push(current.join("\n"));
}
for (let i = 0; i < chunks.length; i += 1) {
await fs.writeFile(path.resolve(extractedDir, `${i}.mts`), chunks[i], {
encoding: "utf-8",
});
}
console.log("finished");
+7 -11
View File
@@ -15,8 +15,8 @@ from mkdocs.structure.files import Files, File
from mkdocs.structure.pages import Page
from _scripts.generate_api_reference_links import update_markdown_with_imports
from _scripts.link_map import JS_LINK_MAP
from _scripts.notebook_convert import convert_notebook
from _scripts.link_map import JS_LINK_MAP
logger = logging.getLogger(__name__)
logging.basicConfig()
@@ -111,6 +111,7 @@ REDIRECT_MAP = {
"concepts/v0-human-in-the-loop.md": "concepts/human-in-the-loop.md",
"how-tos/index.md": "index.md",
"tutorials/introduction.ipynb": "concepts/why-langgraph.md",
"agents/deployment.md": "tutorials/langgraph-platform/local-server.md",
# deployment redirects
"how-tos/deploy-self-hosted.md": "cloud/deployment/self_hosted_data_plane.md",
"concepts/self_hosted.md": "concepts/langgraph_self_hosted_data_plane.md",
@@ -306,12 +307,6 @@ def _highlight_code_blocks(markdown: str) -> str:
return markdown
TARGET_LANGUAGE = os.environ.get("TARGET_LANGUAGE", "python")
if TARGET_LANGUAGE not in {"python", "js"}:
raise ValueError(f"TARGET_LANGUAGE must be 'python' or 'js', got {TARGET_LANGUAGE}")
def _on_page_markdown_with_config(
markdown: str,
page: Page,
@@ -334,15 +329,16 @@ def _on_page_markdown_with_config(
markdown = _highlight_code_blocks(markdown)
# Apply conditional rendering for code blocks
markdown = _apply_conditional_rendering(markdown, TARGET_LANGUAGE)
if TARGET_LANGUAGE == "js":
target_language = kwargs.get("target_language", "python")
markdown = _apply_conditional_rendering(markdown, target_language)
if target_language == "js":
markdown = _resolve_cross_references(markdown, JS_LINK_MAP)
elif TARGET_LANGUAGE == "python":
elif target_language == "python":
# Via a dedicated plugin
pass
else:
raise ValueError(
f"Unsupported target language: {TARGET_LANGUAGE}. "
f"Unsupported target language: {target_language}. "
"Supported languages are 'python' and 'js'."
)
-92
View File
@@ -1,92 +0,0 @@
---
search:
boost: 2
tags:
- agent
hide:
- tags
---
# Deployment
To deploy your LangGraph agent, create and configure a LangGraph app. This setup supports both local development and production deployments.
Features:
* 🖥️ Local server for development
* 🧩 Studio Web UI for visual debugging
* ☁️ Cloud and 🔧 self-hosted deployment options
* 📊 LangSmith integration for tracing and observability
!!! info "Requirements"
- ✅ You **must** have a [LangSmith account](https://www.langchain.com/langsmith). You can sign up for **free** and get started with the free tier.
## Create a LangGraph app
```bash
pip install -U "langgraph-cli[inmem]"
langgraph new path/to/your/app --template new-langgraph-project-python
```
This will create an empty LangGraph project. You can modify it by replacing the code in `src/agent/graph.py` with your agent code. For example:
```python
from langgraph.prebuilt import create_react_agent
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
graph = create_react_agent(
model="anthropic:claude-3-7-sonnet-latest",
tools=[get_weather],
prompt="You are a helpful assistant"
)
```
### Install dependencies
In the root of your new LangGraph app, install the dependencies in `edit` mode so your local changes are used by the server:
```shell
pip install -e .
```
### Create an `.env` file
You will find a `.env.example` in the root of your new LangGraph app. Create
a `.env` file in the root of your new LangGraph app and copy the contents of the `.env.example` file into it, filling in the necessary API keys:
```bash
LANGSMITH_API_KEY=lsv2...
ANTHROPIC_API_KEY=sk-
```
## Launch LangGraph server locally
```shell
langgraph dev
```
This will start up the LangGraph API server locally. If this runs successfully, you should see something like:
> Ready!
>
> - API: [http://localhost:2024](http://localhost:2024/)
>
> - Docs: http://localhost:2024/docs
>
> - LangGraph Studio Web UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
See this [tutorial](https://langchain-ai.github.io/langgraph/tutorials/langgraph-platform/local-server/) to learn more about running LangGraph app locally.
## LangGraph Studio Web UI
LangGraph Studio Web is a specialized UI that you can connect to LangGraph API server to enable visualization, interaction, and debugging of your application locally. Test your graph in the LangGraph Studio Web UI by visiting the URL provided in the output of the `langgraph dev` command.
> - LangGraph Studio Web UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
## Deployment
Once your LangGraph app is running locally, you can deploy it using LangGraph Platform. Refer to the [deployment options guide](../concepts/deployment_options.md) for detailed instructions on all supported deployment models.
+2 -2
View File
@@ -8,9 +8,9 @@ hide:
- tags
---
# Agent development with LangGraph
# Agent development using prebuilt components
**LangGraph** provides both low-level primitives and high-level prebuilt components for building agent-based applications. This section focuses on the **prebuilt**, **reusable** components designed to help you construct agentic systems quickly and reliably—without the need to implement orchestration, memory, or human feedback handling from scratch.
LangGraph provides both low-level primitives and high-level prebuilt components for building agent-based applications. This section focuses on the prebuilt, ready-to-use components designed to help you construct agentic systems quickly and reliably—without the need to implement orchestration, memory, or human feedback handling from scratch.
## What is an agent?
@@ -3,7 +3,7 @@
Before deploying, review the [conceptual guide for the Self-Hosted Control Plane](../../concepts/langgraph_self_hosted_control_plane.md) deployment option.
!!! info "Important"
The Self-Hosted Control Plane deployment option is currently in beta stage and requires an [Enterprise](../../concepts/plans.md) plan.
The Self-Hosted Control Plane deployment option requires an [Enterprise](../../concepts/plans.md) plan.
## Prerequisites
@@ -3,7 +3,7 @@
Before deploying, review the [conceptual guide for the Self-Hosted Data Plane](../../concepts/langgraph_self_hosted_data_plane.md) deployment option.
!!! info "Important"
The Self-Hosted Data Plane deployment option is currently in beta stage and requires an [Enterprise](../../concepts/plans.md) plan.
The Self-Hosted Data Plane deployment option requires an [Enterprise](../../concepts/plans.md) plan.
## Prerequisites
@@ -15,11 +15,15 @@ Before deploying, review the [conceptual guide for the Self-Hosted Data Plane](.
### Prerequisites
1. `KEDA` is installed on your cluster.
helm repo add kedacore https://kedacore.github.io/charts
helm repo add kedacore https://kedacore.github.io/charts
helm install keda kedacore/keda --namespace keda --create-namespace
1. A valid `Ingress` controller is installed on your cluster.
1. You have slack space in your cluster for multiple deployments. `Cluster-Autoscaler` is recommended to automatically provision new nodes.
1. You will need to enable egress to two control plane URLs. The listener polls these endpoints for deployments:
https://api.host.langchain.com
https://api.smith.langchain.com
### Setup
+69 -1
View File
@@ -1,4 +1,4 @@
How to integrate LangGraph into your React application# How to integrate LangGraph into your React application
# How to integrate LangGraph into your React application
!!! info "Prerequisites"
@@ -503,6 +503,74 @@ const handleSubmit = (text: string) => {
};
```
### Cached Thread Display
Use the `initialValues` option to display cached thread data immediately while the history is being loaded from the server. This improves user experience by showing cached data instantly when navigating to existing threads.
```tsx
import { useStream } from "@langchain/langgraph-sdk/react";
const CachedThreadExample = ({ threadId, cachedThreadData }) => {
const stream = useStream({
apiUrl: "http://localhost:2024",
assistantId: "agent",
threadId,
// Show cached data immediately while history loads
initialValues: cachedThreadData?.values,
messagesKey: "messages",
});
return (
<div>
{stream.messages.map((message) => (
<div key={message.id}>{message.content as string}</div>
))}
</div>
);
};
```
### Optimistic Thread Creation
Use the `threadId` option in `submit` function to enable optimistic UI patterns where you need to know the thread ID before the thread is actually created.
```tsx
import { useState } from "react";
import { useStream } from "@langchain/langgraph-sdk/react";
const OptimisticThreadExample = () => {
const [threadId, setThreadId] = useState<string | null>(null);
const [optimisticThreadId] = useState(() => crypto.randomUUID());
const stream = useStream({
apiUrl: "http://localhost:2024",
assistantId: "agent",
threadId,
onThreadId: setThreadId, // (3) Updated after thread has been created.
messagesKey: "messages",
});
const handleSubmit = (text: string) => {
// (1) Perform a soft navigation to /threads/${optimisticThreadId}
// without waiting for thread creation.
window.history.pushState({}, "", `/threads/${optimisticThreadId}`);
// (2) Submit message to create thread with the predetermined ID.
stream.submit(
{ messages: [{ type: "human", content: text }] },
{ threadId: optimisticThreadId }
);
};
return (
<div>
<p>Thread ID: {threadId ?? optimisticThreadId}</p>
{/* Rest of component */}
</div>
);
};
```
### TypeScript
The `useStream()` hook is friendly for apps written in TypeScript and you can specify types for the state to get better type safety and IDE support.
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -395,7 +395,7 @@ The LangGraph CLI requires a JSON configuration file that follows this [schema](
=== "Python"
Start LangGraph API server. For local testing, requires a LangSmith API key with access to LangGraph Platform closed beta. Requires a license key for production use.
Start LangGraph API server. For local testing, requires a LangSmith API key with access to LangGraph Platform. Requires a license key for production use.
**Usage**
@@ -422,7 +422,7 @@ The LangGraph CLI requires a JSON configuration file that follows this [schema](
=== "JS"
Start LangGraph API server. For local testing, requires a LangSmith API key with access to LangGraph Platform closed beta. Requires a license key for production use.
Start LangGraph API server. For local testing, requires a LangSmith API key with access to LangGraph Platform. Requires a license key for production use.
**Usage**
+2 -2
View File
@@ -48,7 +48,7 @@ Below are examples of directory structures for Python and JavaScript application
│ ├── utils # utilities for your graph
│ │ ├── __init__.py
│ │ ├── tools.py # tools for your graph
│ │ ├── nodes.py # node functions for you graph
│ │ ├── nodes.py # node functions for your graph
│ │ └── state.py # state definition of your graph
│ ├── __init__.py
│ └── agent.py # code for constructing your graph
@@ -64,7 +64,7 @@ Below are examples of directory structures for Python and JavaScript application
├── src # all project code lies within here
│ ├── utils # optional utilities for your graph
│ │ ├── tools.ts # tools for your graph
│ │ ├── nodes.ts # node functions for you graph
│ │ ├── nodes.ts # node functions for your graph
│ │ └── state.ts # state definition of your graph
│ └── agent.ts # code for constructing your graph
├── package.json # package dependencies
+4 -4
View File
@@ -18,9 +18,9 @@ There are 4 main options for deploying with the [LangGraph Platform](langgraph_p
1. [Cloud SaaS](#cloud-saas)
1. [Self-Hosted Data Plane<sup>(Beta)</sup>](#self-hosted-data-plane)
1. [Self-Hosted Data Plane](#self-hosted-data-plane)
1. [Self-Hosted Control Plane<sup>(Beta)</sup>](#self-hosted-control-plane)
1. [Self-Hosted Control Plane](#self-hosted-control-plane)
1. [Standalone Container](#standalone-container)
@@ -50,7 +50,7 @@ For more information, please see:
## Self-Hosted Data Plane
!!! info "Important"
The Self-Hosted Data Plane deployment option is currently in beta stage and requires an [Enterprise](../concepts/plans.md) plan.
The Self-Hosted Data Plane deployment option requires an [Enterprise](../concepts/plans.md) plan.
The [Self-Hosted Data Plane](./langgraph_self_hosted_data_plane.md) deployment option is a "hybrid" model for deployment where we manage the [control plane](./langgraph_control_plane.md) in our cloud and you manage the [data plane](./langgraph_data_plane.md) in your cloud. This option provides a way to securely manage your data plane infrastructure, while offloading control plane management to us.
@@ -66,7 +66,7 @@ For more information, please see:
## Self-Hosted Control Plane
!!! info "Important"
The Self-Hosted Control Plane deployment option is currently in beta stage and requires an [Enterprise](../concepts/plans.md) plan.
The Self-Hosted Control Plane deployment option requires an [Enterprise](../concepts/plans.md) plan.
The [Self-Hosted Control Plane](./langgraph_self_hosted_control_plane.md) deployment option is a fully self-hosted model for deployment where you manage the [control plane](./langgraph_control_plane.md) and [data plane](./langgraph_data_plane.md) in your cloud. This option gives you full control and responsibility of the control plane and data plane infrastructure.
+1 -1
View File
@@ -47,7 +47,7 @@ LangGraph is a stateful, orchestration framework that brings added control to ag
No. LangGraph Platform is proprietary software.
There is a free, self-hosted version of LangGraph Platform with access to basic features. The Cloud SaaS deployment option is free while in beta, but will eventually be a paid service. We will always give ample notice before charging for a service and reward our early adopters with preferential pricing. The Self-Hosted deployment options are paid services. [Contact our sales team](https://www.langchain.com/contact-sales) to learn more.
There is a free, self-hosted version of LangGraph Platform with access to basic features. The Cloud SaaS deployment option and the Self-Hosted deployment options are paid services. [Contact our sales team](https://www.langchain.com/contact-sales) to learn more.
For more information, see our [LangGraph Platform pricing page](https://www.langchain.com/pricing-langgraph-platform).
+14 -12
View File
@@ -18,10 +18,21 @@ The Functional API uses two key building blocks:
This provides a minimal abstraction for building workflows with state management and streaming.
!!! tip
!!! tip
For information on how to use the functional API, see [Use Functional API](../how-tos/use-functional-api.md).
## Functional API vs. Graph API
For users who prefer a more declarative approach, LangGraph's [Graph API](./low_level.md) allows you to define workflows using a Graph paradigm. Both APIs share the same underlying runtime, so you can use them together in the same application.
Here are some key differences:
- **Control flow**: The Functional API does not require thinking about graph structure. You can use standard Python constructs to define workflows. This will usually trim the amount of code you need to write.
- **Short-term memory**: The **GraphAPI** requires declaring a [**State**](./low_level.md#state) and may require defining [**reducers**](./low_level.md#reducers) to manage updates to the graph state. `@entrypoint` and `@tasks` do not require explicit state management as their state is scoped to the function and is not shared across functions.
- **Checkpointing**: Both APIs generate and use checkpoints. In the **Graph API** a new checkpoint is generated after every [superstep](./low_level.md). In the **Functional API**, when tasks are executed, their results are saved to an existing checkpoint associated with the given entrypoint instead of creating a new checkpoint.
- **Visualization**: The Graph API makes it easy to visualize the workflow as a graph which can be useful for debugging, understanding the workflow, and sharing with others. The Functional API does not support visualization as the graph is dynamically generated during runtime.
For users who prefer a more declarative approach, LangGraph's [Graph API](./low_level.md) allows you to define workflows using a Graph paradigm. Both APIs share the same underlying runtime, so you can use them together in the same application.
Please see the [Functional API vs. Graph API](#functional-api-vs-graph-api) section for a comparison of the two paradigms.
## Example
@@ -532,15 +543,6 @@ While different runs of a workflow can produce different results, resuming a **s
Idempotency ensures that running the same operation multiple times produces the same result. This helps prevent duplicate API calls and redundant processing if a step is rerun due to a failure. Always place API calls inside **tasks** functions for checkpointing, and design them to be idempotent in case of re-execution. Re-execution can occur if a **task** starts, but does not complete successfully. Then, if the workflow is resumed, the **task** will run again. Use idempotency keys or verify existing results to avoid duplication.
## Functional API vs. Graph API
The **Functional API** and the [Graph APIs (StateGraph)](./low_level.md#stategraph) provide two different paradigms to create applications with LangGraph. Here are some key differences:
- **Control flow**: The Functional API does not require thinking about graph structure. You can use standard Python constructs to define workflows. This will usually trim the amount of code you need to write.
- **Short-term memory**: The **GraphAPI** requires declaring a [**State**](./low_level.md#state) and may require defining [**reducers**](./low_level.md#reducers) to manage updates to the graph state. `@entrypoint` and `@tasks` do not require explicit state management as their state is scoped to the function and is not shared across functions.
- **Checkpointing**: Both APIs generate and use checkpoints. In the **Graph API** a new checkpoint is generated after every [superstep](./low_level.md). In the **Functional API**, when tasks are executed, their results are saved to an existing checkpoint associated with the given entrypoint instead of creating a new checkpoint.
- **Visualization**: The Graph API makes it easy to visualize the workflow as a graph which can be useful for debugging, understanding the workflow, and sharing with others. The Functional API does not support visualization as the graph is dynamically generated during runtime.
## Common Pitfalls
### Handling side effects
@@ -3,7 +3,7 @@
There are two versions of the self-hosted deployment: [Self-Hosted Data Plane](./deployment_options.md#self-hosted-data-plane) and [Self-Hosted Control Plane](./deployment_options.md#self-hosted-control-plane).
!!! info "Important"
The Self-Hosted Control Plane deployment option is currently in beta stage and requires an [Enterprise](plans.md) plan.
The Self-Hosted Control Plane deployment option requires an [Enterprise](plans.md) plan.
## Requirements
@@ -8,7 +8,7 @@ search:
There are two versions of the self-hosted deployment: [Self-Hosted Data Plane](./deployment_options.md#self-hosted-data-plane) and [Self-Hosted Control Plane](./deployment_options.md#self-hosted-control-plane).
!!! info "Important"
The Self-Hosted Data Plane deployment option is currently in beta stage and requires an [Enterprise](plans.md) plan.
The Self-Hosted Data Plane deployment option requires an [Enterprise](plans.md) plan.
## Requirements
@@ -19,7 +19,7 @@ The Standalone Container deployment option is the least restrictive model for de
!!! warning
LangGraph Platform should not be deployed in serverless environments.
LangGraph Platform should not be deployed in serverless environments. Scale to zero may cause task loss and scaling up will not work reliably.
## Architecture
+3 -535
View File
@@ -9,7 +9,6 @@ search:
At its core, LangGraph models agent workflows as graphs. You define the behavior of your agents using three key components:
:::python
1. [`State`](#state): A shared data structure that represents the current snapshot of your application. It can be any Python type, but is typically a `TypedDict` or Pydantic `BaseModel`.
2. [`Nodes`](#nodes): Python functions that encode the logic of your agents. They receive the current `State` as input, perform some computation or side-effect, and return an updated `State`.
@@ -17,17 +16,6 @@ At its core, LangGraph models agent workflows as graphs. You define the behavior
3. [`Edges`](#edges): Python functions that determine which `Node` to execute next based on the current `State`. They can be conditional branches or fixed transitions.
By composing `Nodes` and `Edges`, you can create complex, looping workflows that evolve the `State` over time. The real power, though, comes from how LangGraph manages that `State`. To emphasize: `Nodes` and `Edges` are nothing more than Python functions - they can contain an LLM or just good ol' Python code.
:::
:::js
1. [`State`](#state): A shared data structure that represents the current snapshot of your application. It can be any TypeScript type, but is typically a Zod schema or TypeScript interface.
2. [`Nodes`](#nodes): TypeScript functions that encode the logic of your agents. They receive the current `State` as input, perform some computation or side-effect, and return an updated `State`.
3. [`Edges`](#edges): TypeScript functions that determine which `Node` to execute next based on the current `State`. They can be conditional branches or fixed transitions.
By composing `Nodes` and `Edges`, you can create complex, looping workflows that evolve the `State` over time. The real power, though, comes from how LangGraph manages that `State`. To emphasize: `Nodes` and `Edges` are nothing more than TypeScript functions - they can contain an LLM or just good ol' TypeScript code.
:::
In short: _nodes do the work, edges tell what to do next_.
@@ -43,51 +31,21 @@ The `StateGraph` class is the main graph class to use. This is parameterized by
To build your graph, you first define the [state](#state), you then add [nodes](#nodes) and [edges](#edges), and then you compile it. What exactly is compiling your graph and why is it needed?
Compiling is a pretty simple step. It provides a few basic checks on the structure of your graph (no orphaned nodes, etc). It is also where you can specify runtime args like [checkpointers](./persistence.md) and breakpoints.
:::python
You compile your graph by just calling the `.compile` method:
Compiling is a pretty simple step. It provides a few basic checks on the structure of your graph (no orphaned nodes, etc). It is also where you can specify runtime args like [checkpointers](./persistence.md) and breakpoints. You compile your graph by just calling the `.compile` method:
```python
graph = graph_builder.compile(...)
```
:::
:::js
You compile your graph by just calling the `.compile()` method:
```typescript
const graph = new StateGraph(State)
.addNode("node1", node1)
.addNode("node2", node2)
.addEdge(START, "node1")
.addEdge("node1", "node2")
.addEdge("node2", END)
.compile();
```
:::
You **MUST** compile your graph before you can use it.
## State
:::python
The first thing you do when you define a graph is define the `State` of the graph. The `State` consists of the [schema of the graph](#schema) as well as [`reducer` functions](#reducers) which specify how to apply updates to the state. The schema of the `State` will be the input schema to all `Nodes` and `Edges` in the graph, and can be either a `TypedDict` or a `Pydantic` model. All `Nodes` will emit updates to the `State` which are then applied using the specified `reducer` function.
:::
:::js
The first thing you do when you define a graph is define the `State` of the graph. The `State` consists of the [schema of the graph](#schema) as well as [`reducer` functions](#reducers) which specify how to apply updates to the state. The schema of the `State` will be the input schema to all `Nodes` and `Edges` in the graph, and can be either a Zod schema or a TypeScript interface. All `Nodes` will emit updates to the `State` which are then applied using the specified `reducer` function.
:::
### Schema
:::python
The main documented way to specify the schema of a graph is by using `TypedDict`. However, we also support [using a Pydantic BaseModel](../how-tos/graph-api.ipynb#use-pydantic-models-for-graph-state) as your graph state to add **default values** and additional data validation.
:::
:::js
The main documented way to specify the schema of a graph is by using Zod schemas. However, we also support [using TypeScript interfaces](../how-tos/graph-api.ipynb#use-typescript-interfaces-for-graph-state) as your graph state to add **default values** and additional data validation.
:::
By default, the graph will have the same input and output schemas. If you want to change this, you can also specify explicit input and output schemas directly. This is useful when you have a lot of keys, and some are explicitly for input and others for output. See the [guide here](../how-tos/graph-api.ipynb#define-input-and-output-schemas) for how to use.
@@ -104,7 +62,6 @@ It is also possible to define explicit input and output schemas for a graph. In
Let's look at an example:
:::python
```python
class InputState(TypedDict):
user_input: str
@@ -145,77 +102,12 @@ graph = builder.compile()
graph.invoke({"user_input":"My"})
{'graph_output': 'My name is Lance'}
```
:::
:::js
```typescript
import { z } from "zod";
const InputState = z.object({
userInput: z.string(),
});
const OutputState = z.object({
graphOutput: z.string(),
});
const OverallState = z.object({
foo: z.string(),
userInput: z.string(),
graphOutput: z.string(),
});
const PrivateState = z.object({
bar: z.string(),
});
const node1 = (state: z.infer<typeof InputState>): Partial<z.infer<typeof OverallState>> => {
// Write to OverallState
return { foo: state.userInput + " name" };
};
const node2 = (state: z.infer<typeof OverallState>): Partial<z.infer<typeof PrivateState>> => {
// Read from OverallState, write to PrivateState
return { bar: state.foo + " is" };
};
const node3 = (state: z.infer<typeof PrivateState>): Partial<z.infer<typeof OutputState>> => {
// Read from PrivateState, write to OutputState
return { graphOutput: state.bar + " Lance" };
};
const graph = new StateGraph({
state: OverallState,
input: InputState,
output: OutputState,
})
.addNode("node1", node1)
.addNode("node2", node2)
.addNode("node3", node3)
.addEdge(START, "node1")
.addEdge("node1", "node2")
.addEdge("node2", "node3")
.addEdge("node3", END)
.compile();
await graph.invoke({ userInput: "My" });
// { graphOutput: 'My name is Lance' }
```
:::
There are two subtle and important points to note here:
:::python
1. We pass `state: InputState` as the input schema to `node_1`. But, we write out to `foo`, a channel in `OverallState`. How can we write out to a state channel that is not included in the input schema? This is because a node _can write to any state channel in the graph state._ The graph state is the union of the state channels defined at initialization, which includes `OverallState` and the filters `InputState` and `OutputState`.
2. We initialize the graph with `StateGraph(OverallState,input_schema=InputState,output_schema=OutputState)`. So, how can we write to `PrivateState` in `node_2`? How does the graph gain access to this schema if it was not passed in the `StateGraph` initialization? We can do this because _nodes can also declare additional state channels_ as long as the state schema definition exists. In this case, the `PrivateState` schema is defined, so we can add `bar` as a new state channel in the graph and write to it.
:::
:::js
1. We pass `state: z.infer<typeof InputState>` as the input schema to `node1`. But, we write out to `foo`, a channel in `OverallState`. How can we write out to a state channel that is not included in the input schema? This is because a node _can write to any state channel in the graph state._ The graph state is the union of the state channels defined at initialization, which includes `OverallState` and the filters `InputState` and `OutputState`.
2. We initialize the graph with `new StateGraph({ state: OverallState, input: InputState, output: OutputState })`. So, how can we write to `PrivateState` in `node2`? How does the graph gain access to this schema if it was not passed in the `StateGraph` initialization? We can do this because _nodes can also declare additional state channels_ as long as the state schema definition exists. In this case, the `PrivateState` schema is defined, so we can add `bar` as a new state channel in the graph and write to it.
:::
### Reducers
@@ -227,7 +119,6 @@ These two examples show how to use the default reducer:
**Example A:**
:::python
```python
from typing_extensions import TypedDict
@@ -237,24 +128,9 @@ class State(TypedDict):
```
In this example, no reducer functions are specified for any key. Let's assume the input to the graph is `{"foo": 1, "bar": ["hi"]}`. Let's then assume the first `Node` returns `{"foo": 2}`. This is treated as an update to the state. Notice that the `Node` does not need to return the whole `State` schema - just an update. After applying this update, the `State` would then be `{"foo": 2, "bar": ["hi"]}`. If the second node returns `{"bar": ["bye"]}` then the `State` would then be `{"foo": 2, "bar": ["bye"]}`
:::
:::js
```typescript
import { z } from "zod";
const State = z.object({
foo: z.number(),
bar: z.array(z.string()),
});
```
In this example, no reducer functions are specified for any key. Let's assume the input to the graph is `{ foo: 1, bar: ["hi"] }`. Let's then assume the first `Node` returns `{ foo: 2 }`. This is treated as an update to the state. Notice that the `Node` does not need to return the whole `State` schema - just an update. After applying this update, the `State` would then be `{ foo: 2, bar: ["hi"] }`. If the second node returns `{ bar: ["bye"] }` then the `State` would then be `{ foo: 2, bar: ["bye"] }`
:::
**Example B:**
:::python
```python
from typing import Annotated
from typing_extensions import TypedDict
@@ -266,51 +142,21 @@ class State(TypedDict):
```
In this example, we've used the `Annotated` type to specify a reducer function (`operator.add`) for the second key (`bar`). Note that the first key remains unchanged. Let's assume the input to the graph is `{"foo": 1, "bar": ["hi"]}`. Let's then assume the first `Node` returns `{"foo": 2}`. This is treated as an update to the state. Notice that the `Node` does not need to return the whole `State` schema - just an update. After applying this update, the `State` would then be `{"foo": 2, "bar": ["hi"]}`. If the second node returns `{"bar": ["bye"]}` then the `State` would then be `{"foo": 2, "bar": ["hi", "bye"]}`. Notice here that the `bar` key is updated by adding the two lists together.
:::
:::js
```typescript
import { z } from "zod";
import "@langchain/langgraph/zod";
const State = z.object({
foo: z.number(),
bar: z.array(z.string()).langgraph.reducer((x, y) => x.concat(y)),
});
```
In this example, we've used the `.langgraph.reducer()` method to specify a reducer function for the second key (`bar`). Note that the first key remains unchanged. Let's assume the input to the graph is `{ foo: 1, bar: ["hi"] }`. Let's then assume the first `Node` returns `{ foo: 2 }`. This is treated as an update to the state. Notice that the `Node` does not need to return the whole `State` schema - just an update. After applying this update, the `State` would then be `{ foo: 2, bar: ["hi"] }`. If the second node returns `{ bar: ["bye"] }` then the `State` would then be `{ foo: 2, bar: ["hi", "bye"] }`. Notice here that the `bar` key is updated by concatenating the two arrays together.
:::
### Working with Messages in Graph State
#### Why use messages?
:::python
Most modern LLM providers have a chat model interface that accepts a list of messages as input. LangChain's [`ChatModel`](https://python.langchain.com/docs/concepts/#chat-models) in particular accepts a list of `Message` objects as inputs. These messages come in a variety of forms such as `HumanMessage` (user input) or `AIMessage` (LLM response). To read more about what message objects are, please refer to [this](https://python.langchain.com/docs/concepts/#messages) conceptual guide.
:::
:::js
Most modern LLM providers have a chat model interface that accepts a list of messages as input. LangChain's [`ChatModel`](https://js.langchain.com/docs/concepts/#chat-models) in particular accepts a list of `Message` objects as inputs. These messages come in a variety of forms such as `HumanMessage` (user input) or `AIMessage` (LLM response). To read more about what message objects are, please refer to [this](https://js.langchain.com/docs/concepts/#messages) conceptual guide.
:::
#### Using Messages in your Graph
:::python
In many cases, it is helpful to store prior conversation history as a list of messages in your graph state. To do so, we can add a key (channel) to the graph state that stores a list of `Message` objects and annotate it with a reducer function (see `messages` key in the example below). The reducer function is vital to telling the graph how to update the list of `Message` objects in the state with each state update (for example, when a node sends an update). If you don't specify a reducer, every state update will overwrite the list of messages with the most recently provided value. If you wanted to simply append messages to the existing list, you could use `operator.add` as a reducer.
However, you might also want to manually update messages in your graph state (e.g. human-in-the-loop). If you were to use `operator.add`, the manual state updates you send to the graph would be appended to the existing list of messages, instead of updating existing messages. To avoid that, you need a reducer that can keep track of message IDs and overwrite existing messages, if updated. To achieve this, you can use the prebuilt `add_messages` function. For brand new messages, it will simply append to existing list, but it will also handle the updates for existing messages correctly.
:::
:::js
In many cases, it is helpful to store prior conversation history as a list of messages in your graph state. To do so, we can add a key (channel) to the graph state that stores a list of `Message` objects and annotate it with a reducer function (see `messages` key in the example below). The reducer function is vital to telling the graph how to update the list of `Message` objects in the state with each state update (for example, when a node sends an update). If you don't specify a reducer, every state update will overwrite the list of messages with the most recently provided value. If you wanted to simply append messages to the existing list, you could use `(x, y) => x.concat(y)` as a reducer.
However, you might also want to manually update messages in your graph state (e.g. human-in-the-loop). If you were to use `(x, y) => x.concat(y)`, the manual state updates you send to the graph would be appended to the existing list of messages, instead of updating existing messages. To avoid that, you need a reducer that can keep track of message IDs and overwrite existing messages, if updated. To achieve this, you can use the prebuilt `messagesStateReducer` function. For brand new messages, it will simply append to existing list, but it will also handle the updates for existing messages correctly.
:::
#### Serialization
:::python
In addition to keeping track of message IDs, the `add_messages` function will also try to deserialize messages into LangChain `Message` objects whenever a state update is received on the `messages` channel. See more information on LangChain serialization/deserialization [here](https://python.langchain.com/docs/how_to/serialization/). This allows sending graph inputs / state updates in the following format:
```python
@@ -332,36 +178,9 @@ from typing_extensions import TypedDict
class GraphState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
```
:::
:::js
In addition to keeping track of message IDs, the `messagesStateReducer` function will also try to deserialize messages into LangChain `Message` objects whenever a state update is received on the `messages` channel. See more information on LangChain serialization/deserialization [here](https://js.langchain.com/docs/how_to/serialization/). This allows sending graph inputs / state updates in the following format:
```typescript
// this is supported
{ messages: [new HumanMessage("message")] }
// and this is also supported
{ messages: [{ role: "human", content: "message" }] }
```
Since the state updates are always deserialized into LangChain `Messages` when using `messagesStateReducer`, you should use dot notation to access message attributes, like `state.messages[state.messages.length - 1].content`. Below is an example of a graph that uses `messagesStateReducer` as its reducer function.
```typescript
import { BaseMessage } from "@langchain/core/messages";
import { messagesStateReducer } from "@langchain/langgraph";
import { z } from "zod";
import "@langchain/langgraph/zod";
const GraphState = z.object({
messages: z.array(z.any()).langgraph.reducer(messagesStateReducer),
});
```
:::
#### MessagesState
:::python
Since having a list of messages in your state is so common, there exists a prebuilt state called `MessagesState` which makes it easy to use messages. `MessagesState` is defined with a single `messages` key which is a list of `AnyMessage` objects and uses the `add_messages` reducer. Typically, there is more state to track than just messages, so we see people subclass this state and add more fields, like:
```python
@@ -370,25 +189,9 @@ from langgraph.graph import MessagesState
class State(MessagesState):
documents: list[str]
```
:::
:::js
Since having a list of messages in your state is so common, there exists a prebuilt state called `MessagesZodState` which makes it easy to use messages. `MessagesZodState` is defined with a single `messages` key which is a list of `BaseMessage` objects and uses the `messagesStateReducer` reducer. Typically, there is more state to track than just messages, so we see people merge this state with other schemas, like:
```typescript
import { MessagesZodState } from "@langchain/langgraph";
import { z } from "zod";
const State = z.object({
messages: MessagesZodState.shape.messages,
documents: z.array(z.string()),
});
```
:::
## Nodes
:::python
In LangGraph, nodes are typically python functions (sync or async) where the **first** positional argument is the [state](#state), and (optionally), the **second** positional argument is a "config", containing optional [configurable parameters](#configuration) (such as a `thread_id`).
Similar to `NetworkX`, you add these nodes to a graph using the [add_node][langgraph.graph.StateGraph.add_node] method:
@@ -429,88 +232,26 @@ If you add a node to a graph without specifying a name, it will be given a defau
builder.add_node(my_node)
# You can then create edges to/from this node by referencing it as `"my_node"`
```
:::
:::js
In LangGraph, nodes are typically TypeScript functions (sync or async) where the **first** positional argument is the [state](#state), and (optionally), the **second** positional argument is a "config", containing optional [configurable parameters](#configuration) (such as a `thread_id`).
Similar to `NetworkX`, you add these nodes to a graph using the `addNode` method:
```typescript
import { z } from "zod";
import { RunnableConfig } from "@langchain/core/runnables";
import { StateGraph } from "@langchain/langgraph";
const State = z.object({
input: z.string(),
results: z.string(),
});
const myNode = (state: z.infer<typeof State>, config: RunnableConfig) => {
console.log("In node: ", config?.configurable?.user_id);
return { results: `Hello, ${state.input}!` };
};
// The second argument is optional
const myOtherNode = (state: z.infer<typeof State>) => {
return state;
};
const graph = new StateGraph(State)
.addNode("myNode", myNode)
.addNode("otherNode", myOtherNode)
// ...
.compile();
```
Behind the scenes, functions are converted to [RunnableLambda](https://js.langchain.com/docs/concepts/#runnable-lambda)s, which add batch and async support to your function, along with native tracing and debugging.
:::
### `START` Node
The `START` Node is a special node that represents the node that sends user input to the graph. The main purpose for referencing this node is to determine which nodes should be called first.
:::python
```python
from langgraph.graph import START
graph.add_edge(START, "node_a")
```
:::
:::js
```typescript
import { START } from "@langchain/langgraph";
const graph = new StateGraph(State)
.addNode("nodeA", nodeA)
.addEdge(START, "nodeA")
.compile();
```
:::
### `END` Node
The `END` Node is a special node that represents a terminal node. This node is referenced when you want to denote which edges have no actions after they are done.
:::python
```
from langgraph.graph import END
graph.add_edge("node_a", END)
```
:::
:::js
```typescript
import { END } from "@langchain/langgraph";
const graph = new StateGraph(State)
.addNode("nodeA", nodeA)
.addEdge("nodeA", END)
.compile();
```
:::
### Node Caching
@@ -523,7 +264,6 @@ LangGraph supports caching of tasks/nodes based on the input to the node. To use
For example:
:::python
```py
import time
from typing_extensions import TypedDict
@@ -560,40 +300,6 @@ print(graph.invoke({"x": 5}, stream_mode='updates')) # (2)!
1. First run takes the full second to run (due to mocked expensive computation).
2. Second run utilizes cache and returns quickly.
:::
:::js
```typescript
import { z } from "zod";
import { StateGraph } from "@langchain/langgraph";
import { InMemoryCache } from "@langchain/langgraph";
const State = z.object({
x: z.number(),
result: z.number(),
});
const expensiveNode = (state: z.infer<typeof State>) => {
// expensive computation
return { result: state.x * 2 };
};
const graph = new StateGraph(State)
.addNode("expensiveNode", expensiveNode, {
cachePolicy: { ttl: 3 }
})
.addEdge("__start__", "expensiveNode")
.compile({ cache: new InMemoryCache() });
console.log(await graph.invoke({ x: 5 }, { streamMode: "updates" })); // (1)!
// [{ expensiveNode: { result: 10 } }]
console.log(await graph.invoke({ x: 5 }, { streamMode: "updates" })); // (2)!
// [{ expensiveNode: { result: 10 }, __metadata__: { cached: true } }]
```
1. First run takes the full computation time.
2. Second run utilizes cache and returns quickly.
:::
## Edges
@@ -608,29 +314,14 @@ A node can have MULTIPLE outgoing edges. If a node has multiple out-going edges,
### Normal Edges
:::python
If you **always** want to go from node A to node B, you can use the [add_edge][langgraph.graph.StateGraph.add_edge] method directly.
```python
graph.add_edge("node_a", "node_b")
```
:::
:::js
If you **always** want to go from node A to node B, you can use the `addEdge` method directly.
```typescript
const graph = new StateGraph(State)
.addNode("nodeA", nodeA)
.addNode("nodeB", nodeB)
.addEdge("nodeA", "nodeB")
.compile();
```
:::
### Conditional Edges
:::python
If you want to **optionally** route to 1 or more edges (or optionally terminate), you can use the [add_conditional_edges][langgraph.graph.StateGraph.add_conditional_edges] method. This method accepts the name of a node and a "routing function" to call after that node is executed:
```python
@@ -646,45 +337,12 @@ You can optionally provide a dictionary that maps the `routing_function`'s outpu
```python
graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"})
```
:::
:::js
If you want to **optionally** route to 1 or more edges (or optionally terminate), you can use the `addConditionalEdges` method. This method accepts the name of a node and a "routing function" to call after that node is executed:
```typescript
const graph = new StateGraph(State)
.addNode("nodeA", nodeA)
.addNode("nodeB", nodeB)
.addNode("nodeC", nodeC)
.addConditionalEdges("nodeA", routingFunction)
.compile();
```
Similar to nodes, the `routingFunction` accepts the current `state` of the graph and returns a value.
By default, the return value `routingFunction` is used as the name of the node (or list of nodes) to send the state to next. All those nodes will be run in parallel as a part of the next superstep.
You can optionally provide a dictionary that maps the `routingFunction`'s output to the name of the next node.
```typescript
const graph = new StateGraph(State)
.addNode("nodeA", nodeA)
.addNode("nodeB", nodeB)
.addNode("nodeC", nodeC)
.addConditionalEdges("nodeA", routingFunction, {
true: "nodeB",
false: "nodeC",
})
.compile();
```
:::
!!! tip
Use [`Command`](#command) instead of conditional edges if you want to combine state updates and routing in a single function.
### Entry Point
:::python
The entry point is the first node(s) that are run when the graph starts. You can use the [`add_edge`][langgraph.graph.StateGraph.add_edge] method from the virtual [`START`][langgraph.constants.START] node to the first node to execute to specify where to enter the graph.
```python
@@ -692,24 +350,9 @@ from langgraph.graph import START
graph.add_edge(START, "node_a")
```
:::
:::js
The entry point is the first node(s) that are run when the graph starts. You can use the `addEdge` method from the virtual `START` node to the first node to execute to specify where to enter the graph.
```typescript
import { START } from "@langchain/langgraph";
const graph = new StateGraph(State)
.addNode("nodeA", nodeA)
.addEdge(START, "nodeA")
.compile();
```
:::
### Conditional Entry Point
:::python
A conditional entry point lets you start at different nodes depending on custom logic. You can use [`add_conditional_edges`][langgraph.graph.StateGraph.add_conditional_edges] from the virtual [`START`][langgraph.constants.START] node to accomplish this.
```python
@@ -723,42 +366,11 @@ You can optionally provide a dictionary that maps the `routing_function`'s outpu
```python
graph.add_conditional_edges(START, routing_function, {True: "node_b", False: "node_c"})
```
:::
:::js
A conditional entry point lets you start at different nodes depending on custom logic. You can use `addConditionalEdges` from the virtual `START` node to accomplish this.
```typescript
import { START } from "@langchain/langgraph";
const graph = new StateGraph(State)
.addNode("nodeB", nodeB)
.addNode("nodeC", nodeC)
.addConditionalEdges(START, routingFunction)
.compile();
```
You can optionally provide a dictionary that maps the `routingFunction`'s output to the name of the next node.
```typescript
import { START } from "@langchain/langgraph";
const graph = new StateGraph(State)
.addNode("nodeB", nodeB)
.addNode("nodeC", nodeC)
.addConditionalEdges(START, routingFunction, {
true: "nodeB",
false: "nodeC",
})
.compile();
```
:::
## `Send`
By default, `Nodes` and `Edges` are defined ahead of time and operate on the same shared state. However, there can be cases where the exact edges are not known ahead of time and/or you may want different versions of `State` to exist at the same time. A common example of this is with [map-reduce](https://langchain-ai.github.io/langgraph/how-tos/map-reduce/) design patterns. In this design pattern, a first node may generate a list of objects, and you may want to apply some other node to all those objects. The number of objects may be unknown ahead of time (meaning the number of edges may not be known) and the input `State` to the downstream `Node` should be different (one for each generated object).
:::python
To support this design pattern, LangGraph supports returning [`Send`][langgraph.types.Send] objects from conditional edges. `Send` takes two arguments: first is the name of the node, and second is the state to pass to that node.
```python
@@ -767,31 +379,11 @@ def continue_to_jokes(state: OverallState):
graph.add_conditional_edges("node_a", continue_to_jokes)
```
:::
:::js
To support this design pattern, LangGraph supports returning `Send` objects from conditional edges. `Send` takes two arguments: first is the name of the node, and second is the state to pass to that node.
```typescript
import { Send } from "@langchain/langgraph";
const continueToJokes = (state: OverallState) => {
return state.subjects.map((subject) => new Send("generateJoke", { subject }));
};
const graph = new StateGraph(State)
.addNode("nodeA", nodeA)
.addNode("generateJoke", generateJoke)
.addConditionalEdges("nodeA", continueToJokes)
.compile();
```
:::
## `Command`
It can be useful to combine control flow (edges) and state updates (nodes). For example, you might want to BOTH perform state updates AND decide which node to go to next in the SAME node. LangGraph provides a way to do so by returning a [`Command`][langgraph.types.Command] object from node functions:
:::python
```python
def my_node(state: State) -> Command[Literal["my_other_node"]]:
return Command(
@@ -801,69 +393,18 @@ def my_node(state: State) -> Command[Literal["my_other_node"]]:
goto="my_other_node"
)
```
:::
:::js
```typescript
import { Command } from "@langchain/langgraph";
const myNode = (state: State): Command => {
return new Command({
// state update
update: { foo: "bar" },
// control flow
goto: "myOtherNode",
});
};
```
:::
With `Command` you can also achieve dynamic control flow behavior (identical to [conditional edges](#conditional-edges)):
:::python
```python
def my_node(state: State) -> Command[Literal["my_other_node"]]:
if state["foo"] == "bar":
return Command(update={"foo": "baz"}, goto="my_other_node")
```
:::
:::js
```typescript
import { Command } from "@langchain/langgraph";
const myNode = (state: State): Command => {
if (state.foo === "bar") {
return new Command({
update: { foo: "baz" },
goto: "myOtherNode",
});
}
return new Command({
update: { foo: "qux" },
goto: "myThirdNode",
});
};
```
:::
!!! important
:::python
When returning `Command` in your node functions, you must add return type annotations with the list of node names the node is routing to, e.g. `Command[Literal["my_other_node"]]`. This is necessary for the graph rendering and tells LangGraph that `my_node` can navigate to `my_other_node`.
:::
:::js
When returning `Command` in your node functions, you must specify the `ends` option when adding the node to ensure proper graph rendering and tell LangGraph which nodes this node can navigate to.
```typescript
const graph = new StateGraph(State)
.addNode("myNode", myNode, {
ends: ["myOtherNode", "myThirdNode"],
})
.compile();
```
:::
Check out this [how-to guide](../how-tos/graph-api.ipynb#combine-control-flow-and-state-updates-with-command) for an end-to-end example of how to use `Command`.
@@ -877,7 +418,6 @@ Use [conditional edges](#conditional-edges) to route between nodes conditionally
If you are using [subgraphs](./subgraphs.md), you might want to navigate from a node within a subgraph to a different subgraph (i.e. a different node in the parent graph). To do so, you can specify `graph=Command.PARENT` in `Command`:
:::python
```python
def my_node(state: State) -> Command[Literal["other_subgraph"]]:
return Command(
@@ -886,21 +426,6 @@ def my_node(state: State) -> Command[Literal["other_subgraph"]]:
graph=Command.PARENT
)
```
:::
:::js
```typescript
import { Command } from "@langchain/langgraph";
const myNode = (state: State): Command => {
return new Command({
update: { foo: "bar" },
goto: "otherSubgraph", // where `otherSubgraph` is a node in the parent graph
graph: Command.PARENT,
});
};
```
:::
!!! note
@@ -922,13 +447,7 @@ Refer to [this guide](../how-tos/graph-api.ipynb#use-inside-tools) for detail.
### Human-in-the-loop
:::python
`Command` is an important part of human-in-the-loop workflows: when using `interrupt()` to collect user input, `Command` is then used to supply the input and resume execution via `Command(resume="User input")`. Check out [this conceptual guide](./human_in_the_loop.md) for more information.
:::
:::js
`Command` is an important part of human-in-the-loop workflows: when using `interrupt()` to collect user input, `Command` is then used to supply the input and resume execution via `new Command({ resume: "User input" })`. Check out [this conceptual guide](./human_in_the_loop.md) for more information.
:::
## Graph Migrations
@@ -944,7 +463,6 @@ LangGraph can easily handle migrations of graph definitions (nodes, edges, and s
When creating a graph, you can also mark that certain parts of the graph are configurable. This is commonly done to enable easily switching between models or system prompts. This allows you to create a single "cognitive architecture" (the graph) but have multiple different instance of it.
:::python
You can optionally specify a `config_schema` when creating a graph.
```python
@@ -970,69 +488,19 @@ def node_a(state, config):
llm = get_llm(llm_type)
...
```
:::
:::js
You can optionally specify a `configSchema` when creating a graph.
```typescript
import { z } from "zod";
const ConfigSchema = z.object({
llm: z.string(),
});
const graph = new StateGraph(State, ConfigSchema)
.addNode("nodeA", nodeA)
.compile();
```
You can then pass this configuration into the graph using the `configurable` config field.
```typescript
const config = { configurable: { llm: "anthropic" } };
await graph.invoke(inputs, config);
```
You can then access and use this configuration inside a node or conditional edge:
```typescript
const nodeA = (state: State, config: RunnableConfig) => {
const llmType = config?.configurable?.llm || "openai";
const llm = getLlm(llmType);
// ...
};
```
:::
See [this guide](../how-tos/graph-api.ipynb#add-runtime-configuration) for a full breakdown on configuration.
### Recursion Limit
The recursion limit sets the maximum number of [super-steps](#graphs) the graph can execute during a single execution. Once the limit is reached, LangGraph will raise `GraphRecursionError`. By default this value is set to 25 steps.
:::python
The recursion limit can be set on any graph at runtime, and is passed to `.invoke`/`.stream` via the config dictionary. Importantly, `recursion_limit` is a standalone `config` key and should not be passed inside the `configurable` key as all other user-defined configuration. See the example below:
The recursion limit sets the maximum number of [super-steps](#graphs) the graph can execute during a single execution. Once the limit is reached, LangGraph will raise `GraphRecursionError`. By default this value is set to 25 steps. The recursion limit can be set on any graph at runtime, and is passed to `.invoke`/`.stream` via the config dictionary. Importantly, `recursion_limit` is a standalone `config` key and should not be passed inside the `configurable` key as all other user-defined configuration. See the example below:
```python
graph.invoke(inputs, config={"recursion_limit": 5, "configurable":{"llm": "anthropic"}})
```
:::
:::js
The recursion limit can be set on any graph at runtime, and is passed to `.invoke`/`.stream` via the config dictionary. Importantly, `recursionLimit` is a standalone `config` key and should not be passed inside the `configurable` key as all other user-defined configuration. See the example below:
```typescript
await graph.invoke(inputs, {
recursionLimit: 5,
configurable: { llm: "anthropic" },
});
```
:::
Read [this how-to](https://langchain-ai.github.io/langgraph/how-tos/recursion-limit/) to learn more about how the recursion limit works.
## Visualization
It's often nice to be able to visualize graphs, especially as they get more complex. LangGraph comes with several built-in ways to visualize graphs. See [this how-to guide](../how-tos/graph-api.ipynb#visualize-your-graph) for more info.
It's often nice to be able to visualize graphs, especially as they get more complex. LangGraph comes with several built-in ways to visualize graphs. See [this how-to guide](../how-tos/graph-api.ipynb#visualize-your-graph) for more info.
+8 -1
View File
@@ -1,5 +1,12 @@
# Use the functional API
The [**Functional API**](../concepts/functional_api.md) allows you to add LangGraph's key features — [persistence](../concepts/persistence.md), [memory](../how-tos/memory/add-memory.md), [human-in-the-loop](../concepts/human_in_the_loop.md), and [streaming](../concepts/streaming.md) — to your applications with minimal changes to your existing code.
!!! tip
For conceptual information on the functional API, see [Functional API](../concepts/functional_api.md).
## Creating a simple workflow
When defining an `entrypoint`, input is restricted to the first argument of the function. To pass multiple inputs, you can use a dictionary.
@@ -832,4 +839,4 @@ for chunk in workflow.stream([input_message], config, stream_mode="values"):
## Integrate with other libraries
* [Add LangGraph's features to other frameworks using the functional API](./autogen-integration-functional.ipynb): Add LangGraph features like persistence, memory and streaming to other agent frameworks that do not provide them out of the box.
* [Add LangGraph's features to other frameworks using the functional API](./autogen-integration-functional.ipynb): Add LangGraph features like persistence, memory and streaming to other agent frameworks that do not provide them out of the box.
@@ -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. Lets dive in! 🌟
## Prerequisites
@@ -13,44 +13,13 @@ tool-calling features, such as [OpenAI](https://platform.openai.com/api-keys),
Install the required packages:
:::python
```bash
pip install -U langgraph langsmith
```
:::
:::js
=== "npm"
```bash
npm install @langchain/langgraph @langchain/core zod
```
=== "yarn"
```bash
yarn add @langchain/langgraph @langchain/core zod
```
=== "pnpm"
```bash
pnpm add @langchain/langgraph @langchain/core zod
```
=== "bun"
```bash
bun add @langchain/langgraph @langchain/core zod
```
:::
!!! tip
Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph. For more information on how to get started, see [LangSmith docs](https://docs.smith.langchain.com).
Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph. For more information on how to get started, see [LangSmith docs](https://docs.smith.langchain.com).
## 2. Create a `StateGraph`
@@ -58,8 +27,6 @@ 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
@@ -79,41 +46,23 @@ class State(TypedDict):
graph_builder = StateGraph(State)
```
:::
:::js
```typescript
import { StateGraph, MessagesZodState, START } from "@langchain/langgraph";
import { z } from "zod";
const State = z.object({ messages: MessagesZodState.shape.messages });
const graph = new StateGraph(State).compile();
```
:::
Our graph can now handle two key tasks:
1. Each `node` can receive the current `State` as input and output an update to the state.
2. Updates to `messages` will be appended to the existing list rather than overwriting it, thanks to the prebuilt reducer function.
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.
---
------
!!! 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 schema with one key: `messages`. The reducer function is used to append new messages to the list instead of overwriting it. Keys without a reducer annotation will overwrite previous values.
To learn more about state, reducers, and related concepts, see [LangGraph reference docs](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages).
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).
## 3. Add a node
Next, add a "`chatbot`" node. **Nodes** represent units of work and are typically regular functions.
Next, add a "`chatbot`" node. **Nodes** represent units of work and are typically regular Python functions.
Let's first select a chat model:
:::python
{!snippets/chat_model_tabs.md!}
<!---
@@ -124,26 +73,9 @@ llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
```
-->
:::
:::js
```typescript
import { ChatOpenAI } from "@langchain/openai";
// or import { ChatAnthropic } from "@langchain/anthropic";
const llm = new ChatOpenAI({
model: "gpt-4o",
temperature: 0,
});
```
:::
We can now incorporate the chat model into a simple node:
:::python
```python
def chatbot(state: State):
@@ -156,132 +88,38 @@ def chatbot(state: State):
graph_builder.add_node("chatbot", chatbot)
```
:::
:::js
```typescript hl_lines="7-9"
import { StateGraph, MessagesZodState, START } from "@langchain/langgraph";
import { z } from "zod";
const State = z.object({ messages: MessagesZodState.shape.messages });
const graph = new StateGraph(State)
.addNode("chatbot", async (state: z.infer<typeof State>) => {
return { messages: [await llm.invoke(state.messages)] };
})
.compile();
```
:::
**Notice** how the `chatbot` node function takes the current `State` as input and returns a dictionary containing an updated `messages` list under the key "messages". This is the basic pattern for all LangGraph node functions.
:::python
The `add_messages` function in our `State` will append the LLM's response messages to whatever messages are already in the state.
:::
:::js
The `addMessages` function used within `MessagesZodState` will append the LLM's response messages to whatever messages are already in the state.
:::
## 4. Add an `entry` point
Add an `entry` point to tell the graph **where to start its work** each time it is run:
:::python
```python
graph_builder.add_edge(START, "chatbot")
```
:::
:::js
```typescript hl_lines="10"
import { StateGraph, MessagesZodState, START } from "@langchain/langgraph";
import { z } from "zod";
const State = z.object({ messages: MessagesZodState.shape.messages });
const graph = new StateGraph(State)
.addNode("chatbot", async (state: z.infer<typeof State>) => {
return { messages: [await llm.invoke(state.messages)] };
})
.addEdge(START, "chatbot")
.compile();
```
:::
## 5. Add an `exit` point
Add an `exit` point to indicate **where the graph should finish execution**. This is helpful for more complex flows, but even in a simple graph like this, adding an end node improves clarity.
:::python
```python
graph_builder.add_edge("chatbot", END)
```
:::
:::js
```typescript hl_lines="11"
import { StateGraph, MessagesZodState, START, END } from "@langchain/langgraph";
import { z } from "zod";
const State = z.object({ messages: MessagesZodState.shape.messages });
const graph = new StateGraph(State)
.addNode("chatbot", async (state: z.infer<typeof State>) => {
return { messages: [await llm.invoke(state.messages)] };
})
.addEdge(START, "chatbot")
.addEdge("chatbot", END)
.compile();
```
:::
This tells the graph to terminate after running the chatbot node.
## 6. Compile the graph
Before running the graph, we'll need to compile it. We can do so by calling `compile()` on the graph builder. This creates a `CompiledGraph` we can invoke on our state.
:::python
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
graph = graph_builder.compile()
```
:::
:::js
```typescript hl_lines="12"
import { StateGraph, MessagesZodState, START, END } from "@langchain/langgraph";
import { z } from "zod";
const State = z.object({ messages: MessagesZodState.shape.messages });
const graph = new StateGraph(State)
.addNode("chatbot", async (state: z.infer<typeof State>) => {
return { messages: [await llm.invoke(state.messages)] };
})
.addEdge(START, "chatbot")
.addEdge("chatbot", END)
.compile();
```
:::
## 7. Visualize the graph (optional)
:::python
You can visualize the graph using the `get_graph` method and one of the "draw" methods, like `draw_ascii` or `draw_png`. The `draw` methods each require additional dependencies.
```python
@@ -294,35 +132,17 @@ except Exception:
pass
```
:::
:::js
You can visualize the graph using the `getGraph` method and render the graph with the `drawMermaidPng` method.
```typescript
import * as fs from "node:fs/promises";
const drawableGraph = await graph.getGraphAsync();
const image = await drawableGraph.drawMermaidPng();
const imageBuffer = new Uint8Array(await image.arrayBuffer());
await fs.writeFile("basic-chatbot.png", imageBuffer);
```
:::
![basic chatbot diagram](basic-chatbot.png)
## 8. Run the chatbot
Now run the chatbot!
Now run the chatbot!
!!! tip
You can exit the chat loop at any time by typing `quit`, `exit`, or `q`.
:::python
```python
def stream_graph_updates(user_input: str):
for event in graph.stream({"messages": [{"role": "user", "content": user_input}]}):
@@ -345,66 +165,13 @@ while True:
break
```
:::
:::js
```typescript
import * as readline from "node:readline/promises";
import { StateGraph, MessagesZodState, START, END } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
const llm = new ChatOpenAI({ model: "gpt-4o-mini" });
const State = z.object({ messages: MessagesZodState.shape.messages });
const graph = new StateGraph(State)
.addNode("chatbot", async (state: z.infer<typeof State>) => {
return { messages: [await llm.invoke(state.messages)] };
})
.addEdge(START, "chatbot")
.addEdge("chatbot", END)
.compile();
async function generateText(content: string) {
const stream = await graph.stream(
{ messages: [{ type: "human", content }] },
{ streamMode: "values" }
);
for await (const event of stream) {
const lastMessage = event.messages.at(-1);
if (lastMessage?.getType() === "ai") {
console.log(`Assistant: ${lastMessage.text}`);
}
}
}
const prompt = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
while (true) {
const human = await prompt.question("User: ");
if (["quit", "exit", "q"].includes(human.trim())) break;
await generateText(human || "What do you know about LangGraph?");
}
prompt.close();
```
:::
```
Assistant: LangGraph is a library designed to help build stateful multi-agent applications using language models. It provides tools for creating workflows and state machines to coordinate multiple AI agents or language model interactions. LangGraph is built on top of LangChain, leveraging its components while adding graph-based coordination capabilities. It's particularly useful for developing more complex, stateful AI applications that go beyond simple query-response interactions.
Goodbye!
```
**Congratulations!** You've built your first chatbot using LangGraph. This bot can engage in basic conversation by taking user input and generating responses using an LLM. You can inspect a [LangSmith Trace](https://smith.langchain.com/public/7527e308-9502-4894-b347-f34385740d5a/r) for the call above.
:::python
Below is the full code for this tutorial:
```python
@@ -440,8 +207,8 @@ graph_builder.add_edge("chatbot", END)
graph = graph_builder.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.
+7 -410
View File
@@ -10,64 +10,19 @@ To handle queries that your chatbot can't answer "from memory", integrate a web
Before you start this tutorial, ensure you have the following:
:::python
- An API key for the [Tavily Search Engine](https://python.langchain.com/docs/integrations/tools/tavily_search/).
:::
:::js
- An API key for the [Tavily Search Engine](https://js.langchain.com/docs/integrations/tools/tavily_search/).
:::
## 1. Install the search engine
:::python
Install the requirements to use the [Tavily Search Engine](https://python.langchain.com/docs/integrations/tools/tavily_search/):
```bash
pip install -U langchain-tavily
```
:::
:::js
Install the requirements to use the [Tavily Search Engine](https://docs.tavily.com/):
=== "npm"
```bash
npm install @langchain/tavily
```
=== "yarn"
```bash
yarn add @langchain/tavily
```
=== "pnpm"
```bash
pnpm add @langchain/tavily
```
=== "bun"
```bash
bun add @langchain/tavily
```
:::
## 2. Configure your environment
Configure your environment with your search engine API key:
:::python
```bash
_set_env("TAVILY_API_KEY")
```
@@ -76,22 +31,10 @@ _set_env("TAVILY_API_KEY")
TAVILY_API_KEY: ········
```
:::
:::js
```typescript
process.env.TAVILY_API_KEY = "tvly-...";
```
:::
## 3. Define the tool
Define the web search tool:
:::python
```python
from langchain_tavily import TavilySearch
@@ -100,25 +43,8 @@ tools = [tool]
tool.invoke("What's a 'node' in LangGraph?")
```
:::
:::js
```typescript
import { TavilySearch } from "@langchain/tavily";
const tool = new TavilySearch({ maxResults: 2 });
const tools = [tool];
await tool.invoke({ query: "What's a 'node' in LangGraph?" });
```
:::
The results are page summaries our chat bot can use to answer questions:
:::python
```
{'query': "What's a 'node' in LangGraph?",
'follow_up_questions': None,
@@ -137,51 +63,12 @@ The results are page summaries our chat bot can use to answer questions:
'response_time': 1.38}
```
:::
:::js
```json
{
"query": "What's a 'node' in LangGraph?",
"follow_up_questions": null,
"answer": null,
"images": [],
"results": [
{
"url": "https://blog.langchain.dev/langgraph/",
"title": "LangGraph - LangChain Blog",
"content": "TL;DR: LangGraph is module built on top of LangChain to better enable creation of cyclical graphs, often needed for agent runtimes. This state is updated by nodes in the graph, which return operations to attributes of this state (in the form of a key-value store). After adding nodes, you can then add edges to create the graph. An example of this may be in the basic agent runtime, where we always want the model to be called after we call a tool. The state of this graph by default contains concepts that should be familiar to you if you've used LangChain agents: `input`, `chat_history`, `intermediate_steps` (and `agent_outcome` to represent the most recent agent outcome)",
"score": 0.7407191,
"raw_content": null
},
{
"url": "https://medium.com/@cplog/introduction-to-langgraph-a-beginners-guide-14f9be027141",
"title": "Introduction to LangGraph: A Beginner's Guide - Medium",
"content": "* **Stateful Graph:** LangGraph revolves around the concept of a stateful graph, where each node in the graph represents a step in your computation, and the graph maintains a state that is passed around and updated as the computation progresses. LangGraph supports conditional edges, allowing you to dynamically determine the next node to execute based on the current state of the graph. Image 10: Introduction to AI Agent with LangChain and LangGraph: A Beginners Guide Image 18: How to build LLM Agent with LangGraph — StateGraph and Reducer Image 20: Simplest Graphs using LangGraph Framework Image 24: Building a ReAct Agent with Langgraph: A Step-by-Step Guide Image 28: Building an Agentic RAG with LangGraph: A Step-by-Step Guide",
"score": 0.65279555,
"raw_content": null
}
],
"response_time": 1.34
}
```
:::
## 4. Define the graph
:::python
For the `StateGraph` you created in the [first tutorial](./1-build-basic-chatbot.md), add `bind_tools` on the LLM. This lets the LLM know the correct JSON format to use if it wants to use the search engine.
:::
:::js
For the `StateGraph` you created in the [first tutorial](./1-build-basic-chatbot.md), add `bindTools` on the LLM. This lets the LLM know the correct JSON format to use if it wants to use the search engine.
:::
Let's first select our LLM:
:::python
{!snippets/chat_model_tabs.md!}
<!---
@@ -192,22 +79,8 @@ llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
```
-->
:::
:::js
```typescript
import { ChatAnthropic } from "@langchain/anthropic";
const llm = new ChatAnthropic({ model: "claude-3-5-sonnet-latest" });
```
:::
We can now incorporate it into a `StateGraph`:
:::python
```python hl_lines="15"
from typing import Annotated
@@ -231,31 +104,9 @@ def chatbot(state: State):
graph_builder.add_node("chatbot", chatbot)
```
:::
:::js
```typescript hl_lines="7-8"
import { StateGraph, MessagesZodState } from "@langchain/langgraph";
import { z } from "zod";
const State = z.object({ messages: MessagesZodState.shape.messages });
const chatbot = async (state: z.infer<typeof State>) => {
// Modification: tell the LLM which tools it can call
const llmWithTools = llm.bindTools(tools);
return { messages: [await llmWithTools.invoke(state.messages)] };
};
```
:::
## 5. Create a function to run the tools
:::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.
Now, create a function to run the tools if they are called. Do this by adding the tools to a new node called`BasicToolNode` that checks the most recent message in the state and calls tools if the message contains `tool_calls`. It relies on the LLM's `tool_calling` support, which is available in Anthropic, OpenAI, Google Gemini, and a number of other LLM providers.
```python
import json
@@ -297,80 +148,16 @@ graph_builder.add_node("tools", tool_node)
If you do not want to build this yourself in the future, you can use LangGraph's prebuilt [ToolNode](https://langchain-ai.github.io/langgraph/reference/agents/#langgraph.prebuilt.tool_node.ToolNode).
:::
:::js
Now, create a function to run the tools if they are called. Do this by adding the tools to a new node called `"tools"` that checks the most recent message in the state and calls tools if the message contains `tool_calls`. It relies on the LLM's tool calling support, which is available in Anthropic, OpenAI, Google Gemini, and a number of other LLM providers.
```typescript
import type { StructuredToolInterface } from "@langchain/core/tools";
import { isAIMessage, ToolMessage } from "@langchain/core/messages";
function createToolNode(tools: StructuredToolInterface[]) {
const toolByName: Record<string, StructuredToolInterface> = {};
for (const tool of tools) {
toolByName[tool.name] = tool;
}
return async (inputs: z.infer<typeof State>) => {
const { messages } = inputs;
if (!messages || messages.length === 0) {
throw new Error("No message found in input");
}
const message = messages.at(-1);
if (!message || !isAIMessage(message) || !message.tool_calls) {
throw new Error("Last message is not an AI message with tool calls");
}
const outputs: ToolMessage[] = [];
for (const toolCall of message.tool_calls) {
if (!toolCall.id) throw new Error("Tool call ID is required");
const tool = toolByName[toolCall.name];
if (!tool) throw new Error(`Tool ${toolCall.name} not found`);
const result = await tool.invoke(toolCall.args);
outputs.push(
new ToolMessage({
content: JSON.stringify(result),
name: toolCall.name,
tool_call_id: toolCall.id,
})
);
}
return { messages: outputs };
};
}
```
!!! note
If you do not want to build this yourself in the future, you can use LangGraph's prebuilt [ToolNode](https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph_prebuilt.ToolNode.html).
:::
## 6. Define the `conditional_edges`
With the tool node added, now you can define the `conditional_edges`.
With the tool node added, now you can define the `conditional_edges`.
**Edges** route the control flow from one node to the next. **Conditional edges** start from a single node and usually contain "if" statements to route to different nodes depending on the current graph state. These functions receive the current graph `state` and return a string or list of strings indicating which node(s) to call next.
:::python
Next, define a router function called `route_tools` that checks for `tool_calls` in the chatbot's output. Provide this function to the graph by calling `add_conditional_edges`, which tells the graph that whenever the `chatbot` node completes to check this function to see where to go next.
:::
:::js
Next, define a router function called `routeTools` that checks for `tool_calls` in the chatbot's output. Provide this function to the graph by calling `addConditionalEdges`, which tells the graph that whenever the `chatbot` node completes to check this function to see where to go next.
:::
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.
:::python
```python
def route_tools(
state: State,
@@ -410,61 +197,10 @@ graph = graph_builder.compile()
!!! note
You can replace this with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition) to be more concise.
:::
:::js
```typescript
import { END, START } from "@langchain/langgraph";
const routeTools = (state: z.infer<typeof State>) => {
/**
* Use as conditional edge to route to the ToolNode if the last message
* has tool calls.
*/
const lastMessage = state.messages.at(-1);
if (
lastMessage &&
isAIMessage(lastMessage) &&
lastMessage.tool_calls?.length
) {
return "tools";
}
/** Otherwise, route to the end. */
return END;
};
const graph = new StateGraph(State)
.addNode("chatbot", chatbot)
// The `routeTools` function returns "tools" if the chatbot asks to use a tool, and "END" if
// it is fine directly responding. This conditional routing defines the main agent loop.
.addNode("tools", createToolNode(tools))
// Start the graph with the chatbot
.addEdge(START, "chatbot")
// The `routeTools` function returns "tools" if the chatbot asks to use a tool, and "END" if
// it is fine directly responding.
.addConditionalEdges("chatbot", routeTools, ["tools", END])
// Any time a tool is called, we need to return to the chatbot
.addEdge("tools", "chatbot")
.compile();
```
!!! note
You can replace this with the prebuilt [toolsCondition](https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph_prebuilt.toolsCondition.html) to be more concise.
:::
You can replace this with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition) to be more concise.
## 7. Visualize the graph (optional)
:::python
You can visualize the graph using the `get_graph` method and one of the "draw" methods, like `draw_ascii` or `draw_png`. The `draw` methods each require additional dependencies.
```python
@@ -477,31 +213,12 @@ except Exception:
pass
```
:::
:::js
You can visualize the graph using the `getGraph` method and render the graph with the `drawMermaidPng` method.
```typescript
import * as fs from "node:fs/promises";
const drawableGraph = await graph.getGraphAsync();
const image = await drawableGraph.drawMermaidPng();
const imageBuffer = new Uint8Array(await image.arrayBuffer());
await fs.writeFile("chatbot-with-tools.png", imageBuffer);
```
:::
![chatbot-with-tools-diagram](chatbot-with-tools.png)
## 8. Ask the bot questions
Now you can ask the chatbot questions outside its training data:
:::python
```python
def stream_graph_updates(user_input: str):
for event in graph.stream({"messages": [{"role": "user", "content": user_input}]}):
@@ -524,7 +241,7 @@ while True:
break
```
```
```
Assistant: [{'text': "To provide you with accurate and up-to-date information about LangGraph, I'll need to search for the latest details. Let me do that for you.", 'type': 'text'}, {'id': 'toolu_01Q588CszHaSvvP2MxRq9zRD', 'input': {'query': 'LangGraph AI tool information'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]
Assistant: [{"url": "https://www.langchain.com/langgraph", "content": "LangGraph sets the foundation for how we can build and scale AI workloads \u2014 from conversational agents, complex task automation, to custom LLM-backed experiences that 'just work'. The next chapter in building complex production-ready features with LLMs is agentic, and with LangGraph and LangSmith, LangChain delivers an out-of-the-box solution ..."}, {"url": "https://github.com/langchain-ai/langgraph", "content": "Overview. LangGraph is a library for building stateful, multi-actor applications with LLMs, used to create agent and multi-agent workflows. Compared to other LLM frameworks, it offers these core benefits: cycles, controllability, and persistence. LangGraph allows you to define flows that involve cycles, essential for most agentic architectures ..."}]
Assistant: Based on the search results, I can provide you with information about LangGraph:
@@ -555,99 +272,18 @@ Assistant: Based on the search results, I can provide you with information about
LangGraph appears to be a significant tool in the evolving landscape of LLM-based application development, offering developers new ways to create more complex, stateful, and interactive AI systems.
Goodbye!
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
```
:::
:::js
```typescript
import readline from "node:readline/promises";
const prompt = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
async function generateText(content: string) {
const stream = await graph.stream(
{ messages: [{ type: "human", content }] },
{ streamMode: "values" }
);
for await (const event of stream) {
const lastMessage = event.messages.at(-1);
if (lastMessage?.getType() === "ai" || lastMessage?.getType() === "tool") {
console.log(`Assistant: ${lastMessage?.text}`);
}
}
}
while (true) {
const human = await prompt.question("User: ");
if (["quit", "exit", "q"].includes(human.trim())) break;
await generateText(human || "What do you know about LangGraph?");
}
prompt.close();
```
```
User: What do you know about LangGraph?
Assistant: I'll search for the latest information about LangGraph for you.
Assistant: [{"title":"Introduction to LangGraph: A Beginner's Guide - Medium","url":"https://medium.com/@cplog/introduction-to-langgraph-a-beginners-guide-14f9be027141","content":"..."}]
Assistant: Based on the search results, I can provide you with information about LangGraph:
LangGraph is a library within the LangChain ecosystem designed for building stateful, multi-actor applications with Large Language Models (LLMs). Here are the key aspects:
**Core Purpose:**
- LangGraph is specifically designed for creating agent and multi-agent workflows
- It provides a framework for defining, coordinating, and executing multiple LLM agents in a structured manner
**Key Features:**
1. **Stateful Graph Architecture**: LangGraph revolves around a stateful graph where each node represents a step in computation, and the graph maintains state that is passed around and updated as the computation progresses
2. **Conditional Edges**: It supports conditional edges, allowing you to dynamically determine the next node to execute based on the current state of the graph
3. **Cycles**: Unlike other LLM frameworks, LangGraph allows you to define flows that involve cycles, which is essential for most agentic architectures
4. **Controllability**: It offers enhanced control over the application flow
5. **Persistence**: The library provides ways to maintain state and persistence in LLM-based applications
**Use Cases:**
- Conversational agents
- Complex task automation
- Custom LLM-backed experiences
- Multi-agent systems that perform complex tasks
**Benefits:**
LangGraph allows developers to focus on the high-level logic of their applications rather than the intricacies of agent coordination, making it easier to build complex, production-ready features with LLMs.
This makes LangGraph a significant tool in the evolving landscape of LLM-based application development.
```
:::
## 9. Use prebuilts
For ease of use, adjust your code to replace the following with LangGraph prebuilt components. These have built in functionality like parallel API execution.
:::python
- `BasicToolNode` is replaced with the prebuilt [ToolNode](https://langchain-ai.github.io/langgraph/reference/prebuilt/#toolnode)
- `route_tools` is replaced with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition)
{!snippets/chat_model_tabs.md!}
<!---
```python
from langchain.chat_models import init_chat_model
llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
```
-->
```python hl_lines="25 30"
from typing import Annotated
@@ -687,46 +323,7 @@ graph_builder.add_edge(START, "chatbot")
graph = graph_builder.compile()
```
:::
:::js
- `createToolNode` is replaced with the prebuilt [ToolNode](https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph_prebuilt.ToolNode.html)
- `routeTools` is replaced with the prebuilt [toolsCondition](https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph_prebuilt.toolsCondition.html)
```typescript
import { TavilySearch } from "@langchain/tavily";
import { ChatOpenAI } from "@langchain/openai";
import { StateGraph, START, MessagesZodState, END } from "@langchain/langgraph";
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
import { z } from "zod";
const State = z.object({ messages: MessagesZodState.shape.messages });
const tools = [new TavilySearch({ maxResults: 2 })];
const llm = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools(tools);
const graph = new StateGraph(State)
.addNode("chatbot", async (state) => ({
messages: [await llm.invoke(state.messages)],
}))
.addNode("tools", new ToolNode(tools))
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
.addEdge("tools", "chatbot")
.addEdge(START, "chatbot")
.compile();
```
:::
**Congratulations!** You've created a conversational agent in LangGraph that can use a search engine to retrieve updated information when needed. Now it can handle a wider range of user queries.
:::python
To inspect all the steps your agent just took, check out this [LangSmith trace](https://smith.langchain.com/public/4fbd7636-25af-4638-9587-5a02fdbb0172/r).
:::
**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
+14 -253
View File
@@ -2,7 +2,7 @@
The chatbot can now [use tools](./2-add-tools.md) to answer user questions, but it does not remember the context of previous interactions. This limits its ability to have coherent, multi-turn conversations.
LangGraph solves this problem through **persistent checkpointing**. If you provide a `checkpointer` when compiling the graph and a `thread_id` when calling your graph, LangGraph automatically saves the state after each step. When you invoke the graph again using the same `thread_id`, the graph loads its saved state, allowing the chatbot to pick up where it left off.
LangGraph solves this problem through **persistent checkpointing**. If you provide a `checkpointer` when compiling the graph and a `thread_id` when calling your graph, LangGraph automatically saves the state after each step. When you invoke the graph again using the same `thread_id`, the graph loads its saved state, allowing the chatbot to pick up where it left off.
We will see later that **checkpointing** is _much_ more powerful than simple chat memory - it lets you save and resume complex state at any time for error recovery, human-in-the-loop workflows, time travel interactions, and more. But first, let's add checkpointing to enable multi-turn conversations.
@@ -14,79 +14,43 @@ We will see later that **checkpointing** is _much_ more powerful than simple cha
Create a `MemorySaver` checkpointer:
:::python
```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.
## 2. Compile the graph
Compile the graph with the provided checkpointer, which will checkpoint the `State` as the graph works through each node:
:::python
```python
``` python
graph = graph_builder.compile(checkpointer=memory)
```
:::
``` python
from IPython.display import Image, display
:::js
```typescript hl_lines="7"
const graph = new StateGraph(State)
.addNode("chatbot", chatbot)
.addNode("tools", new ToolNode(tools))
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
.addEdge("tools", "chatbot")
.addEdge(START, "chatbot")
.compile({ checkpointer: memory });
try:
display(Image(graph.get_graph().draw_mermaid_png()))
except Exception:
# This requires some extra dependencies and is optional
pass
```
:::
## 3. Interact with your chatbot
Now you can interact with your bot!
1. Pick a thread to use as the key for this conversation.
:::python
1. Pick a thread to use as the key for this conversation.
```python
config = {"configurable": {"thread_id": "1"}}
```
:::
:::js
```typescript
const config = { configurable: { thread_id: "1" } };
```
:::
2. Call your chatbot:
:::python
2. Call your chatbot:
```python
user_input = "Hi there! My name is Will."
@@ -110,45 +74,14 @@ Now you can interact with your bot!
Hello Will! It's nice to meet you. How can I assist you today? Is there anything specific you'd like to know or discuss?
```
!!! note
!!! note
The config was provided as the **second positional argument** when calling our graph. It importantly is _not_ nested within the graph inputs (`{'messages': []}`).
:::
:::js
```typescript
const userInput = "Hi there! My name is Will.";
const events = await graph.stream(
{ messages: [{ type: "human", content: userInput }] },
{ configurable: { thread_id: "1" }, streamMode: "values" }
);
for await (const event of events) {
const lastMessage = event.messages.at(-1);
console.log(`${lastMessage?.getType()}: ${lastMessage?.text}`);
}
```
```
human: Hi there! My name is Will.
ai: Hello Will! It's nice to meet you. How can I assist you today? Is there anything specific you'd like to know or discuss?
```
!!! note
The config was provided as the **second parameter** when calling our graph. It importantly is _not_ nested within the graph inputs (`{"messages": []}`).
:::
## 4. Ask a follow up question
Ask a follow up question:
:::python
```python
user_input = "Remember my name?"
@@ -171,37 +104,10 @@ Remember my name?
Of course, I remember your name, Will. I always try to pay attention to important details that users share with me. Is there anything else you'd like to talk about or any questions you have? I'm here to help with a wide range of topics or tasks.
```
:::
:::js
```typescript
const userInput2 = "Remember my name?";
const events2 = await graph.stream(
{ messages: [{ type: "human", content: userInput2 }] },
{ configurable: { thread_id: "1" }, streamMode: "values" }
);
for await (const event of events2) {
const lastMessage = event.messages.at(-1);
console.log(`${lastMessage?.getType()}: ${lastMessage?.text}`);
}
```
```
human: Remember my name?
ai: Yes, your name is Will. How can I help you today?
```
:::
**Notice** that we aren't using an external list for memory: it's all handled by the checkpointer! You can inspect the full execution in this [LangSmith trace](https://smith.langchain.com/public/29ba22b5-6d40-4fbe-8d27-b369e3329c84/r) to see what's going on.
Don't believe me? Try this using a different config.
:::python
```python
# The only difference is we change the `thread_id` here to "2" instead of "1"
events = graph.stream(
@@ -223,36 +129,10 @@ Remember my name?
I apologize, but I don't have any previous context or memory of your name. As an AI assistant, I don't retain information from past conversations. Each interaction starts fresh. Could you please tell me your name so I can address you properly in this conversation?
```
:::
:::js
```typescript hl_lines="3-4"
const events3 = await graph.stream(
{ messages: [{ type: "human", content: userInput2 }] },
// The only difference is we change the `thread_id` here to "2" instead of "1"
{ configurable: { thread_id: "2" }, streamMode: "values" }
);
for await (const event of events3) {
const lastMessage = event.messages.at(-1);
console.log(`${lastMessage?.getType()}: ${lastMessage?.text}`);
}
```
```
human: Remember my name?
ai: I don't have the ability to remember personal information about users between interactions. However, I'm here to help you with any questions or topics you want to discuss!
```
:::
**Notice** that the **only** change we've made is to modify the `thread_id` in the config. See this call's [LangSmith trace](https://smith.langchain.com/public/51a62351-2f0a-4058-91cc-9996c5561428/r) for comparison.
## 5. Inspect the state
:::python
By now, we have made a few checkpoints across two different threads. But what goes into a checkpoint? To inspect a graph's `state` for a given config at any time, call `get_state(config)`.
```python
@@ -268,94 +148,12 @@ StateSnapshot(values={'messages': [HumanMessage(content='Hi there! My name is Wi
snapshot.next # (since the graph ended this turn, `next` is empty. If you fetch a state from within a graph invocation, next tells which node will execute next)
```
:::
:::js
By now, we have made a few checkpoints across two different threads. But what goes into a checkpoint? To inspect a graph's `state` for a given config at any time, call `getState(config)`.
```typescript
await graph.getState({ configurable: { thread_id: "1" } });
```
```typescript
{
values: {
messages: [
HumanMessage {
"id": "32fabcef-b3b8-481f-8bcb-fd83399a5f8d",
"content": "Hi there! My name is Will.",
"additional_kwargs": {},
"response_metadata": {}
},
AIMessage {
"id": "chatcmpl-BrPbTsCJbVqBvXWySlYoTJvM75Kv8",
"content": "Hello Will! How can I assist you today?",
"additional_kwargs": {},
"response_metadata": {},
"tool_calls": [],
"invalid_tool_calls": []
},
HumanMessage {
"id": "561c3aad-f8fc-4fac-94a6-54269a220856",
"content": "Remember my name?",
"additional_kwargs": {},
"response_metadata": {}
},
AIMessage {
"id": "chatcmpl-BrPbU4BhhsUikGbW37hYuF5vvnnE2",
"content": "Yes, I remember your name, Will! How can I help you today?",
"additional_kwargs": {},
"response_metadata": {},
"tool_calls": [],
"invalid_tool_calls": []
}
]
},
next: [],
tasks: [],
metadata: {
source: 'loop',
step: 4,
parents: {},
thread_id: '1'
},
config: {
configurable: {
thread_id: '1',
checkpoint_id: '1f05cccc-9bb6-6270-8004-1d2108bcec77',
checkpoint_ns: ''
}
},
createdAt: '2025-07-09T13:58:27.607Z',
parentConfig: {
configurable: {
thread_id: '1',
checkpoint_ns: '',
checkpoint_id: '1f05cccc-78fa-68d0-8003-ffb01a76b599'
}
}
}
```
```typescript
import * as assert from "node:assert";
// Since the graph ended this turn, `next` is empty.
// If you fetch a state from within a graph invocation, next tells which node will execute next)
assert.deepEqual(snapshot.next, []);
```
:::
The snapshot above contains the current state values, corresponding config, and the `next` node to process. In our case, the graph has reached an `END` state, so `next` is empty.
**Congratulations!** Your chatbot can now maintain conversation state across sessions thanks to LangGraph's checkpointing system. This opens up exciting possibilities for more natural, contextual interactions. LangGraph's checkpointing even handles **arbitrarily complex graph states**, which is much more expressive and powerful than simple chat memory.
Check out the code snippet below to review the graph from this tutorial:
:::python
{!snippets/chat_model_tabs.md!}
<!---
@@ -406,43 +204,6 @@ memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
:::
:::js
```typescript hl_lines="16 26"
import { END, MessagesZodState, START } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { TavilySearch } from "@langchain/tavily";
import { MemorySaver } from "@langchain/langgraph";
import { StateGraph } from "@langchain/langgraph";
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
import { z } from "zod";
const State = z.object({
messages: MessagesZodState.shape.messages,
});
const tools = [new TavilySearch({ maxResults: 2 })];
const llm = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools(tools);
// highlight-next-line
const memory = new MemorySaver();
const graph = new StateGraph(State)
.addNode("chatbot", async (state) => ({
messages: [await llm.invoke(state.messages)],
}))
.addNode("tools", new ToolNode(tools))
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
.addEdge("tools", "chatbot")
.addEdge(START, "chatbot")
// highlight-next-line
.compile({ checkpointer: memory });
```
:::
## Next steps
In the next tutorial, you will [add human-in-the-loop to the chatbot](./4-human-in-the-loop.md) to handle situations where it may need guidance or verification before proceeding.
@@ -2,15 +2,7 @@
Agents can be unreliable and may need human input to successfully accomplish tasks. Similarly, for some actions, you may want to require human approval before running to ensure that everything is running as intended.
LangGraph's [persistence](../../concepts/persistence.md) layer supports **human-in-the-loop** workflows, allowing execution to pause and resume based on user feedback. The primary interface to this functionality is the [`interrupt`](../../how-tos/human_in_the_loop/add-human-in-the-loop.md) function. Calling `interrupt` inside a node will pause execution. Execution can be resumed, together with new input from a human, by passing in a [Command](../../concepts/low_level.md#command).
:::python
`interrupt` is ergonomically similar to Python's built-in `input()`, [with some caveats](../../how-tos/human_in_the_loop/add-human-in-the-loop.md).
:::
:::js
`interrupt` is ergonomically similar to Node.js's built-in `readline.question()` function, [with some caveats](../../how-tos/human_in_the_loop/add-human-in-the-loop.md).
:::
LangGraph's [persistence](../../concepts/persistence.md) layer supports **human-in-the-loop** workflows, allowing execution to pause and resume based on user feedback. The primary interface to this functionality is the [`interrupt`](../../how-tos/human_in_the_loop/add-human-in-the-loop.md) function. Calling `interrupt` inside a node will pause execution. Execution can be resumed, together with new input from a human, by passing in a [Command](../../concepts/low_level.md#command). `interrupt` is ergonomically similar to Python's built-in `input()`, [with some caveats](../../how-tos/human_in_the_loop/add-human-in-the-loop.md).
!!! note
@@ -22,7 +14,6 @@ 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!}
<!---
@@ -33,22 +24,9 @@ llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
```
-->
:::
:::js
```typescript
// Add your API key here
process.env.ANTHROPIC_API_KEY = "YOUR_API_KEY";
```
:::
We can now incorporate it into our `StateGraph` with an additional tool:
:::python
```python hl_lines="12 19 20 21 22 23"
``` python hl_lines="12 19 20 21 22 23"
from typing import Annotated
from langchain_tavily import TavilySearch
@@ -98,54 +76,6 @@ graph_builder.add_edge("tools", "chatbot")
graph_builder.add_edge(START, "chatbot")
```
:::
:::js
```typescript hl_lines="1 7-19"
import { interrupt, MessagesZodState } from "@langchain/langgraph";
import { ChatAnthropic } from "@langchain/anthropic";
import { TavilySearch } from "@langchain/tavily";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const humanAssistance = tool(
async ({ query }) => {
const humanResponse = interrupt({ query });
return humanResponse.data;
},
{
name: "humanAssistance",
description: "Request assistance from a human.",
schema: z.object({
query: z.string().describe("Human readable question for the human"),
}),
}
);
const searchTool = new TavilySearch({ maxResults: 2 });
const tools = [searchTool, humanAssistance];
const llmWithTools = new ChatAnthropic({
model: "claude-3-5-sonnet-latest",
}).bindTools(tools);
async function chatbot(state: z.infer<typeof MessagesZodState>) {
const message = await llmWithTools.invoke(state.messages);
// Because we will be interrupting during tool execution,
// we disable parallel tool calling to avoid repeating any
// tool invocations when we resume.
if (message.tool_calls && message.tool_calls.length > 1) {
throw new Error("Multiple tool calls not supported with interrupts");
}
return { messages: message };
}
```
:::
!!! tip
For more information and examples of human-in-the-loop workflows, see [Human-in-the-loop](../../concepts/human_in_the_loop.md).
@@ -154,41 +84,17 @@ async function chatbot(state: z.infer<typeof MessagesZodState>) {
We compile the graph with a checkpointer, as before:
:::python
```python
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
:::
:::js
```typescript hl_lines="3 11"
import { StateGraph, MemorySaver, START, END } from "@langchain/langgraph";
const memory = new MemorySaver();
const graph = new StateGraph(MessagesZodState)
.addNode("chatbot", chatbot)
.addNode("tools", new ToolNode(tools))
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
.addEdge("tools", "chatbot")
.addEdge(START, "chatbot")
.compile({ checkpointer: memory });
```
:::
## 3. Visualize the graph (optional)
Visualizing the graph, you get the same layout as before just with the added tool!
:::python
```python
``` python
from IPython.display import Image, display
try:
@@ -198,30 +104,12 @@ except Exception:
pass
```
:::
:::js
```typescript
import * as fs from "node:fs/promises";
const drawableGraph = await graph.getGraphAsync();
const image = await drawableGraph.drawMermaidPng();
const imageBuffer = new Uint8Array(await image.arrayBuffer());
await fs.writeFile("chatbot-with-tools.png", imageBuffer);
```
:::
![chatbot-with-tools-diagram](chatbot-with-tools.png)
## 4. Prompt the chatbot
Now, prompt the chatbot with a question that will engage the new `human_assistance` tool:
:::python
```python
user_input = "I need some expert guidance for building an AI agent. Could you request assistance for me?"
config = {"configurable": {"thread_id": "1"}}
@@ -250,58 +138,8 @@ Tool Calls:
query: A user is requesting expert guidance for building an AI agent. Could you please provide some expert advice or resources on this topic?
```
:::
:::js
```typescript
import { isAIMessage } from "@langchain/core/messages";
const userInput =
"I need some expert guidance for building an AI agent. Could you request assistance for me?";
const events = await graph.stream(
{ messages: [{ role: "user", content: userInput }] },
{ configurable: { thread_id: "1" }, streamMode: "values" }
);
for await (const event of events) {
if ("messages" in event) {
const lastMessage = event.messages.at(-1);
console.log(`[${lastMessage?.getType()}]: ${lastMessage?.text}`);
if (
lastMessage &&
isAIMessage(lastMessage) &&
lastMessage.tool_calls?.length
) {
console.log("Tool calls:", lastMessage.tool_calls);
}
}
}
```
```
[human]: I need some expert guidance for building an AI agent. Could you request assistance for me?
[ai]: I'll help you request human assistance for guidance on building an AI agent.
Tool calls: [
{
name: 'humanAssistance',
args: {
query: 'I would like expert guidance on building an AI agent. Could you please provide assistance with this topic?'
},
id: 'toolu_01Bpxc8rFVMhSaRosS6b85Ts',
type: 'tool_call'
}
]
```
:::
The chatbot generated a tool call, but then execution has been interrupted. If you inspect the graph state, you see that it stopped at the tools node:
:::python
```python
snapshot = graph.get_state(config)
snapshot.next
@@ -311,25 +149,8 @@ snapshot.next
('tools',)
```
:::
:::js
```typescript
const snapshot = await graph.getState({ configurable: { thread_id: "1" } });
snapshot.next;
```
```json
["tools"]
```
:::
!!! info Additional information
:::python
Take a closer look at the `human_assistance` tool:
```python
@@ -341,40 +162,12 @@ snapshot.next;
```
Similar to Python's built-in `input()` function, calling `interrupt` inside the tool will pause execution. Progress is persisted based on the [checkpointer](../../concepts/persistence.md#checkpointer-libraries); so if it is persisting with Postgres, it can resume at any time as long as the database is alive. In this example, it is persisting with the in-memory checkpointer and can resume any time if the Python kernel is running.
:::
:::js
Take a closer look at the `humanAssistance` tool:
```typescript hl_lines="3"
const humanAssistance = tool(
async ({ query }) => {
const humanResponse = interrupt({ query });
return humanResponse.data;
},
{
name: "humanAssistance",
description: "Request assistance from a human.",
schema: z.object({
query: z.string().describe("Human readable question for the human"),
}),
},
);
```
Calling `interrupt` inside the tool will pause execution. Progress is persisted based on the [checkpointer](../../concepts/persistence.md#checkpointer-libraries); so if it is persisting with Postgres, it can resume at any time as long as the database is alive. In this example, it is persisting with the in-memory checkpointer and can resume any time if the JavaScript runtime is running.
:::
## 5. Resume execution
To resume execution, pass a [`Command`](../../concepts/low_level.md#command) object containing data expected by the tool. The format of this data can be customized based on needs.
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
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."
" It's much more reliable and extensible than simple autonomous agents."
@@ -422,60 +215,12 @@ If you'd like more specific information about LangGraph or have any questions ab
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
```
:::
:::js
For this example, use an object with a key `"data"`:
```typescript
import { Command } from "@langchain/langgraph";
const humanResponse =
"We, the experts are here to help! We'd recommend you check out LangGraph to build your agent." +
" It's much more reliable and extensible than simple autonomous agents.";
const humanCommand = new Command({ resume: { data: humanResponse } });
const resumeEvents = await graph.stream(humanCommand, {
configurable: { thread_id: "1" },
streamMode: "values",
});
for await (const event of resumeEvents) {
if ("messages" in event) {
const lastMessage = event.messages.at(-1);
console.log(`[${lastMessage?.getType()}]: ${lastMessage?.text}`);
}
}
```
```
[tool]: We, the experts are here to help! We'd recommend you check out LangGraph to build your agent. It's much more reliable and extensible than simple autonomous agents.
[ai]: Thank you for your patience. I've received some expert advice regarding your request for guidance on building an AI agent. Here's what the experts have suggested:
The experts recommend that you look into LangGraph for building your AI agent. They mention that LangGraph is a more reliable and extensible option compared to simple autonomous agents.
LangGraph is likely a framework or library designed specifically for creating AI agents with advanced capabilities. Here are a few points to consider based on this recommendation:
1. Reliability: The experts emphasize that LangGraph is more reliable than simpler autonomous agent approaches. This could mean it has better stability, error handling, or consistent performance.
2. Extensibility: LangGraph is described as more extensible, which suggests that it probably offers a flexible architecture that allows you to easily add new features or modify existing ones as your agent's requirements evolve.
3. Advanced capabilities: Given that it's recommended over "simple autonomous agents," LangGraph likely provides more sophisticated tools and techniques for building complex AI agents.
...
```
:::
The input has been received and processed as a tool message. Review this call's [LangSmith trace](https://smith.langchain.com/public/9f0f87e3-56a7-4dde-9c76-b71675624e91/r) to see the exact work that was done in the above call. Notice that the state is loaded in the first step so that our chatbot can continue where it left off.
**Congratulations!** You've used an `interrupt` to add human-in-the-loop execution to your chatbot, allowing for human oversight and intervention when needed. This opens up the potential UIs you can create with your AI systems. Since you have already added a **checkpointer**, as long as the underlying persistence layer is running, the graph can be paused **indefinitely** and resumed at any time as if nothing had happened.
Check out the code snippet below to review the graph from this tutorial:
:::python
{!snippets/chat_model_tabs.md!}
```python
@@ -527,73 +272,6 @@ memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
:::
:::js
```typescript
import {
interrupt,
MessagesZodState,
StateGraph,
MemorySaver,
START,
END,
} from "@langchain/langgraph";
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
import { isAIMessage } from "@langchain/core/messages";
import { ChatAnthropic } from "@langchain/anthropic";
import { TavilySearch } from "@langchain/tavily";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const humanAssistance = tool(
async ({ query }) => {
const humanResponse = interrupt({ query });
return humanResponse.data;
},
{
name: "humanAssistance",
description: "Request assistance from a human.",
schema: z.object({
query: z.string().describe("Human readable question for the human"),
}),
}
);
const searchTool = new TavilySearch({ maxResults: 2 });
const tools = [searchTool, humanAssistance];
const llmWithTools = new ChatAnthropic({
model: "claude-3-5-sonnet-latest",
}).bindTools(tools);
const chatbot = async (state: z.infer<typeof MessagesZodState>) => {
const message = await llmWithTools.invoke(state.messages);
// Because we will be interrupting during tool execution,
// we disable parallel tool calling to avoid repeating any
// tool invocations when we resume.
if (message.tool_calls && message.tool_calls.length > 1) {
throw new Error("Multiple tool calls not supported with interrupts");
}
return { messages: message };
};
const memory = new MemorySaver();
const graph = new StateGraph(MessagesZodState)
.addNode("chatbot", chatbot)
.addNode("tools", new ToolNode(tools))
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
.addEdge("tools", "chatbot")
.addEdge(START, "chatbot")
.compile({ checkpointer: memory });
```
:::
## Next steps
So far, the tutorial examples have relied on a simple state with one entry: a list of messages. You can go far with this simple state, but if you want to define complex behavior without relying on the message list, you can [add additional fields to the state](./5-customize-state.md).
So far, the tutorial examples have relied on a simple state with one entry: a list of messages. You can go far with this simple state, but if you want to define complex behavior without relying on the message list, you can [add additional fields to the state](./5-customize-state.md).
@@ -10,8 +10,6 @@ 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
@@ -28,34 +26,13 @@ class State(TypedDict):
birthday: str
```
:::
:::js
```typescript
import { MessagesZodState } from "@langchain/langgraph";
import { z } from "zod";
const State = z.object({
messages: MessagesZodState.shape.messages,
// highlight-next-line
name: z.string(),
// highlight-next-line
birthday: z.string(),
});
```
:::
Adding this information to the state makes it easily accessible by other graph nodes (like a downstream node that stores or processes the information), as well as the graph's persistence layer.
## 2. Update the state inside the tool
:::python
Now, populate the state keys inside of the `human_assistance` tool. This allows a human to review the information before it is stored in the state. Use [`Command`](../../concepts/low_level.md#using-inside-tools) to issue a state update from inside the tool.
```python
``` python
from langchain_core.messages import ToolMessage
from langchain_core.tools import InjectedToolCallId, tool
@@ -99,78 +76,10 @@ def human_assistance(
return Command(update=state_update)
```
:::
:::js
Now, populate the state keys inside of the `humanAssistance` tool. This allows a human to review the information before it is stored in the state. Use [`Command`](../../concepts/low_level.md#using-inside-tools) to issue a state update from inside the tool.
```typescript
import { tool } from "@langchain/core/tools";
import { ToolMessage } from "@langchain/core/messages";
import { Command, interrupt } from "@langchain/langgraph";
const humanAssistance = tool(
async (input, config) => {
// Note that because we are generating a ToolMessage for a state update,
// we generally require the ID of the corresponding tool call.
// This is available in the tool's config.
const toolCallId = config?.toolCall?.id as string | undefined;
if (!toolCallId) throw new Error("Tool call ID is required");
const humanResponse = await interrupt({
question: "Is this correct?",
name: input.name,
birthday: input.birthday,
});
// We explicitly update the state with a ToolMessage inside the tool.
const stateUpdate = (() => {
// If the information is correct, update the state as-is.
if (humanResponse.correct?.toLowerCase().startsWith("y")) {
return {
name: input.name,
birthday: input.birthday,
messages: [
new ToolMessage({ content: "Correct", tool_call_id: toolCallId }),
],
};
}
// Otherwise, receive information from the human reviewer.
return {
name: humanResponse.name || input.name,
birthday: humanResponse.birthday || input.birthday,
messages: [
new ToolMessage({
content: `Made a correction: ${JSON.stringify(humanResponse)}`,
tool_call_id: toolCallId,
}),
],
};
})();
// We return a Command object in the tool to update our state.
return new Command({ update: stateUpdate });
},
{
name: "humanAssistance",
description: "Request assistance from a human.",
schema: z.object({
name: z.string().describe("The name of the entity"),
birthday: z.string().describe("The birthday/release date of the entity"),
}),
}
);
```
:::
The rest of the graph stays the same.
## 3. Prompt the chatbot
:::python
Prompt the chatbot to look up the "birthday" of the LangGraph library and direct the chatbot to reach out to the `human_assistance` tool once it has the required information. By setting `name` and `birthday` in the arguments for the tool, you force the chatbot to generate proposals for these fields.
```python
@@ -190,51 +99,6 @@ for event in events:
event["messages"][-1].pretty_print()
```
:::
:::js
Prompt the chatbot to look up the "birthday" of the LangGraph library and direct the chatbot to reach out to the `humanAssistance` tool once it has the required information. By setting `name` and `birthday` in the arguments for the tool, you force the chatbot to generate proposals for these fields.
```typescript
import { isAIMessage } from "@langchain/core/messages";
const userInput =
"Can you look up when LangGraph was released? " +
"When you have the answer, use the humanAssistance tool for review.";
const events = await graph.stream(
{ messages: [{ role: "user", content: userInput }] },
{ configurable: { thread_id: "1" }, streamMode: "values" }
);
for await (const event of events) {
if ("messages" in event) {
const lastMessage = event.messages.at(-1);
console.log(
"=".repeat(32),
`${lastMessage?.getType()} Message`,
"=".repeat(32)
);
console.log(lastMessage?.text);
if (
lastMessage &&
isAIMessage(lastMessage) &&
lastMessage.tool_calls?.length
) {
console.log("Tool Calls:");
for (const call of lastMessage.tool_calls) {
console.log(` ${call.name} (${call.id})`);
console.log(` Args: ${JSON.stringify(call.args)}`);
}
}
}
}
```
:::
```
================================ Human Message =================================
@@ -262,20 +126,12 @@ Tool Calls:
birthday: 2023-01-01
```
:::python
We've hit the `interrupt` in the `human_assistance` tool again.
:::
:::js
We've hit the `interrupt` in the `humanAssistance` tool again.
:::
## 4. Add human assistance
The chatbot failed to identify the correct date, so supply it with information:
:::python
```python
human_command = Command(
resume={
@@ -290,53 +146,6 @@ for event in events:
event["messages"][-1].pretty_print()
```
:::
:::js
```typescript
import { Command } from "@langchain/langgraph";
const humanCommand = new Command({
resume: {
name: "LangGraph",
birthday: "Jan 17, 2024",
},
});
const resumeEvents = await graph.stream(humanCommand, {
configurable: { thread_id: "1" },
streamMode: "values",
});
for await (const event of resumeEvents) {
if ("messages" in event) {
const lastMessage = event.messages.at(-1);
console.log(
"=".repeat(32),
`${lastMessage?.getType()} Message`,
"=".repeat(32)
);
console.log(lastMessage?.text);
if (
lastMessage &&
isAIMessage(lastMessage) &&
lastMessage.tool_calls?.length
) {
console.log("Tool Calls:");
for (const call of lastMessage.tool_calls) {
console.log(` ${call.name} (${call.id})`);
console.log(` Args: ${JSON.stringify(call.args)}`);
}
}
}
}
```
:::
```
================================== Ai Message ==================================
@@ -366,8 +175,6 @@ 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)
@@ -378,34 +185,13 @@ snapshot = graph.get_state(config)
{'name': 'LangGraph', 'birthday': 'Jan 17, 2024'}
```
:::
:::js
```typescript
const snapshot = await graph.getState(config);
const relevantState = Object.fromEntries(
Object.entries(snapshot.values).filter(([k]) =>
["name", "birthday"].includes(k)
)
);
```
```
{ name: 'LangGraph', birthday: 'Jan 17, 2024' }
```
:::
This makes them easily accessible to downstream nodes (e.g., a node that further processes or stores the information).
## 5. Manually update the state
:::python
LangGraph gives a high degree of control over the application state. For instance, at any point (including when interrupted), you can manually override a key using `graph.update_state`:
```python
``` python
graph.update_state(config, {"name": "LangGraph (library)"})
```
@@ -415,36 +201,11 @@ graph.update_state(config, {"name": "LangGraph (library)"})
'checkpoint_id': '1efd4ec5-cf69-6352-8006-9278f1730162'}}
```
:::
:::js
LangGraph gives a high degree of control over the application state. For instance, at any point (including when interrupted), you can manually override a key using `graph.updateState`:
```typescript
await graph.updateState(
{ configurable: { thread_id: "1" } },
{ name: "LangGraph (library)" }
);
```
```typescript
{
configurable: {
thread_id: '1',
checkpoint_ns: '',
checkpoint_id: '1efd4ec5-cf69-6352-8006-9278f1730162'
}
}
```
:::
## 6. View the new value
:::python
If you call `graph.get_state`, you can see the new value is reflected:
```python
``` python
snapshot = graph.get_state(config)
{k: v for k, v in snapshot.values.items() if k in ("name", "birthday")}
@@ -454,35 +215,12 @@ snapshot = graph.get_state(config)
{'name': 'LangGraph (library)', 'birthday': 'Jan 17, 2024'}
```
:::
:::js
If you call `graph.getState`, you can see the new value is reflected:
```typescript
const updatedSnapshot = await graph.getState(config);
const updatedRelevantState = Object.fromEntries(
Object.entries(updatedSnapshot.values).filter(([k]) =>
["name", "birthday"].includes(k)
)
);
```
```typescript
{ name: 'LangGraph (library)', birthday: 'Jan 17, 2024' }
```
:::
Manual state updates will [generate a trace](https://smith.langchain.com/public/7ebb7827-378d-49fe-9f6c-5df0e90086c8/r) in LangSmith. If desired, they can also be used to [control human-in-the-loop workflows](../../how-tos/human_in_the_loop/add-human-in-the-loop.md). Use of the `interrupt` function is generally recommended instead, as it allows data to be transmitted in a human-in-the-loop interaction independently of state updates.
**Congratulations!** You've added custom keys to the state to facilitate a more complex workflow, and learned how to generate state updates from inside tools.
Check out the code snippet below to review the graph from this tutorial:
:::python
{!snippets/chat_model_tabs.md!}
<!---
@@ -567,111 +305,7 @@ memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
:::
:::js
```typescript
import {
Command,
interrupt,
MessagesZodState,
MemorySaver,
StateGraph,
END,
START,
} from "@langchain/langgraph";
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
import { ChatAnthropic } from "@langchain/anthropic";
import { TavilySearch } from "@langchain/tavily";
import { ToolMessage } from "@langchain/core/messages";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const State = z.object({
messages: MessagesZodState.shape.messages,
name: z.string(),
birthday: z.string(),
});
const humanAssistance = tool(
async (input, config) => {
// Note that because we are generating a ToolMessage for a state update, we
// generally require the ID of the corresponding tool call. This is available
// in the tool's config.
const toolCallId = config?.toolCall?.id as string | undefined;
if (!toolCallId) throw new Error("Tool call ID is required");
const humanResponse = await interrupt({
question: "Is this correct?",
name: input.name,
birthday: input.birthday,
});
// We explicitly update the state with a ToolMessage inside the tool.
const stateUpdate = (() => {
// If the information is correct, update the state as-is.
if (humanResponse.correct?.toLowerCase().startsWith("y")) {
return {
name: input.name,
birthday: input.birthday,
messages: [
new ToolMessage({ content: "Correct", tool_call_id: toolCallId }),
],
};
}
// Otherwise, receive information from the human reviewer.
return {
name: humanResponse.name || input.name,
birthday: humanResponse.birthday || input.birthday,
messages: [
new ToolMessage({
content: `Made a correction: ${JSON.stringify(humanResponse)}`,
tool_call_id: toolCallId,
}),
],
};
})();
// We return a Command object in the tool to update our state.
return new Command({ update: stateUpdate });
},
{
name: "humanAssistance",
description: "Request assistance from a human.",
schema: z.object({
name: z.string().describe("The name of the entity"),
birthday: z.string().describe("The birthday/release date of the entity"),
}),
}
);
const searchTool = new TavilySearch({ maxResults: 2 });
const tools = [searchTool, humanAssistance];
const llmWithTools = new ChatAnthropic({
model: "claude-3-5-sonnet-latest",
}).bindTools(tools);
const memory = new MemorySaver();
const chatbot = async (state: z.infer<typeof State>) => {
const message = await llmWithTools.invoke(state.messages);
return { messages: message };
};
const graph = new StateGraph(State)
.addNode("chatbot", chatbot)
.addNode("tools", new ToolNode(tools))
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
.addEdge("tools", "chatbot")
.addEdge(START, "chatbot")
.compile({ checkpointer: memory });
```
:::
## Next steps
There's one more concept to review before finishing the LangGraph basics tutorials: connecting `checkpointing` and `state updates` to [time travel](./6-time-travel.md).
There's one more concept to review before finishing the LangGraph basics tutorials: connecting `checkpointing` and `state updates` to [time travel](./6-time-travel.md).
+17 -341
View File
@@ -4,7 +4,7 @@ In a typical chatbot workflow, the user interacts with the bot one or more times
What if you want a user to be able to start from a previous response and explore a different outcome? Or what if you want users to be able to rewind your chatbot's work to fix mistakes or try a different strategy, something that is common in applications like autonomous software engineers?
You can create these types of experiences using LangGraph's built-in **time travel** functionality.
You can create these types of experiences using LangGraph's built-in **time travel** functionality.
!!! note
@@ -12,15 +12,7 @@ You can create these types of experiences using LangGraph's built-in **time trav
## 1. Rewind your graph
:::python
Rewind your graph by fetching a checkpoint using the graph's `get_state_history` method. You can then resume execution at this previous point in time.
:::
:::js
Rewind your graph by fetching a checkpoint using the graph's `getStateHistory` method. You can then resume execution at this previous point in time.
:::
:::python
{!snippets/chat_model_tabs.md!}
@@ -72,49 +64,11 @@ memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
:::
:::js
```typescript
import {
StateGraph,
START,
END,
MessagesZodState,
MemorySaver,
} from "@langchain/langgraph";
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
import { TavilySearch } from "@langchain/tavily";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
const State = z.object({ messages: MessagesZodState.shape.messages });
const tools = [new TavilySearch({ maxResults: 2 })];
const llmWithTools = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools(tools);
const memory = new MemorySaver();
const graph = new StateGraph(State)
.addNode("chatbot", async (state) => ({
messages: [await llmWithTools.invoke(state.messages)],
}))
.addNode("tools", new ToolNode(tools))
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
.addEdge("tools", "chatbot")
.addEdge(START, "chatbot")
.compile({ checkpointer: memory });
```
:::
## 2. Add steps
Add steps to your graph. Every step will be checkpointed in its state history:
:::python
```python
``` python
config = {"configurable": {"thread_id": "1"}}
events = graph.stream(
{
@@ -205,7 +159,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 users 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:
@@ -223,140 +177,11 @@ Building an autonomous agent is an iterative process, so be prepared to refine a
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
```
:::
:::js
```typescript
import { randomUUID } from "node:crypto";
const threadId = randomUUID();
let iter = 0;
for (const userInput of [
"I'm learning LangGraph. Could you do some research on it for me?",
"Ya that's helpful. Maybe I'll build an autonomous agent with it!",
]) {
iter += 1;
console.log(`\n--- Conversation Turn ${iter} ---\n`);
const events = await graph.stream(
{ messages: [{ role: "user", content: userInput }] },
{ configurable: { thread_id: threadId }, streamMode: "values" }
);
for await (const event of events) {
if ("messages" in event) {
const lastMessage = event.messages.at(-1);
console.log(
"=".repeat(32),
`${lastMessage?.getType()} Message`,
"=".repeat(32)
);
console.log(lastMessage?.text);
}
}
}
```
```
--- Conversation Turn 1 ---
================================ human Message ================================
I'm learning LangGraph.js. Could you do some research on it for me?
================================ ai Message ================================
I'll search for information about LangGraph.js for you.
================================ tool Message ================================
{
"query": "LangGraph.js framework TypeScript langchain what is it tutorial guide",
"follow_up_questions": null,
"answer": null,
"images": [],
"results": [
{
"url": "https://techcommunity.microsoft.com/blog/educatordeveloperblog/an-absolute-beginners-guide-to-langgraph-js/4212496",
"title": "An Absolute Beginner's Guide to LangGraph.js",
"content": "(...)",
"score": 0.79369855,
"raw_content": null
},
{
"url": "https://langchain-ai.github.io/langgraphjs/",
"title": "LangGraph.js",
"content": "(...)",
"score": 0.78154784,
"raw_content": null
}
],
"response_time": 2.37
}
================================ ai Message ================================
Let me provide you with an overview of LangGraph.js based on the search results:
LangGraph.js is a JavaScript/TypeScript library that's part of the LangChain ecosystem, specifically designed for creating and managing complex LLM (Large Language Model) based workflows. Here are the key points about LangGraph.js:
1. Purpose:
- It's a low-level orchestration framework for building controllable agents
- Particularly useful for creating agentic workflows where LLMs decide the course of action based on current state
- Helps model workflows as graphs with nodes and edges
(...)
--- Conversation Turn 2 ---
================================ human Message ================================
Ya that's helpful. Maybe I'll build an autonomous agent with it!
================================ ai Message ================================
Let me search for specific information about building autonomous agents with LangGraph.js.
================================ tool Message ================================
{
"query": "how to build autonomous agents with LangGraph.js examples tutorial react agent",
"follow_up_questions": null,
"answer": null,
"images": [],
"results": [
{
"url": "https://ai.google.dev/gemini-api/docs/langgraph-example",
"title": "ReAct agent from scratch with Gemini 2.5 and LangGraph",
"content": "(...)",
"score": 0.7602419,
"raw_content": null
},
{
"url": "https://www.youtube.com/watch?v=ZfjaIshGkmk",
"title": "Build Autonomous AI Agents with ReAct and LangGraph Tools",
"content": "(...)",
"score": 0.7471924,
"raw_content": null
}
],
"response_time": 1.98
}
================================ ai Message ================================
Based on the search results, I can provide you with a practical overview of how to build an autonomous agent with LangGraph.js. Here's what you need to know:
1. Basic Structure for Building an Agent:
- LangGraph.js provides a ReAct (Reason + Act) pattern implementation
- The basic components include:
- State management for conversation history
- Nodes for different actions
- Edges for decision-making flow
- Tools for specific functionalities
(...)
```
:::
## 3. Replay the full state history
Now that you have added steps to the chatbot, you can `replay` the full state history to see everything that occurred.
:::python
```python
``` python
to_replay = None
for state in graph.get_state_history(config):
print("Num Messages: ", len(state.values["messages"]), "Next: ", state.next)
@@ -389,61 +214,10 @@ Num Messages: 0 Next: ('__start__',)
--------------------------------------------------------------------------------
```
:::
:::js
```typescript
import type { StateSnapshot } from "@langchain/langgraph";
let toReplay: StateSnapshot | undefined;
for await (const state of graph.getStateHistory({
configurable: { thread_id: threadId },
})) {
console.log(
`Num Messages: ${state.values.messages.length}, Next: ${JSON.stringify(
state.next
)}`
);
console.log("-".repeat(80));
if (state.values.messages.length === 6) {
// We are somewhat arbitrarily selecting a specific state based on the number of chat messages in the state.
toReplay = state;
}
}
```
```
Num Messages: 8, Next: []
--------------------------------------------------------------------------------
Num Messages: 7, Next: ["chatbot"]
--------------------------------------------------------------------------------
Num Messages: 6, Next: ["tools"]
--------------------------------------------------------------------------------
Num Messages: 5, Next: ["chatbot"]
--------------------------------------------------------------------------------
Num Messages: 4, Next: ["__start__"]
--------------------------------------------------------------------------------
Num Messages: 4, Next: []
--------------------------------------------------------------------------------
Num Messages: 3, Next: ["chatbot"]
--------------------------------------------------------------------------------
Num Messages: 2, Next: ["tools"]
--------------------------------------------------------------------------------
Num Messages: 1, Next: ["chatbot"]
--------------------------------------------------------------------------------
Num Messages: 0, Next: ["__start__"]
--------------------------------------------------------------------------------
```
:::
Checkpoints are saved for every step of the graph. This **spans invocations** so you can rewind across a full thread's history.
Checkpoints are saved for every step of the graph. This __spans invocations__ so you can rewind across a full thread's history.
## Resume from a checkpoint
:::python
Resume from the `to_replay` state, which is after the `chatbot` node in the second graph invocation. Resuming from this point will call the **action** node next.
```python
@@ -456,37 +230,12 @@ print(to_replay.config)
{'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1efd43e3-0c1f-6c4e-8006-891877d65740'}}
```
:::
:::js
Resume from the `toReplay` state, which is after the `chatbot` node in one of the graph invocations. Resuming from this point will call the next scheduled node.
```typescript
console.log(toReplay.next);
console.log(toReplay.config);
```
```
["tools"]
{
configurable: {
thread_id: "007708b8-ea9b-4ff7-a7ad-3843364dbf75",
checkpoint_ns: "",
checkpoint_id: "1efd43e3-0c1f-6c4e-8006-891877d65740"
}
}
```
:::
## 4. Load a state from a moment-in-time
:::python
The checkpoint's `to_replay.config` contains a `checkpoint_id` timestamp. Providing this `checkpoint_id` value tells LangGraph's checkpointer to **load** the state from that moment in time.
```python
``` python
# The `checkpoint_id` in the `to_replay.config` corresponds to a state we've persisted to our checkpointer.
for event in graph.stream(None, to_replay.config, stream_mode="values"):
if "messages" in event:
@@ -505,16 +254,19 @@ 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 users 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:
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:
1. Multi-Tool Agents: LangGraph is particularly well-suited for creating autonomous agents that can use multiple tools. This allows your agent to have a diverse set of capabilities and choose the right tool for each task.
1. Multi-Tool Agents:
LangGraph is well-suited for building autonomous agents that can use multiple tools. This allows your agent to have a variety of capabilities and choose the appropriate tool based on the task at hand.
2. Integration with Large Language Models (LLMs): You can combine LangGraph with powerful LLMs like Gemini 2.0 to create more intelligent and capable agents. The LLM can serve as the "brain" of your agent, making decisions and generating responses.
2. Integration with Large Language Models (LLMs):
There's a tutorial that specifically mentions using Gemini 2.0 (Google's LLM) with LangGraph to build autonomous agents. This suggests that LangGraph can be integrated with various LLMs, giving you flexibility in choosing the language model that best fits your needs.
3. Workflow Management: LangGraph excels at managing complex, multi-step AI workflows. This is crucial for autonomous agents that need to break down tasks into smaller steps and execute them in the right order.
3. Practical Tutorials:
There are tutorials available that provide full code examples for building and running multi-tool agents. These can be invaluable as you start your project, giving you a concrete starting point and demonstrating best practices.
...
Remember, building an autonomous agent is an iterative process. Start simple and gradually increase complexity as you become more comfortable with LangGraph and its capabilities.
@@ -523,83 +275,7 @@ Would you like more information on any specific aspect of building your autonomo
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
```
The graph resumed execution from the `tools` node. You can tell this is the case since the first value printed above is the response from our search engine tool.
:::
:::js
The checkpoint's `toReplay.config` contains a `checkpoint_id` timestamp. Providing this `checkpoint_id` value tells LangGraph's checkpointer to **load** the state from that moment in time.
```typescript
// The `checkpoint_id` in the `toReplay.config` corresponds to a state we've persisted to our checkpointer.
for await (const event of await graph.stream(null, {
...toReplay?.config,
streamMode: "values",
})) {
if ("messages" in event) {
const lastMessage = event.messages.at(-1);
console.log(
"=".repeat(32),
`${lastMessage?.getType()} Message`,
"=".repeat(32)
);
console.log(lastMessage?.text);
}
}
```
```
================================ ai Message ================================
Let me search for specific information about building autonomous agents with LangGraph.js.
================================ tool Message ================================
{
"query": "how to build autonomous agents with LangGraph.js examples tutorial",
"follow_up_questions": null,
"answer": null,
"images": [],
"results": [
{
"url": "https://www.mongodb.com/developer/languages/typescript/build-javascript-ai-agent-langgraphjs-mongodb/",
"title": "Build a JavaScript AI Agent With LangGraph.js and MongoDB",
"content": "(...)",
"score": 0.7672197,
"raw_content": null
},
{
"url": "https://medium.com/@lorevanoudenhove/how-to-build-ai-agents-with-langgraph-a-step-by-step-guide-5d84d9c7e832",
"title": "How to Build AI Agents with LangGraph: A Step-by-Step Guide",
"content": "(...)",
"score": 0.7407191,
"raw_content": null
}
],
"response_time": 0.82
}
================================ ai Message ================================
Based on the search results, I can share some practical information about building autonomous agents with LangGraph.js. Here are some concrete examples and approaches:
1. Example HR Assistant Agent:
- Can handle HR-related queries using employee information
- Features include:
- Starting and continuing conversations
- Looking up information using vector search
- Persisting conversation state using checkpoints
- Managing threaded conversations
2. Energy Savings Calculator Agent:
- Functions as a lead generation tool for solar panel sales
- Capabilities include:
- Calculating potential energy savings
- Handling multi-step conversations
- Processing user inputs for personalized estimates
- Managing conversation state
(...)
```
The graph resumed execution from the `tools` node. You can tell this is the case since the first value printed above is the response from our search engine tool.
:::
The graph resumed execution from the `action` node. You can tell this is the case since the first value printed above is the response from our search engine tool.
**Congratulations!** You've now used time-travel checkpoint traversal in LangGraph. Being able to rewind and explore alternative paths opens up a world of possibilities for debugging, experimentation, and interactive applications.
@@ -609,4 +285,4 @@ Take your LangGraph journey further by exploring deployment and advanced feature
- **[LangGraph Server quickstart](../../tutorials/langgraph-platform/local-server.md)**: Launch a LangGraph server locally and interact with it using the REST API and LangGraph Studio Web UI.
- **[LangGraph Platform quickstart](../../cloud/quick_start.md)**: Deploy your LangGraph app using LangGraph Platform.
- **[LangGraph Platform concepts](../../concepts/langgraph_platform.md)**: Understand the foundational concepts of the LangGraph Platform.
- **[LangGraph Platform concepts](../../concepts/langgraph_platform.md)**: Understand the foundational concepts of the LangGraph Platform.
@@ -1,4 +1,4 @@
# LangGraph Platform quickstart
# Run a local server
This guide shows you how to run a LangGraph application locally.
+123 -121
View File
@@ -92,38 +92,75 @@ nav:
- Get started:
- index.md
- Quickstarts:
- Agent: agents/agents.md
- LangGraph basics:
- Start with a prebuilt agent: agents/agents.md
- Build a custom workflow:
- concepts/why-langgraph.md
- Build a basic chatbot: tutorials/get-started/1-build-basic-chatbot.md
- tutorials/get-started/2-add-tools.md
- tutorials/get-started/3-add-memory.md
- Add human-in-the-loop: tutorials/get-started/4-human-in-the-loop.md
- tutorials/get-started/5-customize-state.md
- tutorials/get-started/6-time-travel.md
- Local server: tutorials/langgraph-platform/local-server.md
- Deployment: cloud/quick_start.md
- General concepts:
- Common patterns:
- Agent architectures: concepts/agentic_concepts.md
- Workflows & agents: tutorials/workflows.md
- Agent development: agents/overview.md
- Workflow orchestration:
- Graph API: concepts/low_level.md
- Subgraphs: concepts/subgraphs.md
- Runtime: concepts/pregel.md
- Functional API: concepts/functional_api.md
- 1. Build a basic chatbot: tutorials/get-started/1-build-basic-chatbot.md
- 2. Add tools: tutorials/get-started/2-add-tools.md
- 3. Add memory: tutorials/get-started/3-add-memory.md
- 4. Add human-in-the-loop: tutorials/get-started/4-human-in-the-loop.md
- 5. Customize state: tutorials/get-started/5-customize-state.md
- 6. Time travel: tutorials/get-started/6-time-travel.md
- Run a local server: tutorials/langgraph-platform/local-server.md
- Agent development:
- Workflows & agents: tutorials/workflows.md
- Prebuilt components: agents/overview.md
- Run an agent: agents/run_agents.md
- Agent architectures: concepts/agentic_concepts.md
- Guides:
- LangGraph APIs:
- Graph API:
- Overview: concepts/low_level.md
- Use the Graph API: how-tos/graph-api.ipynb
- Functional API:
- Overview: concepts/functional_api.md
- Use the Functional API: how-tos/use-functional-api.md
- Runtime: concepts/pregel.md
- Core capabilities:
- Streaming: concepts/streaming.md
- Persistence: concepts/persistence.md
- Durable execution: concepts/durable_execution.md
- Memory: concepts/memory.md
- Tools: concepts/tools.md
- Human-in-the-loop: concepts/human_in_the_loop.md
- Breakpoints: concepts/breakpoints.md
- Time travel: concepts/time-travel.md
- Multi-agent: concepts/multi_agent.md
- Platform capabilities:
- Streaming:
- Overview: concepts/streaming.md
- Stream outputs: how-tos/streaming.md
- Use Server API: cloud/how-tos/streaming.md
- Persistence:
- Overview: concepts/persistence.md
- Durable execution:
- Overview: concepts/durable_execution.md
- Memory:
- Overview: concepts/memory.md
- Add memory: how-tos/memory/add-memory.md
- Context:
- Add context: agents/context.md
- Models:
- Configure model: agents/models.md
- Tools:
- Overview: concepts/tools.md
- Call tools: how-tos/tool-calling.md
- Human-in-the-loop:
- Overview: concepts/human_in_the_loop.md
- Add human intervention: how-tos/human_in_the_loop/add-human-in-the-loop.md
- Use Server API: cloud/how-tos/add-human-in-the-loop.md
- Breakpoints:
- Overview: concepts/breakpoints.md
- Set breakpoints: how-tos/human_in_the_loop/breakpoints.md
- Use Server API: cloud/how-tos/human_in_the_loop_breakpoint.md
- Time travel:
- Overview: concepts/time-travel.md
- Use time travel: how-tos/human_in_the_loop/time-travel.md
- Use Server API: cloud/how-tos/human_in_the_loop_time_travel.md
- Subgraphs:
- Overview: concepts/subgraphs.md
- Use subgraphs: how-tos/subgraph.ipynb
- Multi-agent:
- Overview: concepts/multi_agent.md
- Prebuilt implementation: agents/multi-agent.md
- Custom implementation: how-tos/multi_agent.ipynb
- MCP:
- Use MCP: agents/mcp.md
- Server API: concepts/server-mcp.md
- Evaluation:
- Basic implementation: agents/evals.md
- Platform-only capabilities:
- LangGraph Platform:
- Overview: concepts/langgraph_platform.md
- Components:
@@ -133,107 +170,72 @@ nav:
- Data plane: concepts/langgraph_data_plane.md
- Control plane: concepts/langgraph_control_plane.md
- LangGraph CLI: concepts/langgraph_cli.md
- LangGraph Studio: concepts/langgraph_studio.md
- LangGraph Studio:
- Overview: concepts/langgraph_studio.md
- Quickstart: cloud/how-tos/studio/quick_start.md
- cloud/how-tos/invoke_studio.md
- cloud/how-tos/studio/manage_assistants.md
- cloud/how-tos/threads_studio.md
- cloud/how-tos/iterate_graph_studio.md
- cloud/how-tos/studio/run_evals.md
- cloud/how-tos/clone_traces_studio.md
- cloud/how-tos/datasets_studio.md
- LangGraph SDK: concepts/sdk.md
- Plans & pricing: concepts/plans.md
- Application structure: concepts/application_structure.md
- Scalability & resilience: concepts/scalability_and_resilience.md
- Authentication & access control: concepts/auth.md
- Assistants: concepts/assistants.md
- Double-texting: concepts/double_texting.md
- Webhooks: cloud/concepts/webhooks.md
- Cron jobs: cloud/concepts/cron_jobs.md
- Deployment:
- Overview: concepts/deployment_options.md
- Deployment options:
- Cloud SaaS: concepts/langgraph_cloud.md
- Self-Hosted Data Plane: concepts/langgraph_self_hosted_data_plane.md
- Self-Hosted Control Plane: concepts/langgraph_self_hosted_control_plane.md
- Standalone Container: concepts/langgraph_standalone_container.md
- Guides:
- LangGraph APIs:
- Use the Graph API: how-tos/graph-api.ipynb
- Use the Functional API: how-tos/use-functional-api.md
- Models:
- Configure model: agents/models.md
- Streaming:
- Stream outputs: how-tos/streaming.md
- Use Server API: cloud/how-tos/streaming.md
- Context:
- Add context: agents/context.md
- Memory:
- Add memory: how-tos/memory/add-memory.md
- Human-in-the-loop:
- how-tos/human_in_the_loop/add-human-in-the-loop.md
- Use Server API: cloud/how-tos/add-human-in-the-loop.md
- Time travel:
- how-tos/human_in_the_loop/time-travel.md
- Use Server API: cloud/how-tos/human_in_the_loop_time_travel.md
- Breakpoints:
- Set breakpoints: how-tos/human_in_the_loop/breakpoints.md
- Use Server API: cloud/how-tos/human_in_the_loop_breakpoint.md
- Tools:
- Call tools: how-tos/tool-calling.md
- Subgraphs:
- Use subgraphs: how-tos/subgraph.ipynb
- Multi-agent:
- Prebuilt implementation: agents/multi-agent.md
- Custom implementation: how-tos/multi_agent.ipynb
- MCP:
- Use MCP: agents/mcp.md
- Server API: concepts/server-mcp.md
- Evaluation:
- Basic implementation: agents/evals.md
- Deployment:
- Basic deployment: agents/deployment.md
- Set up your application:
- Use requirements.txt: cloud/deployment/setup.md
- Use pyproject.toml: cloud/deployment/setup_pyproject.md
- Use JavaScript: cloud/deployment/setup_javascript.md
- Use custom Docker: cloud/deployment/custom_docker.md
- Rebuild graph at runtime: cloud/deployment/graph_rebuild.md
- Deploy to production:
- Cloud SaaS: cloud/deployment/cloud.md
- Self-Hosted Data Plane: cloud/deployment/self_hosted_data_plane.md
- Self-Hosted Control Plane: cloud/deployment/self_hosted_control_plane.md
- Standalone Container: cloud/deployment/standalone_container.md
- Platform capabilities:
- LangGraph Studio:
- Quickstart: cloud/how-tos/studio/quick_start.md
- cloud/how-tos/invoke_studio.md
- cloud/how-tos/studio/manage_assistants.md
- cloud/how-tos/threads_studio.md
- cloud/how-tos/iterate_graph_studio.md
- cloud/how-tos/studio/run_evals.md
- cloud/how-tos/clone_traces_studio.md
- cloud/how-tos/datasets_studio.md
- Authentication & access control:
- how-tos/auth/custom_auth.md
- how-tos/auth/openapi_security.md
- Overview: concepts/auth.md
- how-tos/auth/custom_auth.md
- how-tos/auth/openapi_security.md
- Assistants:
- cloud/how-tos/configuration_cloud.md
- Threads: cloud/how-tos/use_threads.md
- Runs:
- cloud/how-tos/background_run.md
- cloud/how-tos/same-thread.md
- Overview: concepts/assistants.md
- cloud/how-tos/configuration_cloud.md
- Threads: cloud/how-tos/use_threads.md
- Runs:
- cloud/how-tos/background_run.md
- cloud/how-tos/same-thread.md
- cloud/how-tos/cron_jobs.md
- cloud/how-tos/stateless_runs.md
- cloud/how-tos/configurable_headers.md
- Double-texting:
- Overview: concepts/double_texting.md
- cloud/how-tos/interrupt_concurrent.md
- cloud/how-tos/rollback_concurrent.md
- cloud/how-tos/reject_concurrent.md
- cloud/how-tos/enqueue_concurrent.md
- Webhooks:
- Overview: cloud/concepts/webhooks.md
- Use webhooks: cloud/how-tos/webhooks.md
- Cron jobs:
- Overview: cloud/concepts/cron_jobs.md
- cloud/how-tos/cron_jobs.md
- cloud/how-tos/stateless_runs.md
- cloud/how-tos/configurable_headers.md
- Double-texting:
- cloud/how-tos/interrupt_concurrent.md
- cloud/how-tos/rollback_concurrent.md
- cloud/how-tos/reject_concurrent.md
- cloud/how-tos/enqueue_concurrent.md
- Webhooks: cloud/how-tos/webhooks.md
- Cron jobs: cloud/how-tos/cron_jobs.md
- Server customization:
- how-tos/http/custom_lifespan.md
- how-tos/http/custom_middleware.md
- how-tos/http/custom_routes.md
- how-tos/http/custom_lifespan.md
- how-tos/http/custom_middleware.md
- how-tos/http/custom_routes.md
- Data management:
- Add semantic search: cloud/deployment/semantic_search.md
- Add TTLs: how-tos/ttl/configure_ttl.md
- Deployment:
- Overview: concepts/deployment_options.md
- Quickstart: cloud/quick_start.md
- Set up your application:
- Use requirements.txt: cloud/deployment/setup.md
- Use pyproject.toml: cloud/deployment/setup_pyproject.md
- Use JavaScript: cloud/deployment/setup_javascript.md
- Use custom Docker: cloud/deployment/custom_docker.md
- Rebuild graph at runtime: cloud/deployment/graph_rebuild.md
- Deployment options:
- Cloud SaaS: concepts/langgraph_cloud.md
- Self-Hosted Data Plane: concepts/langgraph_self_hosted_data_plane.md
- Self-Hosted Control Plane: concepts/langgraph_self_hosted_control_plane.md
- Standalone Container: concepts/langgraph_standalone_container.md
- Deploy to production:
- Cloud SaaS: cloud/deployment/cloud.md
- Self-Hosted Data Plane: cloud/deployment/self_hosted_data_plane.md
- Self-Hosted Control Plane: cloud/deployment/self_hosted_control_plane.md
- Standalone Container: cloud/deployment/standalone_container.md
- Reference:
- reference/index.md
@@ -263,7 +265,6 @@ nav:
- Environment variables: cloud/reference/env_var.md
- Examples:
- agents/run_agents.md
- Template applications: concepts/template_applications.md # TODO: make tutorial
- Agentic RAG: tutorials/rag/langgraph_agentic_rag.ipynb
- Agent Supervisor: tutorials/multi_agent/agent_supervisor.ipynb
@@ -288,6 +289,7 @@ nav:
- Case studies: adopters.md
- concepts/faq.md
- llms.txt: llms-txt-overview.md
- LangChain Forum: https://forum.langchain.com/
- Troubleshooting:
- Errors:
- troubleshooting/errors/index.md
+1 -1
View File
@@ -12,7 +12,7 @@ readme = "README.md"
license = "MIT"
license-files = ['LICENSE']
dependencies = [
"langgraph-checkpoint>=2.0.21",
"langgraph-checkpoint>=2.0.21,<3.0.0",
"orjson>=3.10.1",
"psycopg>=3.2.0",
"psycopg-pool>=3.2.0",
@@ -223,10 +223,9 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
Yields:
An SQLite cursor object.
"""
if not self.is_setup:
await self.setup()
async with self.lock:
if not self.is_setup:
await self.setup()
if transaction:
await self.conn.execute("BEGIN")
@@ -981,10 +981,9 @@ class SqliteStore(BaseSqliteStore, BaseStore):
Args:
transaction (bool): whether to use transaction for the DB operations
"""
if not self.is_setup:
self.setup()
with self.lock:
if not self.is_setup:
self.setup()
if transaction:
self.conn.execute("BEGIN")
@@ -1002,10 +1001,10 @@ class SqliteStore(BaseSqliteStore, BaseStore):
This method creates the necessary tables in the SQLite database if they don't
already exist and runs database migrations. It should be called before first use.
"""
if self.is_setup:
return
with self.lock:
if self.is_setup:
return
# Create migrations table if it doesn't exist
self.conn.executescript(
"""
+1 -1
View File
@@ -12,7 +12,7 @@ readme = "README.md"
license = "MIT"
license-files = ['LICENSE']
dependencies = [
"langgraph-checkpoint>=2.0.21",
"langgraph-checkpoint>=2.0.21,<3.0.0",
"aiosqlite>=0.20",
"sqlite-vec>=0.1.6",
]
+1 -1
View File
@@ -208,7 +208,7 @@ def up(
):
click.secho("Starting LangGraph API server...", fg="green")
click.secho(
"""For local dev, requires env var LANGSMITH_API_KEY with access to LangGraph Platform closed beta.
"""For local dev, requires env var LANGSMITH_API_KEY with access to LangGraph Platform.
For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KEY.""",
)
with Runner() as runner, Progress(message="Pulling...") as set:
+2 -2
View File
@@ -18,8 +18,8 @@ dependencies = [
[project.optional-dependencies]
inmem = [
"langgraph-api>=0.2.67 ; python_version >= '3.11'",
"langgraph-runtime-inmem>=0.3.0 ; python_version >= '3.11'",
"langgraph-api>=0.2.67,<0.3.0 ; python_version >= '3.11'",
"langgraph-runtime-inmem>=0.3.0,<0.4.0 ; python_version >= '3.11'",
"python-dotenv>=0.8.0",
]
+2 -2
View File
@@ -530,8 +530,8 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "click", specifier = ">=8.1.7" },
{ name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.2.67" },
{ name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.3.0" },
{ name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.2.67,<0.3.0" },
{ name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.3.0,<0.4.0" },
{ name = "langgraph-sdk", marker = "python_full_version >= '3.11'", specifier = ">=0.1.0" },
{ name = "python-dotenv", marker = "extra == 'inmem'", specifier = ">=0.8.0" },
]
+1 -1
View File
@@ -63,7 +63,7 @@ LangGraph provides low-level supporting infrastructure for *any* long-running, s
While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
- [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
- [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
- [LangChain](https://python.langchain.com/docs/introduction/) Provides integrations and composable components to streamline LLM application development.
> [!NOTE]
+3 -3
View File
@@ -13,9 +13,9 @@ license = "MIT"
license-files = ['LICENSE']
dependencies = [
"langchain-core>=0.1",
"langgraph-checkpoint>=2.1.0",
"langgraph-sdk>=0.1.42",
"langgraph-prebuilt>=0.5.0",
"langgraph-checkpoint>=2.1.0,<3.0.0",
"langgraph-sdk>=0.1.42,<0.2.0",
"langgraph-prebuilt>=0.5.0,<0.6.0",
"xxhash>=3.5.0",
"pydantic>=2.7.4",
]
File diff suppressed because one or more lines are too long
+8 -8
View File
@@ -1183,7 +1183,7 @@ wheels = [
[[package]]
name = "langchain-core"
version = "0.3.63"
version = "0.3.67"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonpatch" },
@@ -1194,9 +1194,9 @@ dependencies = [
{ name = "tenacity" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/0a/b71a9a5d42e743d6876cce23d803e284b191ed4d6544e2f7fe1b37f7854c/langchain_core-0.3.63.tar.gz", hash = "sha256:e2e30cfbb7684a5a0319f6cbf065fc3c438bfd1060302f085a122527890fb01e", size = 558302, upload-time = "2025-05-29T18:57:19.933Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/40/875af0194024d0006874f061958fa417d3500bbfdc9a57e1bd1c2f4e6ed2/langchain_core-0.3.67.tar.gz", hash = "sha256:2c14aa44a0e78e014e96d7f2f8916ac109d0a0ba87ed67ee25bf7296bed7e7ba", size = 561952, upload-time = "2025-06-30T17:09:35.142Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/71/a748861e6a69ab6ef50ab8e65120422a1f36245c71a0dd0f02de49c208e1/langchain_core-0.3.63-py3-none-any.whl", hash = "sha256:f91db8221b1bc6808f70b2e72fded1a94d50ee3f1dff1636fb5a5a514c64b7f5", size = 438468, upload-time = "2025-05-29T18:57:17.424Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2b/a0d283089c6d08c12d47dca39a55029ff714e939ec04f4560420426ab613/langchain_core-0.3.67-py3-none-any.whl", hash = "sha256:b699f1f24b24fa2747c05e2daa280aa64478a51e01a4e82c7f8e20b6167dfa99", size = 440237, upload-time = "2025-06-30T17:09:33.323Z" },
]
[[package]]
@@ -1423,7 +1423,7 @@ inmem = [
[[package]]
name = "langgraph-prebuilt"
version = "0.5.1"
version = "0.5.2"
source = { editable = "../prebuilt" }
dependencies = [
{ name = "langchain-core" },
@@ -1432,7 +1432,7 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "langchain-core", specifier = ">=0.3.22" },
{ name = "langchain-core", specifier = ">=0.3.67" },
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
]
@@ -1497,7 +1497,7 @@ dev = [
[[package]]
name = "langsmith"
version = "0.3.43"
version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -1508,9 +1508,9 @@ dependencies = [
{ name = "requests-toolbelt" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/02/21/df84fe8b5c16971999650cbfc95a49f176d044a606e2b4eb957bbc122e1c/langsmith-0.3.43.tar.gz", hash = "sha256:7dab99b635859e24a1a252ad4f7e23170a45f4ea742567a10b4b26c50478ed43", size = 346328, upload-time = "2025-05-29T00:21:11.637Z" }
sdist = { url = "https://files.pythonhosted.org/packages/20/c8/8d2e0fc438d2d3d8d4300f7684ea30a754344ed00d7ba9cc2705241d2a5f/langsmith-0.4.4.tar.gz", hash = "sha256:70c53bbff24a7872e88e6fa0af98270f4986a6e364f9e85db1cc5636defa4d66", size = 352105, upload-time = "2025-06-27T19:20:36.207Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/72/f5304de3e7e80e6dc266c161230aecb7895958f78b5af1541317d7615fc6/langsmith-0.3.43-py3-none-any.whl", hash = "sha256:2d4558068abf2eeb60ff80871187724e07f5e657d7d6be9e0c603df36c41140a", size = 361148, upload-time = "2025-05-29T00:21:08.759Z" },
{ url = "https://files.pythonhosted.org/packages/1d/33/a3337eb70d795495a299a1640d7a75f17fb917155a64309b96106e7b9452/langsmith-0.4.4-py3-none-any.whl", hash = "sha256:014c68329bd085bd6c770a6405c61bb6881f82eb554ce8c4d1984b0035fd1716", size = 367687, upload-time = "2025-06-27T19:20:33.839Z" },
]
[[package]]
@@ -30,7 +30,10 @@ from langchain_core.runnables.config import (
)
from langchain_core.tools import BaseTool, InjectedToolArg
from langchain_core.tools import tool as create_tool
from langchain_core.tools.base import get_all_basemodel_annotations
from langchain_core.tools.base import (
TOOL_MESSAGE_BLOCK_TYPES,
get_all_basemodel_annotations,
)
from pydantic import BaseModel
from typing_extensions import Annotated, get_args, get_origin
@@ -46,12 +49,11 @@ TOOL_CALL_ERROR_TEMPLATE = "Error: {error}\n Please fix your mistakes."
def msg_content_output(output: Any) -> Union[str, list[dict]]:
recognized_content_block_types = ("image", "image_url", "text", "json")
if isinstance(output, str):
return output
elif isinstance(output, list) and all(
[
isinstance(x, dict) and x.get("type") in recognized_content_block_types
isinstance(x, dict) and x.get("type") in TOOL_MESSAGE_BLOCK_TYPES
for x in output
]
):
+3 -3
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-prebuilt"
version = "0.5.1"
version = "0.5.2"
description = "Library with high-level APIs for creating and executing LangGraph agents and tools."
authors = []
requires-python = ">=3.9"
@@ -12,8 +12,8 @@ readme = "README.md"
license = "MIT"
license-files = ['LICENSE']
dependencies = [
"langgraph-checkpoint>=2.1.0",
"langchain-core>=0.3.22",
"langgraph-checkpoint>=2.1.0,<3.0.0",
"langchain-core>=0.3.67",
]
[project.urls]
+8 -8
View File
@@ -302,7 +302,7 @@ wheels = [
[[package]]
name = "langchain-core"
version = "0.3.60"
version = "0.3.67"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonpatch" },
@@ -313,9 +313,9 @@ dependencies = [
{ name = "tenacity" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5b/75/95129aaada92980a002a31e002610a80af3c8967ae7884710372e89cdde0/langchain_core-0.3.60.tar.gz", hash = "sha256:63dd1bdf7939816115399522661ca85a2f3686a61440f2f46ebd86d1b028595b", size = 557456, upload-time = "2025-05-15T15:23:23.642Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/40/875af0194024d0006874f061958fa417d3500bbfdc9a57e1bd1c2f4e6ed2/langchain_core-0.3.67.tar.gz", hash = "sha256:2c14aa44a0e78e014e96d7f2f8916ac109d0a0ba87ed67ee25bf7296bed7e7ba", size = 561952, upload-time = "2025-06-30T17:09:35.142Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/bc/344f5b11fdfe0e27f7064d2e829921a791461dc32e5ed285fe6325518c26/langchain_core-0.3.60-py3-none-any.whl", hash = "sha256:2ccdf06b12e699b1b0962bc02837056c075b4981c3d13f82a4d4c30bb22ea3dc", size = 437890, upload-time = "2025-05-15T15:23:22.278Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2b/a0d283089c6d08c12d47dca39a55029ff714e939ec04f4560420426ab613/langchain_core-0.3.67-py3-none-any.whl", hash = "sha256:b699f1f24b24fa2747c05e2daa280aa64478a51e01a4e82c7f8e20b6167dfa99", size = 440237, upload-time = "2025-06-30T17:09:33.323Z" },
]
[[package]]
@@ -464,7 +464,7 @@ dev = [
[[package]]
name = "langgraph-prebuilt"
version = "0.5.1"
version = "0.5.2"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },
@@ -489,7 +489,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "langchain-core", specifier = ">=0.3.22" },
{ name = "langchain-core", specifier = ">=0.3.67" },
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
]
@@ -537,7 +537,7 @@ dev = [
[[package]]
name = "langsmith"
version = "0.3.42"
version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -548,9 +548,9 @@ dependencies = [
{ name = "requests-toolbelt" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3a/44/fe171c0b0fb0377b191aebf0b7779e0c7b2a53693c6a01ddad737212495d/langsmith-0.3.42.tar.gz", hash = "sha256:2b5cbc450ab808b992362aac6943bb1d285579aa68a3a8be901d30a393458f25", size = 345619, upload-time = "2025-05-03T03:07:17.873Z" }
sdist = { url = "https://files.pythonhosted.org/packages/20/c8/8d2e0fc438d2d3d8d4300f7684ea30a754344ed00d7ba9cc2705241d2a5f/langsmith-0.4.4.tar.gz", hash = "sha256:70c53bbff24a7872e88e6fa0af98270f4986a6e364f9e85db1cc5636defa4d66", size = 352105, upload-time = "2025-06-27T19:20:36.207Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/8e/e8a58e0abaae3f3ac4702e9ca35d1fc6159711556b64ffd0e247771a3f12/langsmith-0.3.42-py3-none-any.whl", hash = "sha256:18114327f3364385dae4026ebfd57d1c1cb46d8f80931098f0f10abe533475ff", size = 360334, upload-time = "2025-05-03T03:07:15.491Z" },
{ url = "https://files.pythonhosted.org/packages/1d/33/a3337eb70d795495a299a1640d7a75f17fb917155a64309b96106e7b9452/langsmith-0.4.4-py3-none-any.whl", hash = "sha256:014c68329bd085bd6c770a6405c61bb6881f82eb554ce8c4d1984b0035fd1716", size = 367687, upload-time = "2025-06-27T19:20:33.839Z" },
]
[[package]]
+5 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
"version": "0.0.86",
"version": "0.0.88",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
@@ -22,7 +22,9 @@
"uuid": "^9.0.0"
},
"devDependencies": {
"@langchain/core": "^0.3.31",
"@langchain/langgraph-api": "~0.0.41",
"@langchain/core": "^0.3.61",
"@langchain/langgraph": "^0.3.5",
"@langchain/scripts": "^0.1.4",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
@@ -35,6 +37,7 @@
"@types/uuid": "^9.0.1",
"@vitejs/plugin-react": "^4.4.1",
"concat-md": "^0.5.1",
"hono": "^4.8.2",
"jsdom": "^26.1.0",
"msw": "^2.8.2",
"prettier": "^3.2.5",
+34 -4
View File
@@ -1014,8 +1014,8 @@ export class RunsClient<
const stream: ReadableStream<{ event: any; data: any }> = (
response.body || new ReadableStream({ start: (ctrl) => ctrl.close() })
)
.pipeThrough(new BytesLineDecoder())
.pipeThrough(new SSEDecoder());
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
yield* IterableReadableStream.fromReadableStream(stream);
}
@@ -1318,8 +1318,8 @@ export class RunsClient<
const stream: ReadableStream<{ event: string; data: any }> = (
response.body || new ReadableStream({ start: (ctrl) => ctrl.close() })
)
.pipeThrough(new BytesLineDecoder())
.pipeThrough(new SSEDecoder());
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
yield* IterableReadableStream.fromReadableStream(stream);
}
@@ -1658,7 +1658,30 @@ export class Client<
*/
public "~ui": UiClient;
/**
* @internal Used to obtain a stable key representing the client.
*/
private "~configHash": string | undefined;
constructor(config?: ClientConfig) {
this["~configHash"] = (() =>
JSON.stringify({
apiUrl: config?.apiUrl,
apiKey: config?.apiKey,
timeoutMs: config?.timeoutMs,
defaultHeaders: config?.defaultHeaders,
maxConcurrency: config?.callerOptions?.maxConcurrency,
maxRetries: config?.callerOptions?.maxRetries,
callbacks: {
onFailedResponseHook:
config?.callerOptions?.onFailedResponseHook != null,
onRequest: config?.onRequest != null,
fetch: config?.callerOptions?.fetch != null,
},
}))();
this.assistants = new AssistantsClient(config);
this.threads = new ThreadsClient(config);
this.runs = new RunsClient(config);
@@ -1667,3 +1690,10 @@ export class Client<
this["~ui"] = new UiClient(config);
}
}
/**
* @internal Used to obtain a stable key representing the client.
*/
export function getClientConfigHash(client: Client): string | undefined {
return client["~configHash"];
}
+131 -28
View File
@@ -1,7 +1,7 @@
/* __LC_ALLOW_ENTRYPOINT_SIDE_EFFECTS__ */
"use client";
import { Client, type ClientConfig } from "../client.js";
import { Client, getClientConfigHash, type ClientConfig } from "../client.js";
import type {
Command,
DisconnectMode,
@@ -31,7 +31,7 @@ import type {
} from "../types.stream.js";
import {
type MutableRefObject,
type RefObject,
useCallback,
useEffect,
useMemo,
@@ -316,16 +316,21 @@ function fetchHistory<StateType extends Record<string, unknown>>(
function useThreadHistory<StateType extends Record<string, unknown>>(
threadId: string | undefined | null,
client: Client,
clearCallbackRef: MutableRefObject<(() => void) | undefined>,
submittingRef: MutableRefObject<boolean>,
clearCallbackRef: RefObject<(() => void) | undefined>,
submittingRef: RefObject<boolean>,
) {
const [history, setHistory] = useState<ThreadState<StateType>[]>([]);
const clientHash = getClientConfigHash(client);
const clientRef = useRef(client);
clientRef.current = client;
const fetcher = useCallback(
(
threadId: string | undefined | null,
): Promise<ThreadState<StateType>[]> => {
if (threadId != null) {
const client = clientRef.current;
return fetchHistory<StateType>(client, threadId).then((history) => {
setHistory(history);
return history;
@@ -342,7 +347,7 @@ function useThreadHistory<StateType extends Record<string, unknown>>(
useEffect(() => {
if (submittingRef.current) return;
fetcher(threadId);
}, [fetcher, submittingRef, threadId]);
}, [fetcher, clientHash, submittingRef, threadId]);
return {
data: history,
@@ -506,6 +511,31 @@ export interface UseStreamOptions<
*/
onDebugEvent?: (data: DebugStreamEvent["data"]) => void;
/**
* Callback that is called when the stream is stopped by the user.
* Provides a mutate function to update the stream state immediately
* without requiring a server roundtrip.
*
* @example
* ```typescript
* onStop: ({ mutate }) => {
* mutate((prev) => ({
* ...prev,
* ui: prev.ui?.map(component =>
* component.props.isLoading
* ? { ...component, props: { ...component.props, stopped: true, isLoading: false }}
* : component
* )
* }));
* }
* ```
*/
onStop?: (options: {
mutate: (
update: Partial<StateType> | ((prev: StateType) => Partial<StateType>),
) => void;
}) => void;
/**
* The ID of the thread to fetch history and current values from.
*/
@@ -518,6 +548,17 @@ export interface UseStreamOptions<
/** Will reconnect the stream on mount */
reconnectOnMount?: boolean | (() => RunMetadataStorage);
/**
* Initial values to display immediately when loading a thread.
* Useful for displaying cached thread data while official history loads.
* These values will be replaced when official thread data is fetched.
*
* Note: UI components from initialValues will render immediately if they're
* predefined in LoadExternalComponent's components prop, providing instant
* cached UI display without server fetches.
*/
initialValues?: StateType | null;
}
interface RunMetadataStorage {
@@ -616,7 +657,11 @@ export interface UseStream<
/**
* Join an active stream.
*/
joinStream: (runId: string) => Promise<void>;
joinStream: (
runId: string,
lastEventId?: string,
options?: { streamMode?: StreamMode | StreamMode[] },
) => Promise<void>;
}
type ConfigWithConfigurable<ConfigurableType extends Record<string, unknown>> =
@@ -647,6 +692,61 @@ interface SubmitOptions<
*/
streamSubgraphs?: boolean;
streamResumable?: boolean;
/**
* The ID to use when creating a new thread. When provided, this ID will be used
* for thread creation when threadId is `null` or `undefined`.
* This enables optimistic UI updates where you know the thread ID
* before the thread is actually created.
*/
threadId?: string;
}
function useStreamValuesState<StateType extends Record<string, unknown>>() {
type Kind = "stream" | "stop";
type Values = StateType | null;
type Update = Values | ((prev: Values, kind?: Kind) => Values);
type Mutate = Partial<StateType> | ((prev: StateType) => Partial<StateType>);
const [values, setValues] = useState<[values: StateType, kind: Kind] | null>(
null,
);
const setStreamValues = useCallback(
(values: Update, kind: Kind = "stream") => {
if (typeof values === "function") {
setValues((prevTuple) => {
const [prevValues, prevKind] = prevTuple ?? [null, "stream"];
const next = values(prevValues, prevKind);
if (next == null) return null;
return [next, kind] as [StateType, Kind];
});
return;
}
if (values == null) setValues(null);
setValues([values, kind] as [StateType, Kind]);
},
[],
);
const mutate = useCallback(
(kind: Kind, serverValues: StateType) => (update: Mutate) => {
setStreamValues((clientValues) => {
const prev = { ...serverValues, ...clientValues };
const next = typeof update === "function" ? update(prev) : update;
return { ...prev, ...next };
}, kind);
},
[setStreamValues],
);
return [values?.[0] ?? null, setStreamValues, mutate] as [
Values,
(update: Update, kind?: Kind) => void,
(kind: Kind, serverValues: StateType) => (update: Mutate) => void,
];
}
export function useStream<
@@ -712,7 +812,8 @@ export function useStream<
const [isLoading, setIsLoading] = useState(false);
const [streamError, setStreamError] = useState<unknown>(undefined);
const [streamValues, setStreamValues] = useState<StateType | null>(null);
const [streamValues, setStreamValues, getMutateFn] =
useStreamValuesState<StateType>();
const messageManagerRef = useRef(new MessageTupleManager());
const submittingRef = useRef(false);
@@ -783,7 +884,9 @@ export function useStream<
);
const threadHead: ThreadState<StateType> | undefined = flatHistory.at(-1);
const historyValues = threadHead?.values ?? ({} as StateType);
const historyValues =
threadHead?.values ?? options.initialValues ?? ({} as StateType);
const historyError = (() => {
const error = threadHead?.tasks?.at(-1)?.error;
if (error == null) return undefined;
@@ -848,6 +951,8 @@ export function useStream<
if (runId) client.runs.cancel(threadId, runId);
runMetadataStorage.removeItem(`lg:stream:${threadId}`);
}
options?.onStop?.({ mutate: getMutateFn("stop", historyValues) });
};
async function consumeStream(
@@ -880,15 +985,7 @@ export function useStream<
if (event === "updates") options.onUpdateEvent?.(data);
if (event === "custom")
options.onCustomEvent?.(data, {
mutate: (update) =>
setStreamValues((prev) => {
// should not happen
if (prev == null) return prev;
return {
...prev,
...(typeof update === "function" ? update(prev) : update),
};
}),
mutate: getMutateFn("stream", historyValues),
});
if (event === "metadata") options.onMetadataEvent?.(data);
if (event === "events") options.onLangChainEvent?.(data);
@@ -930,8 +1027,11 @@ export function useStream<
// TODO: stream created checkpoints to avoid an unnecessary network request
const result = await run.onSuccess();
setStreamValues(null);
setStreamValues((values, kind) => {
// Do not clear out the user values set on `stop`.
if (kind === "stop") return values;
return null;
});
if (streamError != null) throw streamError;
const lastHead = result.at(0);
@@ -957,13 +1057,18 @@ export function useStream<
}
}
const joinStream = async (runId: string, lastEventId?: string) => {
const joinStream = async (
runId: string,
lastEventId?: string,
options?: { streamMode?: StreamMode | StreamMode[] },
) => {
lastEventId ??= "-1";
if (!threadId) return;
await consumeStream(async (signal: AbortSignal) => {
const stream = client.runs.joinStream(threadId, runId, {
signal,
lastEventId,
streamMode: options?.streamMode,
}) as AsyncGenerator<EventStreamEvent>;
return {
@@ -989,26 +1094,24 @@ export function useStream<
if (newPath != null) setBranch(newPath ?? "");
// Assumption: we're setting the initial value
// Used for instant feedback
setStreamValues(() => {
const values = { ...historyValues };
if (submitOptions?.optimisticValues != null) {
return {
...values,
...historyValues,
...(typeof submitOptions.optimisticValues === "function"
? submitOptions.optimisticValues(values)
? submitOptions.optimisticValues(historyValues)
: submitOptions.optimisticValues),
};
}
return values;
return { ...historyValues };
});
let usableThreadId = threadId;
if (!usableThreadId) {
const thread = await client.threads.create();
const thread = await client.threads.create({
threadId: submitOptions?.threadId,
});
onThreadId(thread.thread_id);
usableThreadId = thread.thread_id;
}
+16 -16
View File
@@ -20,7 +20,7 @@ describe("BytesLineDecoder", () => {
test("handles single line with newline", async () => {
const input = createStream([textEncoder.encode("hello\n")]);
const decoded = input.pipeThrough(new BytesLineDecoder());
const decoded = input.pipeThrough(BytesLineDecoder());
const results = await gather(decoded);
expect(results.length).toBe(1);
@@ -29,7 +29,7 @@ describe("BytesLineDecoder", () => {
test("handles multiple lines", async () => {
const input = createStream([textEncoder.encode("line1\nline2\nline3\n")]);
const decoded = input.pipeThrough(new BytesLineDecoder());
const decoded = input.pipeThrough(BytesLineDecoder());
const results = await gather(decoded);
expect(results.length).toBe(3);
@@ -44,7 +44,7 @@ describe("BytesLineDecoder", () => {
textEncoder.encode("ne1\nli"),
textEncoder.encode("ne2\n"),
]);
const decoded = input.pipeThrough(new BytesLineDecoder());
const decoded = input.pipeThrough(BytesLineDecoder());
const results = await gather(decoded);
expect(results.length).toBe(2);
@@ -54,7 +54,7 @@ describe("BytesLineDecoder", () => {
test("handles CR LF line endings", async () => {
const input = createStream([textEncoder.encode("line1\r\nline2\r\n")]);
const decoded = input.pipeThrough(new BytesLineDecoder());
const decoded = input.pipeThrough(BytesLineDecoder());
const results = await gather(decoded);
expect(results.length).toBe(2);
@@ -67,7 +67,7 @@ describe("BytesLineDecoder", () => {
textEncoder.encode("line1\r"),
textEncoder.encode("\nline2\r\n"),
]);
const decoded = input.pipeThrough(new BytesLineDecoder());
const decoded = input.pipeThrough(BytesLineDecoder());
const results = await gather(decoded);
expect(results.length).toBe(2);
@@ -77,7 +77,7 @@ describe("BytesLineDecoder", () => {
test("handles stale line", async () => {
const input = createStream([textEncoder.encode("hello")]);
const decoded = input.pipeThrough(new BytesLineDecoder());
const decoded = input.pipeThrough(BytesLineDecoder());
const results = await gather(decoded);
expect(results.length).toBe(1);
@@ -99,8 +99,8 @@ describe("SSEDecoder", () => {
"\n",
]);
const decoded = input
.pipeThrough(new BytesLineDecoder())
.pipeThrough(new SSEDecoder());
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
const results = await gather(decoded);
expect(results.length).toBe(1);
@@ -117,8 +117,8 @@ describe("SSEDecoder", () => {
'data: {"message": "hello"}\n',
]);
const decoded = input
.pipeThrough(new BytesLineDecoder())
.pipeThrough(new SSEDecoder());
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
const results = await gather(decoded);
expect(results.length).toBe(1);
@@ -138,8 +138,8 @@ describe("SSEDecoder", () => {
"\n",
]);
const decoded = input
.pipeThrough(new BytesLineDecoder())
.pipeThrough(new SSEDecoder());
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
const results = await gather(decoded);
expect(results.length).toBe(2);
@@ -156,8 +156,8 @@ describe("SSEDecoder", () => {
test("end event without data", async () => {
const input = createStream(["event: test\n"]);
const decoded = input
.pipeThrough(new BytesLineDecoder())
.pipeThrough(new SSEDecoder());
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
const results = await gather(decoded);
expect(results.length).toBe(1);
@@ -170,8 +170,8 @@ describe("SSEDecoder", () => {
test("end event without newline", async () => {
const input = createStream(["event: end"]);
const decoded = input
.pipeThrough(new BytesLineDecoder())
.pipeThrough(new SSEDecoder());
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
const results = await gather(decoded);
expect(results.length).toBe(1);
+361 -354
View File
@@ -1,14 +1,58 @@
import "@testing-library/jest-dom/vitest";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";
import { http } from "msw";
import { useStream } from "../react/stream.js";
import "@testing-library/jest-dom/vitest";
import type { Message } from "../types.messages.js";
import { StateGraph, MessagesAnnotation, START } from "@langchain/langgraph";
import { MemorySaver } from "@langchain/langgraph-checkpoint";
import { FakeStreamingChatModel } from "@langchain/core/utils/testing";
import { AIMessage } from "@langchain/core/messages";
import { createEmbedServer } from "@langchain/langgraph-api/experimental/embed";
import { randomUUID } from "node:crypto";
import { useState } from "react";
const threads = (() => {
const THREADS: Record<
string,
{ thread_id: string; metadata: Record<string, unknown> }
> = {};
return {
get: async (id: string) => THREADS[id],
put: async (
threadId: string,
{ metadata }: { metadata?: Record<string, unknown> },
) => {
THREADS[threadId] = { thread_id: threadId, metadata: metadata ?? {} };
},
delete: async (threadId: string) => {
delete THREADS[threadId];
},
};
})();
const checkpointer = new MemorySaver();
const model = new FakeStreamingChatModel({ responses: [new AIMessage("Hey")] });
const agent = new StateGraph(MessagesAnnotation)
.addNode("agent", async (state: { messages: Message[] }) => {
const response = await model.invoke(state.messages);
return { messages: [response] };
})
.addEdge(START, "agent")
.compile();
const app = createEmbedServer({ graph: { agent }, checkpointer, threads });
const server = setupServer(http.all("*", (ctx) => app.fetch(ctx.request)));
function TestChatComponent() {
const { messages, isLoading, error, submit, stop } = useStream({
assistantId: "test-assistant",
assistantId: "agent",
apiKey: "test-api-key",
});
@@ -42,353 +86,6 @@ function TestChatComponent() {
);
}
// Mock server setup
const server = setupServer(
// Mock thread creation
http.post("*/threads", () => {
return HttpResponse.json({ thread_id: "test-thread-id" });
}),
// Mock stream endpoint
http.post("*/threads/:threadId/runs/stream", async () => {
const encoder = new TextEncoder();
const sendSSE = (event: string, data: unknown) =>
encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
const stream = new ReadableStream({
async start(controller) {
await new Promise((resolve) => setTimeout(resolve, 10));
controller.enqueue(
sendSSE("metadata", {
run_id: "1f03278a-1734-6518-80a4-3390db59f960",
attempt: 1,
}),
);
controller.enqueue(
sendSSE("values", {
messages: [
{
content: "Hey",
additional_kwargs: {},
response_metadata: {},
type: "human",
name: null,
id: "2d8c0d9f-a614-4e44-b474-6a56e9471cf5",
example: false,
},
],
}),
);
controller.enqueue(
sendSSE("messages", [
{
content: "",
additional_kwargs: {},
response_metadata: { model_name: "claude-3-7-sonnet-latest" },
type: "AIMessageChunk",
name: null,
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
tool_calls: [],
invalid_tool_calls: [],
tool_call_chunks: [],
},
{ run_attempt: 1 },
]),
);
controller.enqueue(
sendSSE("messages", [
{
content: "Hello",
additional_kwargs: {},
response_metadata: { model_name: "claude-3-7-sonnet-latest" },
type: "AIMessageChunk",
name: null,
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
tool_calls: [],
invalid_tool_calls: [],
tool_call_chunks: [],
},
{ run_attempt: 1 },
]),
);
controller.enqueue(
sendSSE("messages", [
{
content: "! How can I assist you today?",
additional_kwargs: {},
response_metadata: { model_name: "claude-3-7-sonnet-latest" },
type: "AIMessageChunk",
name: null,
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
tool_calls: [],
invalid_tool_calls: [],
tool_call_chunks: [],
},
{ run_attempt: 1 },
]),
);
controller.enqueue(
sendSSE("messages", [
{
content: "",
additional_kwargs: {},
response_metadata: {
stop_reason: "end_turn",
stop_sequence: null,
},
type: "AIMessageChunk",
name: null,
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
tool_calls: [],
invalid_tool_calls: [],
tool_call_chunks: [],
},
{ run_attempt: 1 },
]),
);
controller.enqueue(
sendSSE("values", {
messages: [
{
content: "Hey",
additional_kwargs: {},
response_metadata: {},
type: "human",
name: null,
id: "2d8c0d9f-a614-4e44-b474-6a56e9471cf5",
example: false,
},
{
content: "Hello! How can I assist you today?",
additional_kwargs: {},
response_metadata: {
model_name: "claude-3-7-sonnet-latest",
stop_reason: "end_turn",
stop_sequence: null,
},
type: "ai",
name: null,
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
tool_calls: [],
invalid_tool_calls: [],
},
],
}),
);
controller.close();
},
});
server.use(
http.post("*/threads/:threadId/history", () => {
return HttpResponse.json([
{
values: {
messages: [
{
content: "Hey",
additional_kwargs: {},
response_metadata: {},
type: "human",
name: null,
id: "2d8c0d9f-a614-4e44-b474-6a56e9471cf5",
example: false,
},
{
content: "Hello! How can I assist you today?",
additional_kwargs: {},
response_metadata: {
model_name: "claude-3-7-sonnet-latest",
stop_reason: "end_turn",
stop_sequence: null,
},
type: "ai",
name: null,
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
example: false,
tool_calls: [],
invalid_tool_calls: [],
},
],
},
next: [],
tasks: [],
metadata: {
run_attempt: 1,
source: "loop",
writes: {
agent: {
messages: [
{
content: "Hello! How can I assist you today?",
additional_kwargs: {},
response_metadata: {
model_name: "claude-3-7-sonnet-latest",
stop_reason: "end_turn",
stop_sequence: null,
},
type: "ai",
name: null,
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
example: false,
tool_calls: [],
invalid_tool_calls: [],
},
],
},
},
step: 1,
parents: {},
},
created_at: "2025-05-16T17:10:16.987537+00:00",
checkpoint: {
checkpoint_id: "1f03278a-38cf-6c68-8001-22b77ac43ff6",
thread_id: "b06fd92a-955c-446e-b233-7977716c4a9c",
checkpoint_ns: "",
},
parent_checkpoint: {
checkpoint_id: "1f03278a-206b-67c6-8000-ac34a0872e1a",
thread_id: "b06fd92a-955c-446e-b233-7977716c4a9c",
checkpoint_ns: "",
},
checkpoint_id: "1f03278a-38cf-6c68-8001-22b77ac43ff6",
parent_checkpoint_id: "1f03278a-206b-67c6-8000-ac34a0872e1a",
},
{
values: {
messages: [
{
content: "Hey",
additional_kwargs: {},
response_metadata: {},
type: "human",
name: null,
id: "2d8c0d9f-a614-4e44-b474-6a56e9471cf5",
example: false,
},
],
},
next: ["agent"],
tasks: [
{
id: "e1b7b52b-a78e-4b32-0c89-e06bf46405ed",
name: "agent",
path: ["__pregel_pull", "agent"],
error: null,
interrupts: [],
checkpoint: null,
state: null,
result: {
messages: [
{
content: "Hello! How can I assist you today?",
additional_kwargs: {},
response_metadata: {
model_name: "claude-3-7-sonnet-latest",
stop_reason: "end_turn",
stop_sequence: null,
},
type: "ai",
name: null,
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
example: false,
tool_calls: [],
invalid_tool_calls: [],
},
],
},
},
],
metadata: {
run_attempt: 1,
},
created_at: "2025-05-16T17:10:14.429889+00:00",
checkpoint: {
checkpoint_id: "1f03278a-206b-67c6-8000-ac34a0872e1a",
thread_id: "b06fd92a-955c-446e-b233-7977716c4a9c",
checkpoint_ns: "",
},
parent_checkpoint: {
checkpoint_id: "1f03278a-2067-6590-bfff-3fb740466fc3",
thread_id: "b06fd92a-955c-446e-b233-7977716c4a9c",
checkpoint_ns: "",
},
checkpoint_id: "1f03278a-206b-67c6-8000-ac34a0872e1a",
parent_checkpoint_id: "1f03278a-2067-6590-bfff-3fb740466fc3",
},
{
values: {
messages: [],
},
next: ["__start__"],
tasks: [
{
id: "291af033-2ddc-3320-8bbc-28060057cae5",
name: "__start__",
path: ["__pregel_pull", "__start__"],
error: null,
interrupts: [],
checkpoint: null,
state: null,
result: {
messages: [
{
id: "2d8c0d9f-a614-4e44-b474-6a56e9471cf5",
type: "human",
content: "Hey",
},
],
},
},
],
metadata: {
run_attempt: 1,
source: "input",
writes: {
__start__: {
messages: [
{
id: "2d8c0d9f-a614-4e44-b474-6a56e9471cf5",
type: "human",
content: "Hey",
},
],
},
},
step: -1,
parents: {},
},
created_at: "2025-05-16T17:10:14.428191+00:00",
checkpoint: {
checkpoint_id: "1f03278a-2067-6590-bfff-3fb740466fc3",
thread_id: "b06fd92a-955c-446e-b233-7977716c4a9c",
checkpoint_ns: "",
},
parent_checkpoint: null,
checkpoint_id: "1f03278a-2067-6590-bfff-3fb740466fc3",
parent_checkpoint_id: null,
},
]);
}),
);
return new HttpResponse(stream, {
headers: { "Content-Type": "text/event-stream" },
});
}),
);
server.use;
describe("useStream", () => {
beforeEach(() => server.listen());
@@ -417,10 +114,8 @@ describe("useStream", () => {
// Wait for messages to appear
await waitFor(() => {
expect(screen.getByTestId("message-0")).toHaveTextContent("Hey");
expect(screen.getByTestId("message-1")).toHaveTextContent(
"Hello! How can I assist you today?",
);
expect(screen.getByTestId("message-0")).toHaveTextContent("Hello");
expect(screen.getByTestId("message-1")).toHaveTextContent("Hey");
});
// Check final state
@@ -440,4 +135,316 @@ describe("useStream", () => {
expect(screen.getByTestId("loading")).toHaveTextContent("Not loading");
});
});
it("displays initial values immediately and clears them when submitting", async () => {
const user = userEvent.setup();
function TestCachedComponent() {
const { messages, values, submit } = useStream<{
messages: Message[];
}>({
assistantId: "agent",
apiKey: "test-api-key",
initialValues: {
messages: [
{ id: "cached-1", type: "human", content: "Cached user message" },
{ id: "cached-2", type: "ai", content: "Cached AI response" },
],
},
});
return (
<div>
<div data-testid="messages">
{messages.map((msg, i) => (
<div
key={msg.id ?? i}
data-testid={
msg.id?.includes("cached")
? `message-cached-${i}`
: `message-${i}`
}
>
{typeof msg.content === "string"
? msg.content
: JSON.stringify(msg.content)}
</div>
))}
</div>
<div data-testid="values">{JSON.stringify(values)}</div>
<button
data-testid="submit"
onClick={() =>
submit({ messages: [{ content: "Hello", type: "human" }] })
}
>
Submit
</button>
</div>
);
}
render(<TestCachedComponent />);
// Should immediately show cached messages
expect(screen.getByTestId("message-cached-0")).toHaveTextContent(
"Cached user message",
);
expect(screen.getByTestId("message-cached-1")).toHaveTextContent(
"Cached AI response",
);
// Values should include initial values
expect(screen.getByTestId("values")).toHaveTextContent(
"Cached user message",
);
// Submitting should clear out the cached messages
await user.click(screen.getByTestId("submit"));
// Wait for messages to appear
await waitFor(() => {
expect(screen.getByTestId("message-0")).toHaveTextContent("Hello");
expect(screen.getByTestId("message-1")).toHaveTextContent("Hey");
});
});
it("accepts newThreadId option without errors", async () => {
const user = userEvent.setup();
const spy = vi.fn();
const predeterminedThreadId = randomUUID();
// Test that newThreadId option can be passed without causing errors
function TestNewThreadComponent() {
const stream = useStream<{ messages: Message[] }>({
assistantId: "agent",
apiKey: "test-api-key",
threadId: null, // Start with no thread
onThreadId: spy, // Mock callback
});
return (
<div>
<div data-testid="loading">
{stream.isLoading ? "Loading..." : "Not loading"}
</div>
<div data-testid="thread-id">
{stream.client ? "Client ready" : "No client"}
</div>
<button
data-testid="submit"
onClick={() =>
stream.submit({}, { threadId: predeterminedThreadId })
}
>
Submit
</button>
</div>
);
}
render(<TestNewThreadComponent />);
// Should render without errors
expect(screen.getByTestId("loading")).toHaveTextContent("Not loading");
expect(screen.getByTestId("thread-id")).toHaveTextContent("Client ready");
await user.click(screen.getByTestId("submit"));
expect(spy).toHaveBeenCalledWith(predeterminedThreadId);
expect(await threads.get(predeterminedThreadId)).toEqual({
thread_id: predeterminedThreadId,
metadata: {
graph_id: "agent",
assistant_id: "agent",
},
});
});
it("onStop callback is called when stop is called", async () => {
const user = userEvent.setup();
const onStopCallback = vi.fn();
function TestComponent() {
const { submit, stop } = useStream({
assistantId: "agent",
apiKey: "test-api-key",
onStop: onStopCallback,
});
return (
<div>
<button data-testid="submit" onClick={() => submit({})}>
Send
</button>
<button data-testid="stop" onClick={stop}>
Stop
</button>
</div>
);
}
render(<TestComponent />);
// Start a stream and stop it
await user.click(screen.getByTestId("submit"));
await user.click(screen.getByTestId("stop"));
// Verify onStop was called with mutate function
expect(onStopCallback).toHaveBeenCalledTimes(1);
expect(onStopCallback).toHaveBeenCalledWith(
expect.objectContaining({
mutate: expect.any(Function),
}),
);
});
it("onStop mutate function updates stream values immediately", async () => {
const user = userEvent.setup();
function TestComponent() {
const [stopped, setStopped] = useState(false);
const { submit, stop, messages } = useStream<{ messages: Message[] }>({
assistantId: "agent",
apiKey: "test-api-key",
onStop: ({ mutate }) => {
setStopped(true);
mutate((prev) => ({
...prev,
messages: [
...(prev.messages ?? []),
{ type: "ai", content: "Stream stopped" },
],
}));
},
});
return (
<div>
<div data-testid="stopped-status">
{stopped ? "Stopped" : "Not stopped"}
</div>
<div data-testid="messages">
{messages.map((msg, i) => (
<div key={msg.id ?? i} data-testid={`message-${i}`}>
{typeof msg.content === "string"
? msg.content
: JSON.stringify(msg.content)}
</div>
))}
</div>
<button data-testid="submit" onClick={() => submit({})}>
Send
</button>
<button data-testid="stop" onClick={stop}>
Stop
</button>
</div>
);
}
render(<TestComponent />);
// Initial state
expect(screen.getByTestId("stopped-status")).toHaveTextContent(
"Not stopped",
);
// Start and stop stream
await user.click(screen.getByTestId("submit"));
await user.click(screen.getByTestId("stop"));
// Verify state was updated immediately
await waitFor(() => {
expect(screen.getByTestId("stopped-status")).toHaveTextContent("Stopped");
expect(screen.getByTestId("message-0")).toHaveTextContent(
"Stream stopped",
);
});
});
it("onStop handles functional updates correctly", async () => {
const user = userEvent.setup();
function TestComponent() {
const { submit, stop, values } = useStream({
assistantId: "agent",
apiKey: "test-api-key",
initialValues: {
counter: 5,
items: ["item1", "item2"],
},
onStop: ({ mutate }) => {
mutate((prev: any) => ({
...prev,
counter: (prev.counter || 0) + 10,
items: [...(prev.items || []), "stopped"],
}));
},
});
return (
<div>
<div data-testid="counter">{(values as any).counter}</div>
<div data-testid="items">{(values as any).items?.join(", ")}</div>
<button data-testid="submit" onClick={() => submit({})}>
Send
</button>
<button data-testid="stop" onClick={stop}>
Stop
</button>
</div>
);
}
render(<TestComponent />);
// Initial state
expect(screen.getByTestId("counter")).toHaveTextContent("5");
expect(screen.getByTestId("items")).toHaveTextContent("item1, item2");
// Start and stop stream
await user.click(screen.getByTestId("submit"));
await user.click(screen.getByTestId("stop"));
// Verify functional update was applied correctly
await waitFor(() => {
expect(screen.getByTestId("counter")).toHaveTextContent("15");
expect(screen.getByTestId("items")).toHaveTextContent(
"item1, item2, stopped",
);
});
});
it("onStop is not called when stream completes naturally", async () => {
const user = userEvent.setup();
const onStopCallback = vi.fn();
function TestComponent() {
const { submit } = useStream({
assistantId: "agent",
apiKey: "test-api-key",
onStop: onStopCallback,
});
return (
<div>
<button data-testid="submit" onClick={() => submit({})}>
Send
</button>
</div>
);
}
render(<TestComponent />);
// Start a stream and let it complete naturally
await user.click(screen.getByTestId("submit"));
// Wait for stream to complete naturally
await waitFor(() => {
expect(onStopCallback).not.toHaveBeenCalled();
});
});
});
+16 -23
View File
@@ -13,16 +13,22 @@ type MessageContent = string | MessageContentComplex[];
*/
type MessageAdditionalKwargs = Record<string, unknown>;
export type HumanMessage = {
type: "human";
id?: string | undefined;
type BaseMessage = {
additional_kwargs?: MessageAdditionalKwargs | undefined;
content: MessageContent;
id?: string | undefined;
name?: string | undefined;
response_metadata?: Record<string, unknown> | undefined;
};
export type AIMessage = {
export type HumanMessage = BaseMessage & {
type: "human";
example?: boolean | undefined;
};
export type AIMessage = BaseMessage & {
type: "ai";
id?: string | undefined;
content: MessageContent;
example?: boolean | undefined;
tool_calls?:
| {
name: string;
@@ -57,19 +63,12 @@ export type AIMessage = {
| undefined;
}
| undefined;
additional_kwargs?: MessageAdditionalKwargs | undefined;
response_metadata?: Record<string, unknown> | undefined;
};
export type ToolMessage = {
export type ToolMessage = BaseMessage & {
type: "tool";
name?: string | undefined;
id?: string | undefined;
content: MessageContent;
status?: "error" | "success" | undefined;
tool_call_id: string;
additional_kwargs?: MessageAdditionalKwargs | undefined;
response_metadata?: Record<string, unknown> | undefined;
/**
* Artifact of the Tool execution which is not meant to be sent to the model.
*
@@ -81,22 +80,16 @@ export type ToolMessage = {
artifact?: any;
};
export type SystemMessage = {
export type SystemMessage = BaseMessage & {
type: "system";
id?: string | undefined;
content: MessageContent;
};
export type FunctionMessage = {
export type FunctionMessage = BaseMessage & {
type: "function";
id?: string | undefined;
content: MessageContent;
};
export type RemoveMessage = {
export type RemoveMessage = BaseMessage & {
type: "remove";
id: string;
content: MessageContent;
};
export type Message =
+119 -123
View File
@@ -6,90 +6,88 @@ const SPACE = " ".charCodeAt(0);
const TRAILING_NEWLINE = [CR, LF];
export class BytesLineDecoder extends TransformStream<Uint8Array, Uint8Array> {
constructor() {
let buffer: Uint8Array[] = [];
let trailingCr = false;
export function BytesLineDecoder() {
let buffer: Uint8Array[] = [];
let trailingCr = false;
super({
start() {
buffer = [];
return new TransformStream<Uint8Array, Uint8Array>({
start() {
buffer = [];
trailingCr = false;
},
transform(chunk, controller) {
// See https://docs.python.org/3/glossary.html#term-universal-newlines
let text = chunk;
// Handle trailing CR from previous chunk
if (trailingCr) {
text = joinArrays([[CR], text]);
trailingCr = false;
},
}
transform(chunk, controller) {
// See https://docs.python.org/3/glossary.html#term-universal-newlines
let text = chunk;
// Check for trailing CR in current chunk
if (text.length > 0 && text.at(-1) === CR) {
trailingCr = true;
text = text.subarray(0, -1);
}
// Handle trailing CR from previous chunk
if (trailingCr) {
text = joinArrays([[CR], text]);
trailingCr = false;
}
if (!text.length) return;
const trailingNewline = TRAILING_NEWLINE.includes(text.at(-1)!);
// Check for trailing CR in current chunk
if (text.length > 0 && text.at(-1) === CR) {
trailingCr = true;
text = text.subarray(0, -1);
}
const lastIdx = text.length - 1;
const { lines } = text.reduce<{ lines: Uint8Array[]; from: number }>(
(acc, cur, idx) => {
if (acc.from > idx) return acc;
if (!text.length) return;
const trailingNewline = TRAILING_NEWLINE.includes(text.at(-1)!);
const lastIdx = text.length - 1;
const { lines } = text.reduce<{ lines: Uint8Array[]; from: number }>(
(acc, cur, idx) => {
if (acc.from > idx) return acc;
if (cur === CR || cur === LF) {
acc.lines.push(text.subarray(acc.from, idx));
if (cur === CR && text[idx + 1] === LF) {
acc.from = idx + 2;
} else {
acc.from = idx + 1;
}
if (cur === CR || cur === LF) {
acc.lines.push(text.subarray(acc.from, idx));
if (cur === CR && text[idx + 1] === LF) {
acc.from = idx + 2;
} else {
acc.from = idx + 1;
}
}
if (idx === lastIdx && acc.from <= lastIdx) {
acc.lines.push(text.subarray(acc.from));
}
if (idx === lastIdx && acc.from <= lastIdx) {
acc.lines.push(text.subarray(acc.from));
}
return acc;
},
{ lines: [], from: 0 },
);
return acc;
},
{ lines: [], from: 0 },
);
if (lines.length === 1 && !trailingNewline) {
buffer.push(lines[0]);
return;
}
if (lines.length === 1 && !trailingNewline) {
buffer.push(lines[0]);
return;
}
if (buffer.length) {
// Include existing buffer in first line
buffer.push(lines[0]);
lines[0] = joinArrays(buffer);
buffer = [];
}
if (buffer.length) {
// Include existing buffer in first line
buffer.push(lines[0]);
lines[0] = joinArrays(buffer);
buffer = [];
}
if (!trailingNewline) {
// If the last segment is not newline terminated,
// buffer it for the next chunk
if (lines.length) buffer = [lines.pop()!];
}
if (!trailingNewline) {
// If the last segment is not newline terminated,
// buffer it for the next chunk
if (lines.length) buffer = [lines.pop()!];
}
// Enqueue complete lines
for (const line of lines) {
controller.enqueue(line);
}
},
// Enqueue complete lines
for (const line of lines) {
controller.enqueue(line);
}
},
flush(controller) {
if (buffer.length) {
controller.enqueue(joinArrays(buffer));
}
},
});
}
flush(controller) {
if (buffer.length) {
controller.enqueue(joinArrays(buffer));
}
},
});
}
interface StreamPart {
@@ -98,69 +96,67 @@ interface StreamPart {
data: unknown;
}
export class SSEDecoder extends TransformStream<Uint8Array, StreamPart> {
constructor() {
let event = "";
let data: Uint8Array[] = [];
let lastEventId = "";
let retry: number | null = null;
export function SSEDecoder() {
let event = "";
let data: Uint8Array[] = [];
let lastEventId = "";
let retry: number | null = null;
const decoder = new TextDecoder();
const decoder = new TextDecoder();
super({
transform(chunk, controller) {
// Handle empty line case
if (!chunk.length) {
if (!event && !data.length && !lastEventId && retry == null) return;
return new TransformStream<Uint8Array, StreamPart>({
transform(chunk, controller) {
// Handle empty line case
if (!chunk.length) {
if (!event && !data.length && !lastEventId && retry == null) return;
const sse = {
id: lastEventId || undefined,
event,
data: data.length ? decodeArraysToJson(decoder, data) : null,
};
const sse = {
id: lastEventId || undefined,
event,
data: data.length ? decodeArraysToJson(decoder, data) : null,
};
// NOTE: as per the SSE spec, do not reset lastEventId
event = "";
data = [];
retry = null;
// NOTE: as per the SSE spec, do not reset lastEventId
event = "";
data = [];
retry = null;
controller.enqueue(sse);
return;
}
controller.enqueue(sse);
return;
}
// Ignore comments
if (chunk[0] === COLON) return;
// Ignore comments
if (chunk[0] === COLON) return;
const sepIdx = chunk.indexOf(COLON);
if (sepIdx === -1) return;
const sepIdx = chunk.indexOf(COLON);
if (sepIdx === -1) return;
const fieldName = decoder.decode(chunk.subarray(0, sepIdx));
let value = chunk.subarray(sepIdx + 1);
if (value[0] === SPACE) value = value.subarray(1);
const fieldName = decoder.decode(chunk.subarray(0, sepIdx));
let value = chunk.subarray(sepIdx + 1);
if (value[0] === SPACE) value = value.subarray(1);
if (fieldName === "event") {
event = decoder.decode(value);
} else if (fieldName === "data") {
data.push(value);
} else if (fieldName === "id") {
if (value.indexOf(NULL) === -1) lastEventId = decoder.decode(value);
} else if (fieldName === "retry") {
const retryNum = Number.parseInt(decoder.decode(value));
if (!Number.isNaN(retryNum)) retry = retryNum;
}
},
if (fieldName === "event") {
event = decoder.decode(value);
} else if (fieldName === "data") {
data.push(value);
} else if (fieldName === "id") {
if (value.indexOf(NULL) === -1) lastEventId = decoder.decode(value);
} else if (fieldName === "retry") {
const retryNum = Number.parseInt(decoder.decode(value));
if (!Number.isNaN(retryNum)) retry = retryNum;
}
},
flush(controller) {
if (event) {
controller.enqueue({
id: lastEventId || undefined,
event,
data: data.length ? decodeArraysToJson(decoder, data) : null,
});
}
},
});
}
flush(controller) {
if (event) {
controller.enqueue({
id: lastEventId || undefined,
event,
data: data.length ? decodeArraysToJson(decoder, data) : null,
});
}
},
});
}
function joinArrays(data: ArrayLike<number>[]) {
+1046 -22
View File
File diff suppressed because it is too large Load Diff