mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-22 15:42:25 +02:00
feat: add docs translations (#5552)
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com> Co-authored-by: Tat Dat Duong <david@duong.cz>
This commit is contained in:
co-authored by
Eugene Yurtsev
Tat Dat Duong
parent
72e418e4d0
commit
d59091672f
@@ -2,7 +2,13 @@
|
||||
|
||||
In [the last tutorial](resource_auth.md), you added [resource authorization](../../tutorials/auth/resource_auth.md) to give users private conversations. However, you are still using hard-coded tokens for authentication, which is not secure. Now you'll replace those tokens with real user accounts using [OAuth2](../auth/getting_started.md).
|
||||
|
||||
:::python
|
||||
You'll keep the same [`Auth`](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) object and [resource-level access control](../../concepts/auth.md#single-owner-resources), but upgrade authentication to use Supabase as your identity provider. While Supabase is used in this tutorial, the concepts apply to any OAuth2 provider. You'll learn how to:
|
||||
:::
|
||||
|
||||
:::js
|
||||
You'll keep the same [`Auth`](../../cloud/reference/sdk/typescript_sdk_ref.md#auth) object and [resource-level access control](../../concepts/auth.md#single-owner-resources), but upgrade authentication to use Supabase as your identity provider. While Supabase is used in this tutorial, the concepts apply to any OAuth2 provider. You'll learn how to:
|
||||
:::
|
||||
|
||||
1. Replace test tokens with real JWT tokens
|
||||
2. Integrate with OAuth2 providers for secure user authentication
|
||||
@@ -18,7 +24,6 @@ OAuth2 involves three main roles:
|
||||
|
||||
A standard OAuth2 flow works something like this:
|
||||
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
@@ -40,35 +45,49 @@ sequenceDiagram
|
||||
Before you start this tutorial, ensure you have:
|
||||
|
||||
- The [bot from the second tutorial](resource_auth.md) running without errors.
|
||||
- A [Supabase project](https://supabase.com/dashboard) to use its authentication server.
|
||||
|
||||
- A [Supabase project](https://supabase.com/dashboard) to use as your authentication server.
|
||||
|
||||
## 1. Install dependencies
|
||||
|
||||
Install the required dependencies. Start in your `custom-auth` directory and ensure you have the `langgraph-cli` installed:
|
||||
|
||||
:::python
|
||||
|
||||
```bash
|
||||
cd custom-auth
|
||||
pip install -U "langgraph-cli[inmem]"
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```bash
|
||||
cd custom-auth
|
||||
npm install -g @langchain/langgraph-cli
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 2. Set up the authentication provider {#setup-auth-provider}
|
||||
|
||||
Next, fetch the URL of your auth server and the private key for authentication.
|
||||
Since you're using Supabase for this, you can do this in the Supabase dashboard:
|
||||
|
||||
1. In the left sidebar, click on t️⚙ Project Settings" and then click "API"
|
||||
1. Copy your project URL and add it to your `.env` file
|
||||
1. In the left sidebar, click on "⚙️ Project Settings" and then click "API"
|
||||
2. Copy your project URL and add it to your `.env` file
|
||||
|
||||
```shell
|
||||
echo "SUPABASE_URL=your-project-url" >> .env
|
||||
```
|
||||
1. Copy your service role secret key and add it to your `.env` file:
|
||||
|
||||
3. Copy your service role secret key and add it to your `.env` file:
|
||||
|
||||
```shell
|
||||
echo "SUPABASE_SERVICE_KEY=your-service-role-key" >> .env
|
||||
```
|
||||
1. Copy your "anon public" key and note it down. This will be used later when you set up our client code.
|
||||
|
||||
4. Copy your "anon public" key and note it down. This will be used later when you set up our client code.
|
||||
|
||||
```bash
|
||||
SUPABASE_URL=your-project-url
|
||||
@@ -77,14 +96,23 @@ Since you're using Supabase for this, you can do this in the Supabase dashboard:
|
||||
|
||||
## 3. Implement token validation
|
||||
|
||||
:::python
|
||||
In the previous tutorials, you used the [`Auth`](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) object to [validate hard-coded tokens](getting_started.md) and [add resource ownership](resource_auth.md).
|
||||
|
||||
Now you'll upgrade your authentication to validate real JWT tokens from Supabase. The main changes will all be in the [`@auth.authenticate`](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.authenticate) decorated function:
|
||||
:::
|
||||
|
||||
:::js
|
||||
In the previous tutorials, you used the [`Auth`](../../cloud/reference/sdk/typescript_sdk_ref.md#auth) object to [validate hard-coded tokens](getting_started.md) and [add resource ownership](resource_auth.md).
|
||||
|
||||
Now you'll upgrade your authentication to validate real JWT tokens from Supabase. The main changes will all be in the [`auth.authenticate`](../../cloud/reference/sdk/typescript_sdk_ref.md#auth) decorated function:
|
||||
:::
|
||||
|
||||
- Instead of checking against a hard-coded list of tokens, you'll make an HTTP request to Supabase to validate the token.
|
||||
- You'll extract real user information (ID, email) from the validated token.
|
||||
- The existing resource authorization logic remains unchanged.
|
||||
|
||||
:::python
|
||||
Update `src/security/auth.py` to implement this:
|
||||
|
||||
```python hl_lines="8-9 20-30" title="src/security/auth.py"
|
||||
@@ -138,6 +166,69 @@ async def add_owner(ctx, value):
|
||||
return filters
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
Update `src/security/auth.ts` to implement this:
|
||||
|
||||
```typescript hl_lines="1-2 9-10 21-31" title="src/security/auth.ts"
|
||||
import { Auth } from "@langchain/langgraph-sdk";
|
||||
|
||||
// This is loaded from the `.env` file you created above
|
||||
const SUPABASE_URL = process.env.SUPABASE_URL;
|
||||
const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_KEY;
|
||||
|
||||
const auth = new Auth()
|
||||
.authenticate(async (request) => {
|
||||
// Validate JWT tokens and extract user information.
|
||||
const apiKey = request.headers.get("x-api-key");
|
||||
if (!apiKey || !isValidKey(apiKey)) {
|
||||
throw new HTTPException(401, "Invalid API key");
|
||||
}
|
||||
|
||||
const [scheme, token] = apiKey.split(" ");
|
||||
if (scheme.toLowerCase() !== "bearer") {
|
||||
throw new Error("Invalid authorization scheme");
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify token with auth provider
|
||||
const response = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
|
||||
headers: {
|
||||
Authorization: authorization,
|
||||
apiKey: SUPABASE_SERVICE_KEY!,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error("Invalid token");
|
||||
}
|
||||
|
||||
const user = await response.json();
|
||||
return {
|
||||
identity: user.id, // Unique user identifier
|
||||
email: user.email,
|
||||
is_authenticated: true,
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Auth.HTTPException(401, String(e));
|
||||
}
|
||||
})
|
||||
.on(async ({ user, value }) => {
|
||||
// Keep our resource authorization from the previous tutorial
|
||||
// Make resources private to their creator using resource metadata.
|
||||
const filters = { owner: user.identity };
|
||||
const metadata = value.metadata || {};
|
||||
Object.assign(metadata, filters);
|
||||
value.metadata = metadata;
|
||||
return filters;
|
||||
});
|
||||
|
||||
export { auth };
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
The most important change is that we're now validating tokens with a real authentication server. Our authentication handler has the private key for our Supabase project, which we can use to validate the user's token and extract their information.
|
||||
|
||||
## 4. Test authentication flow
|
||||
@@ -148,6 +239,8 @@ Let's test out the new authentication flow. You can run the following code in a
|
||||
- A Supabase project URL (from [above](#setup-auth-provider))
|
||||
- A Supabase anon **public key** (also from [above](#setup-auth-provider))
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
import os
|
||||
import httpx
|
||||
@@ -190,9 +283,63 @@ await sign_up(email1, password)
|
||||
await sign_up(email2, password)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
// Get email from command line
|
||||
const email = process.env.TEST_EMAIL || "your-email@example.com";
|
||||
const baseEmail = email.split("@");
|
||||
const password = "secure-password"; // CHANGEME
|
||||
const email1 = `${baseEmail[0]}+1@${baseEmail[1]}`;
|
||||
const email2 = `${baseEmail[0]}+2@${baseEmail[1]}`;
|
||||
|
||||
const SUPABASE_URL = process.env.SUPABASE_URL;
|
||||
if (!SUPABASE_URL) {
|
||||
throw new Error("SUPABASE_URL environment variable is required");
|
||||
}
|
||||
|
||||
// This is your PUBLIC anon key (which is safe to use client-side)
|
||||
// Do NOT mistake this for the secret service role key
|
||||
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY;
|
||||
if (!SUPABASE_ANON_KEY) {
|
||||
throw new Error("SUPABASE_ANON_KEY environment variable is required");
|
||||
}
|
||||
|
||||
async function signUp(email: string, password: string) {
|
||||
/**Create a new user account.*/
|
||||
const response = await fetch(`${SUPABASE_URL}/auth/v1/signup`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
apiKey: SUPABASE_ANON_KEY,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to sign up: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Create two test users
|
||||
console.log(`Creating test users: ${email1} and ${email2}`);
|
||||
await signUp(email1, password);
|
||||
await signUp(email2, password);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
⚠️ Before continuing: Check your email and click both confirmation links. Supabase will reject `/login` requests until after you have confirmed your users' email.
|
||||
|
||||
Now test that users can only see their own data. Make sure the server is running (run `langgraph dev`) before proceeding. The following snippet requires the "anon public" key that you copied from the Supabase dashboard while [setting up the auth provider](#setup-auth-provider) previously.
|
||||
Now test that users can only see their own data. Make sure the server is running (run `langgraph dev`) before proceeding. The following snippet requires the "anon public" key that you copied from the Supabase dashboard while [setting up the auth provider](#setup-auth-provider) previously.
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
async def login(email: str, password: str):
|
||||
@@ -243,6 +390,71 @@ try:
|
||||
except Exception as e:
|
||||
print("✅ User 2 blocked from User 1's thread:", e)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
async function login(email: string, password: string): Promise<string> {
|
||||
/**Get an access token for an existing user.*/
|
||||
const response = await fetch(
|
||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
apikey: SUPABASE_ANON_KEY,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to login: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.access_token;
|
||||
}
|
||||
|
||||
// Log in as user 1
|
||||
const user1Token = await login(email1, password);
|
||||
const user1Client = new Client({
|
||||
apiUrl: "http://localhost:2024",
|
||||
headers: { Authorization: `Bearer ${user1Token}` },
|
||||
});
|
||||
|
||||
// Create a thread as user 1
|
||||
const thread = await user1Client.threads.create();
|
||||
console.log(`✅ User 1 created thread: ${thread.thread_id}`);
|
||||
|
||||
// Try to access without a token
|
||||
const unauthenticatedClient = new Client({ apiUrl: "http://localhost:2024" });
|
||||
try {
|
||||
await unauthenticatedClient.threads.create();
|
||||
console.log("❌ Unauthenticated access should fail!");
|
||||
} catch (e) {
|
||||
console.log("✅ Unauthenticated access blocked:", e.message);
|
||||
}
|
||||
|
||||
// Try to access user 1's thread as user 2
|
||||
const user2Token = await login(email2, password);
|
||||
const user2Client = new Client({
|
||||
apiUrl: "http://localhost:2024",
|
||||
headers: { Authorization: `Bearer ${user2Token}` },
|
||||
});
|
||||
|
||||
try {
|
||||
await user2Client.threads.get(thread.thread_id);
|
||||
console.log("❌ User 2 shouldn't see User 1's thread!");
|
||||
} catch (e) {
|
||||
console.log("✅ User 2 blocked from User 1's thread:", e.message);
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
The output should look like this:
|
||||
|
||||
```shell
|
||||
@@ -272,4 +484,11 @@ Now that you have production authentication, consider:
|
||||
|
||||
1. Building a web UI with your preferred framework (see the [Custom Auth](https://github.com/langchain-ai/custom-auth) template for an example)
|
||||
2. Learn more about the other aspects of authentication and authorization in the [conceptual guide on authentication](../../concepts/auth.md).
|
||||
3. Customize your handlers and setup further after reading the [reference docs](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth).
|
||||
|
||||
:::python
|
||||
3. Customize your handlers and setup further after reading the [reference docs](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth).
|
||||
:::
|
||||
|
||||
:::js
|
||||
3. Customize your handlers and setup further after reading the [reference docs](../../cloud/reference/sdk/typescript_sdk_ref.md#auth).
|
||||
:::
|
||||
|
||||
@@ -10,8 +10,8 @@ This is part 1 of our authentication series:
|
||||
|
||||
This guide assumes basic familiarity with the following concepts:
|
||||
|
||||
* [**Authentication & Access Control**](../../concepts/auth.md)
|
||||
* [**LangGraph Platform**](../../concepts/langgraph_platform.md)
|
||||
- [**Authentication & Access Control**](../../concepts/auth.md)
|
||||
- [**LangGraph Platform**](../../concepts/langgraph_platform.md)
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -21,26 +21,52 @@ This guide assumes basic familiarity with the following concepts:
|
||||
|
||||
Create a new chatbot using the LangGraph starter template:
|
||||
|
||||
:::python
|
||||
|
||||
```bash
|
||||
pip install -U "langgraph-cli[inmem]"
|
||||
langgraph new --template=new-langgraph-project-python custom-auth
|
||||
cd custom-auth
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```bash
|
||||
npx @langchain/langgraph-cli new --template=new-langgraph-project-typescript custom-auth
|
||||
cd custom-auth
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
The template gives us a placeholder LangGraph app. Try it out by installing the local dependencies and running the development server:
|
||||
|
||||
:::python
|
||||
|
||||
```shell
|
||||
pip install -e .
|
||||
langgraph dev
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```shell
|
||||
npm install
|
||||
npm run langgraph dev
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
The server will start and open the studio in your browser:
|
||||
|
||||
```
|
||||
> - 🚀 API: http://127.0.0.1:2024
|
||||
> - 🎨 Studio UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
|
||||
> - 📚 API Docs: http://127.0.0.1:2024/docs
|
||||
>
|
||||
>
|
||||
> This in-memory server is designed for development and testing.
|
||||
> For production use, please use LangGraph Platform.
|
||||
```
|
||||
@@ -49,7 +75,6 @@ If you were to self-host this on the public internet, anyone could access it!
|
||||
|
||||

|
||||
|
||||
|
||||
## 2. Add authentication
|
||||
|
||||
Now that you have a base LangGraph app, add authentication to it.
|
||||
@@ -58,6 +83,7 @@ Now that you have a base LangGraph app, add authentication to it.
|
||||
|
||||
In this tutorial, you will start with a hard-coded token for example purposes. You will get to a "production-ready" authentication scheme in the third tutorial.
|
||||
|
||||
:::python
|
||||
The [`Auth`](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) object lets you register an authentication function that the LangGraph platform will run on every request. This function receives each request and decides whether to accept or reject.
|
||||
|
||||
Create a new file `src/security/auth.py`. This is where your code will live to check if users are allowed to access your bot:
|
||||
@@ -98,9 +124,61 @@ Notice that your [authentication](../../cloud/reference/sdk/python_sdk_ref.md#la
|
||||
|
||||
1. Checks if a valid token is provided in the request's [Authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization)
|
||||
2. Returns the user's [identity](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.types.MinimalUserDict)
|
||||
:::
|
||||
|
||||
:::js
|
||||
The [`Auth`](../../cloud/reference/sdk/js_sdk_ref.md#Auth) object lets you register an authentication function that the LangGraph platform will run on every request. This function receives each request and decides whether to accept or reject.
|
||||
|
||||
Create a new file `src/security/auth.ts`. This is where your code will live to check if users are allowed to access your bot:
|
||||
|
||||
```typescript title="src/security/auth.ts"
|
||||
import { Auth } from "@langchain/langgraph-sdk";
|
||||
|
||||
// This is our toy user database. Do not do this in production
|
||||
const VALID_TOKENS: Record<string, { id: string; name: string }> = {
|
||||
"user1-token": { id: "user1", name: "Alice" },
|
||||
"user2-token": { id: "user2", name: "Bob" },
|
||||
};
|
||||
|
||||
// The "Auth" object is a container that LangGraph will use to mark our authentication function
|
||||
const auth = new Auth();
|
||||
// The `authenticate` method tells LangGraph to call this function as middleware
|
||||
// for every request. This will determine whether the request is allowed or not
|
||||
.authenticate((request) => {
|
||||
// Our authentication handler from the previous tutorial.
|
||||
const apiKey = request.headers.get("x-api-key");
|
||||
if (!apiKey || !isValidKey(apiKey)) {
|
||||
throw new HTTPException(401, "Invalid API key");
|
||||
}
|
||||
|
||||
const [scheme, token] = apiKey.split(" ");
|
||||
if (scheme.toLowerCase() !== "bearer") {
|
||||
throw new Error("Bearer token required");
|
||||
}
|
||||
|
||||
if (!VALID_TOKENS[token]) {
|
||||
throw new HTTPException(401, "Invalid token");
|
||||
}
|
||||
|
||||
const userData = VALID_TOKENS[token];
|
||||
return {
|
||||
identity: userData.id,
|
||||
};
|
||||
});
|
||||
|
||||
export { auth };
|
||||
```
|
||||
|
||||
Notice that your [authentication](../../cloud/reference/sdk/js_sdk_ref.md#Auth) handler does two important things:
|
||||
|
||||
1. Checks if a valid token is provided in the request's [Authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization)
|
||||
2. Returns the user's [identity](../../cloud/reference/sdk/js_sdk_ref.md#Auth.types.MinimalUserDict)
|
||||
:::
|
||||
|
||||
Now tell LangGraph to use authentication by adding the following to the [`langgraph.json`](../../cloud/reference/cli.md#configuration-file) configuration:
|
||||
|
||||
:::python
|
||||
|
||||
```json hl_lines="7-9" title="langgraph.json"
|
||||
{
|
||||
"dependencies": ["."],
|
||||
@@ -114,6 +192,25 @@ Now tell LangGraph to use authentication by adding the following to the [`langgr
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```json hl_lines="7-9" title="langgraph.json"
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"agent": "./src/agent/graph.ts:graph"
|
||||
},
|
||||
"env": ".env",
|
||||
"auth": {
|
||||
"path": "src/security/auth.ts:auth"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 3. Test your bot
|
||||
|
||||
Start the server again to test everything out:
|
||||
@@ -124,21 +221,39 @@ langgraph dev --no-browser
|
||||
|
||||
If you didn't add the `--no-browser`, the studio UI will open in the browser. You may wonder, how is the studio able to still connect to our server? By default, we also permit access from the LangGraph studio, even when using custom auth. This makes it easier to develop and test your bot in the studio. You can remove this alternative authentication option by setting `disable_studio_auth: "true"` in your auth configuration:
|
||||
|
||||
:::python
|
||||
|
||||
```json
|
||||
{
|
||||
"auth": {
|
||||
"path": "src/security/auth.py:auth",
|
||||
"disable_studio_auth": "true"
|
||||
}
|
||||
"auth": {
|
||||
"path": "src/security/auth.py:auth",
|
||||
"disable_studio_auth": "true"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```json
|
||||
{
|
||||
"auth": {
|
||||
"path": "src/security/auth.ts:auth",
|
||||
"disable_studio_auth": "true"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 4. Chat with your bot
|
||||
|
||||
You should now only be able to access the bot if you provide a valid token in the request header. Users will still, however, be able to access each other's resources until you add [resource authorization handlers](../../concepts/auth.md#resource-specific-handlers) in the next section of the tutorial.
|
||||
|
||||

|
||||
|
||||
:::python
|
||||
Run the following code in a file or notebook:
|
||||
|
||||
```python
|
||||
@@ -170,6 +285,46 @@ print("✅ Bot responded:")
|
||||
print(response)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
Run the following code in a TypeScript file:
|
||||
|
||||
```typescript
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
async function testAuth() {
|
||||
// Try without a token (should fail)
|
||||
const clientWithoutToken = new Client({ apiUrl: "http://localhost:2024" });
|
||||
try {
|
||||
const thread = await clientWithoutToken.threads.create();
|
||||
console.log("❌ Should have failed without token!");
|
||||
} catch (e) {
|
||||
console.log("✅ Correctly blocked access:", e);
|
||||
}
|
||||
|
||||
// Try with a valid token
|
||||
const client = new Client({
|
||||
apiUrl: "http://localhost:2024",
|
||||
headers: { Authorization: "Bearer user1-token" },
|
||||
});
|
||||
|
||||
// Create a thread and chat
|
||||
const thread = await client.threads.create();
|
||||
console.log(`✅ Created thread as Alice: ${thread.thread_id}`);
|
||||
|
||||
const response = await client.runs.create(thread.thread_id, "agent", {
|
||||
input: { messages: [{ role: "user", content: "Hello!" }] },
|
||||
});
|
||||
console.log("✅ Bot responded:");
|
||||
console.log(response);
|
||||
}
|
||||
|
||||
testAuth().catch(console.error);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
You should see that:
|
||||
|
||||
1. Without a valid token, we can't access the bot
|
||||
@@ -183,4 +338,11 @@ Now that you can control who accesses your bot, you might want to:
|
||||
|
||||
1. Continue the tutorial by going to [Make conversations private](resource_auth.md) to learn about resource authorization.
|
||||
2. Read more about [authentication concepts](../../concepts/auth.md).
|
||||
3. Check out the [API reference](../../cloud/reference/sdk/python_sdk_ref.md) for more authentication details.
|
||||
|
||||
:::python
|
||||
3. Check out the [API reference](../../cloud/reference/sdk/python_sdk_ref.md) for more authentication details.
|
||||
:::
|
||||
|
||||
:::js
|
||||
3. Check out the [API reference](../../cloud/reference/sdk/js_sdk_ref.md) for more authentication details.
|
||||
:::
|
||||
|
||||
@@ -10,10 +10,17 @@ Before you start this tutorial, ensure you have the [bot from the first tutorial
|
||||
|
||||
## 1. Add resource authorization
|
||||
|
||||
:::python
|
||||
Recall that in the last tutorial, the [`Auth`](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) object lets you register an [authentication function](../../concepts/auth.md#authentication), which LangGraph Platform uses to validate the bearer tokens in incoming requests. Now you'll use it to register an **authorization** handler.
|
||||
:::
|
||||
|
||||
:::js
|
||||
Recall that in the last tutorial, the @[`Auth`][Auth] object lets you register an [authentication function](../../concepts/auth.md#authentication), which LangGraph Platform uses to validate the bearer tokens in incoming requests. Now you'll use it to register an **authorization** handler.
|
||||
:::
|
||||
|
||||
Authorization handlers are functions that run **after** authentication succeeds. These handlers can add [metadata](../../concepts/auth.md#filter-operations) to resources (like who owns them) and filter what each user can see.
|
||||
|
||||
:::python
|
||||
Update your `src/security/auth.py` and add one authorization handler to run on every request:
|
||||
|
||||
```python hl_lines="29-39" title="src/security/auth.py"
|
||||
@@ -61,7 +68,7 @@ async def add_owner(
|
||||
# resource='threads',
|
||||
# action='create_run'
|
||||
# )
|
||||
# value:
|
||||
# value:
|
||||
# {
|
||||
# 'thread_id': UUID('1e1b2733-303f-4dcd-9620-02d370287d72'),
|
||||
# 'assistant_id': UUID('fe096781-5601-53d2-b2f6-0d3403f7e9ca'),
|
||||
@@ -103,10 +110,112 @@ async def add_owner(
|
||||
return filters
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
Update your `src/security/auth.ts` and add one authorization handler to run on every request:
|
||||
|
||||
```typescript hl_lines="29-39" title="src/security/auth.ts"
|
||||
import { Auth, HTTPException } from "@langchain/langgraph-sdk";
|
||||
|
||||
// Keep our test users from the previous tutorial
|
||||
const VALID_TOKENS: Record<string, { id: string; name: string }> = {
|
||||
"user1-token": { id: "user1", name: "Alice" },
|
||||
"user2-token": { id: "user2", name: "Bob" },
|
||||
};
|
||||
|
||||
const auth = new Auth()
|
||||
.authenticate(async (request) => {
|
||||
// Our authentication handler from the previous tutorial.
|
||||
const apiKey = request.headers.get("x-api-key");
|
||||
if (!apiKey || !isValidKey(apiKey)) {
|
||||
throw new HTTPException(401, "Invalid API key");
|
||||
}
|
||||
|
||||
const [scheme, token] = apiKey.split(" ");
|
||||
if (scheme.toLowerCase() !== "bearer") {
|
||||
throw new Error("Bearer token required");
|
||||
}
|
||||
|
||||
if (!VALID_TOKENS[token]) {
|
||||
throw new HTTPException(401, "Invalid token");
|
||||
}
|
||||
|
||||
const userData = VALID_TOKENS[token];
|
||||
return {
|
||||
identity: userData.id,
|
||||
};
|
||||
})
|
||||
.on("*", ({ value, user }) => {
|
||||
// This handler makes resources private to their creator by doing 2 things:
|
||||
// 1. Add the user's ID to the resource's metadata. Each LangGraph resource has a `metadata` object that persists with the resource.
|
||||
// this metadata is useful for filtering in read and update operations
|
||||
// 2. Return a filter that lets users only see their own resources
|
||||
// Examples:
|
||||
// {
|
||||
// user: ProxyUser {
|
||||
// identity: 'user1',
|
||||
// is_authenticated: true,
|
||||
// display_name: 'user1'
|
||||
// },
|
||||
// value: {
|
||||
// 'thread_id': UUID('1e1b2733-303f-4dcd-9620-02d370287d72'),
|
||||
// 'assistant_id': UUID('fe096781-5601-53d2-b2f6-0d3403f7e9ca'),
|
||||
// 'run_id': UUID('1efbe268-1627-66d4-aa8d-b956b0f02a41'),
|
||||
// 'status': 'pending',
|
||||
// 'metadata': {},
|
||||
// 'prevent_insert_if_inflight': true,
|
||||
// 'multitask_strategy': 'reject',
|
||||
// 'if_not_exists': 'reject',
|
||||
// 'after_seconds': 0,
|
||||
// 'kwargs': {
|
||||
// 'input': {'messages': [{'role': 'user', 'content': 'Hello!'}]},
|
||||
// 'command': null,
|
||||
// 'config': {
|
||||
// 'configurable': {
|
||||
// 'langgraph_auth_user': ... Your user object...
|
||||
// 'langgraph_auth_user_id': 'user1'
|
||||
// }
|
||||
// },
|
||||
// 'stream_mode': ['values'],
|
||||
// 'interrupt_before': null,
|
||||
// 'interrupt_after': null,
|
||||
// 'webhook': null,
|
||||
// 'feedback_keys': null,
|
||||
// 'temporary': false,
|
||||
// 'subgraphs': false
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
const filters = { owner: user.identity };
|
||||
const metadata = value.metadata || {};
|
||||
Object.assign(metadata, filters);
|
||||
value.metadata = metadata;
|
||||
|
||||
// Only let users see their own resources
|
||||
return filters;
|
||||
});
|
||||
|
||||
export { auth };
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::python
|
||||
The handler receives two parameters:
|
||||
|
||||
1. `ctx` ([AuthContext](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.types.AuthContext)): contains info about the current `user`, the user's `permissions`, the `resource` ("threads", "crons", "assistants"), and the `action` being taken ("create", "read", "update", "delete", "search", "create_run")
|
||||
2. `value` (`dict`): data that is being created or accessed. The contents of this dict depend on the resource and action being accessed. See [adding scoped authorization handlers](#scoped-authorization) below for information on how to get more tightly scoped access control.
|
||||
:::
|
||||
|
||||
:::js
|
||||
The handler receives an object with the following properties:
|
||||
|
||||
1. `user` ([ProxyUser](../../cloud/reference/sdk/js_ts_sdk_ref.md#langgraph_sdk.auth.types.ProxyUser)): contains info about the current `user`, the user's `permissions`, the `resource` ("threads", "crons", "assistants")
|
||||
2. `action` contains information about the action being taken ("create", "read", "update", "delete", "search", "create_run")
|
||||
3. `value` (`Record<string, any>`): data that is being created or accessed. The contents of this object depend on the resource and action being accessed. See [adding scoped authorization handlers](#scoped-authorization) below for information on how to get more tightly scoped access control.
|
||||
:::
|
||||
|
||||
Notice that the simple handler does two things:
|
||||
|
||||
@@ -117,6 +226,8 @@ Notice that the simple handler does two things:
|
||||
|
||||
Test your authorization. If you have set things up correctly, you will see all ✅ messages. Be sure to have your development server running (run `langgraph dev`):
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
@@ -168,6 +279,64 @@ print(f"✅ Alice sees {len(alice_threads)} thread")
|
||||
print(f"✅ Bob sees {len(bob_threads)} thread")
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import { getClient } from "@langgraph/sdk";
|
||||
|
||||
// Create clients for both users
|
||||
const alice = getClient({
|
||||
url: "http://localhost:2024",
|
||||
headers: { Authorization: "Bearer user1-token" },
|
||||
});
|
||||
|
||||
const bob = getClient({
|
||||
url: "http://localhost:2024",
|
||||
headers: { Authorization: "Bearer user2-token" },
|
||||
});
|
||||
|
||||
// Alice creates an assistant
|
||||
const aliceAssistant = await alice.assistants.create();
|
||||
console.log(`✅ Alice created assistant: ${aliceAssistant.assistant_id}`);
|
||||
|
||||
// Alice creates a thread and chats
|
||||
const aliceThread = await alice.threads.create();
|
||||
console.log(`✅ Alice created thread: ${aliceThread.thread_id}`);
|
||||
|
||||
await alice.runs.create(aliceThread.thread_id, "agent", {
|
||||
input: {
|
||||
messages: [{ role: "user", content: "Hi, this is Alice's private chat" }],
|
||||
},
|
||||
});
|
||||
|
||||
// Bob tries to access Alice's thread
|
||||
try {
|
||||
await bob.threads.get(aliceThread.thread_id);
|
||||
console.log("❌ Bob shouldn't see Alice's thread!");
|
||||
} catch (error) {
|
||||
console.log("✅ Bob correctly denied access:", error);
|
||||
}
|
||||
|
||||
// Bob creates his own thread
|
||||
const bobThread = await bob.threads.create();
|
||||
await bob.runs.create(bobThread.thread_id, "agent", {
|
||||
input: {
|
||||
messages: [{ role: "user", content: "Hi, this is Bob's private chat" }],
|
||||
},
|
||||
});
|
||||
console.log(`✅ Bob created his own thread: ${bobThread.thread_id}`);
|
||||
|
||||
// List threads - each user only sees their own
|
||||
const aliceThreads = await alice.threads.search();
|
||||
const bobThreads = await bob.threads.search();
|
||||
console.log(`✅ Alice sees ${aliceThreads.length} thread`);
|
||||
console.log(`✅ Bob sees ${bobThreads.length} thread`);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Output:
|
||||
|
||||
```bash
|
||||
@@ -188,6 +357,7 @@ This means:
|
||||
|
||||
## 3. Add scoped authorization handlers {#scoped-authorization}
|
||||
|
||||
:::python
|
||||
The broad `@auth.on` handler matches on all [authorization events](../../concepts/auth.md#supported-resources). This is concise, but it means the contents of the `value` dict are not well-scoped, and the same user-level access control is applied to every resource. If you want to be more fine-grained, you can also control specific actions on resources.
|
||||
|
||||
Update `src/security/auth.py` to add handlers for specific resource types:
|
||||
@@ -203,7 +373,7 @@ async def on_thread_create(
|
||||
value: Auth.types.on.threads.create.value,
|
||||
):
|
||||
"""Add owner when creating threads.
|
||||
|
||||
|
||||
This handler runs when creating new threads and does two things:
|
||||
1. Sets metadata on the thread being created to track ownership
|
||||
2. Returns a filter that ensures only the creator can access it
|
||||
@@ -215,8 +385,7 @@ async def on_thread_create(
|
||||
# This metadata is stored with the thread and persists
|
||||
metadata = value.setdefault("metadata", {})
|
||||
metadata["owner"] = ctx.user.identity
|
||||
|
||||
|
||||
|
||||
# Return filter to restrict access to just the creator
|
||||
return {"owner": ctx.user.identity}
|
||||
|
||||
@@ -226,7 +395,7 @@ async def on_thread_read(
|
||||
value: Auth.types.on.threads.read.value,
|
||||
):
|
||||
"""Only let users read their own threads.
|
||||
|
||||
|
||||
This handler runs on read operations. We don't need to set
|
||||
metadata since the thread already exists - we just need to
|
||||
return a filter to ensure users can only see their own threads.
|
||||
@@ -261,16 +430,88 @@ async def authorize_store(ctx: Auth.types.AuthContext, value: dict):
|
||||
assert namespace[0] == ctx.user.identity, "Not authorized"
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
The broad `auth.on("*")` handler matches on all [authorization events](../../concepts/auth.md#supported-resources). This is concise, but it means the contents of the `value` object are not well-scoped, and the same user-level access control is applied to every resource. If you want to be more fine-grained, you can also control specific actions on resources.
|
||||
|
||||
Update `src/security/auth.ts` to add handlers for specific resource types:
|
||||
|
||||
```typescript
|
||||
// Keep our previous handlers...
|
||||
|
||||
import { Auth, HTTPException } from "@langchain/langgraph-sdk";
|
||||
|
||||
auth.on("threads:create", async ({ user, value }) => {
|
||||
// Add owner when creating threads.
|
||||
// This handler runs when creating new threads and does two things:
|
||||
// 1. Sets metadata on the thread being created to track ownership
|
||||
// 2. Returns a filter that ensures only the creator can access it
|
||||
|
||||
// Example value:
|
||||
// {thread_id: UUID('99b045bc-b90b-41a8-b882-dabc541cf740'), metadata: {}, if_exists: 'raise'}
|
||||
|
||||
// Add owner metadata to the thread being created
|
||||
// This metadata is stored with the thread and persists
|
||||
const metadata = value.metadata || {};
|
||||
metadata.owner = user.identity;
|
||||
value.metadata = metadata;
|
||||
|
||||
// Return filter to restrict access to just the creator
|
||||
return { owner: user.identity };
|
||||
});
|
||||
|
||||
auth.on("threads:read", async ({ user, value }) => {
|
||||
// Only let users read their own threads.
|
||||
// This handler runs on read operations. We don't need to set
|
||||
// metadata since the thread already exists - we just need to
|
||||
// return a filter to ensure users can only see their own threads.
|
||||
return { owner: user.identity };
|
||||
});
|
||||
|
||||
auth.on("assistants", async ({ user, value }) => {
|
||||
// For illustration purposes, we will deny all requests
|
||||
// that touch the assistants resource
|
||||
// Example value:
|
||||
// {
|
||||
// 'assistant_id': UUID('63ba56c3-b074-4212-96e2-cc333bbc4eb4'),
|
||||
// 'graph_id': 'agent',
|
||||
// 'config': {},
|
||||
// 'metadata': {},
|
||||
// 'name': 'Untitled'
|
||||
// }
|
||||
throw new HTTPException(403, "User lacks the required permissions.");
|
||||
});
|
||||
|
||||
auth.on("store", async ({ user, value }) => {
|
||||
// The "namespace" field for each store item is a tuple you can think of as the directory of an item.
|
||||
const namespace: string[] = value.namespace;
|
||||
if (namespace[0] !== user.identity) {
|
||||
throw new Error("Not authorized");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Notice that instead of one global handler, you now have specific handlers for:
|
||||
|
||||
1. Creating threads
|
||||
2. Reading threads
|
||||
3. Accessing assistants
|
||||
|
||||
:::python
|
||||
The first three of these match specific **actions** on each resource (see [resource actions](../../concepts/auth.md#resource-specific-handlers)), while the last one (`@auth.on.assistants`) matches _any_ action on the `assistants` resource. For each request, LangGraph will run the most specific handler that matches the resource and action being accessed. This means that the four handlers above will run rather than the broadly scoped "`@auth.on`" handler.
|
||||
:::
|
||||
|
||||
:::js
|
||||
The first three of these match specific **actions** on each resource (see [resource actions](../../concepts/auth.md#resource-specific-handlers)), while the last one (`auth.on.assistants`) matches _any_ action on the `assistants` resource. For each request, LangGraph will run the most specific handler that matches the resource and action being accessed. This means that the four handlers above will run rather than the broadly scoped "`auth.on`" handler.
|
||||
:::
|
||||
|
||||
Try adding the following test code to your test file:
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
# ... Same as before
|
||||
# Try creating an assistant. This should fail
|
||||
@@ -292,6 +533,38 @@ alice_thread = await alice.threads.create()
|
||||
print(f"✅ Alice created thread: {alice_thread['thread_id']}")
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
// ... Same as before
|
||||
// Try creating an assistant. This should fail
|
||||
try {
|
||||
await alice.assistants.create("agent");
|
||||
console.log("❌ Alice shouldn't be able to create assistants!");
|
||||
} catch (error) {
|
||||
console.log("✅ Alice correctly denied access:", error);
|
||||
}
|
||||
|
||||
// Try searching for assistants. This also should fail
|
||||
try {
|
||||
await alice.assistants.search();
|
||||
console.log("❌ Alice shouldn't be able to search assistants!");
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"✅ Alice correctly denied access to searching assistants:",
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
// Alice can still create threads
|
||||
const aliceThread = await alice.threads.create();
|
||||
console.log(`✅ Alice created thread: ${aliceThread.thread_id}`);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Output:
|
||||
|
||||
```bash
|
||||
@@ -302,7 +575,7 @@ For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/St
|
||||
✅ Alice sees 1 thread
|
||||
✅ Bob sees 1 thread
|
||||
✅ Alice correctly denied access:
|
||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500
|
||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/50j0
|
||||
✅ Alice correctly denied access to searching assistants:
|
||||
```
|
||||
|
||||
@@ -314,4 +587,11 @@ Now that you can control access to resources, you might want to:
|
||||
|
||||
1. Move on to [Connect an authentication provider](add_auth_server.md) to add real user accounts.
|
||||
2. Read more about [authorization patterns](../../concepts/auth.md#authorization).
|
||||
|
||||
:::python
|
||||
3. Check out the [API reference](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) for details about the interfaces and methods used in this tutorial.
|
||||
:::
|
||||
|
||||
:::js
|
||||
3. Check out the [API reference](../../cloud/reference/sdk/js_sdk_ref.md#langgraph_sdk.auth.Auth) for details about the interfaces and methods used in this tutorial.
|
||||
:::
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Build a basic chatbot
|
||||
|
||||
In this tutorial, you will build a basic chatbot. This chatbot is the basis for the following series of tutorials where you will progressively add more sophisticated capabilities, and be introduced to key LangGraph concepts along the way. Let’s dive in! 🌟
|
||||
In this tutorial, you will build a basic chatbot. This chatbot is the basis for the following series of tutorials where you will progressively add more sophisticated capabilities, and be introduced to key LangGraph concepts along the way. Let's dive in! 🌟
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -13,13 +13,44 @@ 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`
|
||||
|
||||
@@ -27,6 +58,8 @@ Now you can create a basic chatbot using LangGraph. This chatbot will respond di
|
||||
|
||||
Start by creating a `StateGraph`. A `StateGraph` object defines the structure of our chatbot as a "state machine". We'll add `nodes` to represent the llm and functions our chatbot can call and `edges` to specify how the bot should transition between these functions.
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
|
||||
@@ -46,23 +79,40 @@ class State(TypedDict):
|
||||
graph_builder = StateGraph(State)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import { StateGraph, MessagesZodState, START } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
const State = z.object({ messages: MessagesZodState.shape.messages });
|
||||
|
||||
const graph = new StateGraph(State).compile();
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Our graph can now handle two key tasks:
|
||||
|
||||
1. Each `node` can receive the current `State` as input and output an update to the state.
|
||||
2. Updates to `messages` will be appended to the existing list rather than overwriting it, thanks to the prebuilt [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/?h=add+messages#add_messages) function used with the `Annotated` syntax.
|
||||
|
||||
------
|
||||
2. Updates to `messages` will be appended to the existing list rather than overwriting it, thanks to the prebuilt reducer function.
|
||||
|
||||
!!! tip "Concept"
|
||||
|
||||
When defining a graph, the first step is to define its `State`. The `State` includes the graph's schema and [reducer functions](https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers) that handle state updates. In our example, `State` is a `TypedDict` with one key: `messages`. The [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages) reducer function is used to append new messages to the list instead of overwriting it. Keys without a reducer annotation will overwrite previous values. To learn more about state, reducers, and related concepts, see [LangGraph reference docs](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages).
|
||||
When defining a graph, the first step is to define its `State`. The `State` includes the graph's schema and [reducer functions](https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers) that handle state updates. In our example, `State` is a schema with one key: `messages`. The reducer function is used to append new messages to the list instead of overwriting it. Keys without a reducer annotation will overwrite previous values.
|
||||
|
||||
To learn more about state, reducers, and related concepts, see [LangGraph reference docs](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages).
|
||||
|
||||
## 3. Add a node
|
||||
|
||||
Next, add a "`chatbot`" node. **Nodes** represent units of work and are typically regular Python functions.
|
||||
Next, add a "`chatbot`" node. **Nodes** represent units of work and are typically regular functions.
|
||||
|
||||
Let's first select a chat model:
|
||||
|
||||
:::python
|
||||
|
||||
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
|
||||
|
||||
<!---
|
||||
@@ -73,9 +123,26 @@ llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
|
||||
```
|
||||
-->
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
// or import { ChatAnthropic } from "@langchain/anthropic";
|
||||
|
||||
const llm = new ChatOpenAI({
|
||||
model: "gpt-4o",
|
||||
temperature: 0,
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
We can now incorporate the chat model into a simple node:
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
|
||||
def chatbot(state: State):
|
||||
@@ -88,38 +155,133 @@ def chatbot(state: State):
|
||||
graph_builder.add_node("chatbot", chatbot)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript hl_lines="7-9"
|
||||
import { StateGraph, MessagesZodState, START } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
const State = z.object({ messages: MessagesZodState.shape.messages });
|
||||
|
||||
const graph = new StateGraph(State)
|
||||
.addNode("chatbot", async (state: z.infer<typeof State>) => {
|
||||
return { messages: [await llm.invoke(state.messages)] };
|
||||
})
|
||||
.compile();
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
**Notice** how the `chatbot` node function takes the current `State` as input and returns a dictionary containing an updated `messages` list under the key "messages". This is the basic pattern for all LangGraph node functions.
|
||||
|
||||
:::python
|
||||
The `add_messages` function in our `State` will append the LLM's response messages to whatever messages are already in the state.
|
||||
:::
|
||||
|
||||
:::js
|
||||
The `addMessages` function used within `MessagesZodState` will append the LLM's response messages to whatever messages are already in the state.
|
||||
:::
|
||||
|
||||
## 4. Add an `entry` point
|
||||
|
||||
Add an `entry` point to tell the graph **where to start its work** each time it is run:
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
graph_builder.add_edge(START, "chatbot")
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript hl_lines="10"
|
||||
import { StateGraph, MessagesZodState, START } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
const State = z.object({ messages: MessagesZodState.shape.messages });
|
||||
|
||||
const graph = new StateGraph(State)
|
||||
.addNode("chatbot", async (state: z.infer<typeof State>) => {
|
||||
return { messages: [await llm.invoke(state.messages)] };
|
||||
})
|
||||
.addEdge(START, "chatbot")
|
||||
.compile();
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 5. Add an `exit` point
|
||||
|
||||
Add an `exit` point to indicate **where the graph should finish execution**. This is helpful for more complex flows, but even in a simple graph like this, adding an end node improves clarity.
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
graph_builder.add_edge("chatbot", END)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript hl_lines="11"
|
||||
import { StateGraph, MessagesZodState, START, END } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
const State = z.object({ messages: MessagesZodState.shape.messages });
|
||||
|
||||
const graph = new StateGraph(State)
|
||||
.addNode("chatbot", async (state: z.infer<typeof State>) => {
|
||||
return { messages: [await llm.invoke(state.messages)] };
|
||||
})
|
||||
.addEdge(START, "chatbot")
|
||||
.addEdge("chatbot", END)
|
||||
.compile();
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
This tells the graph to terminate after running the chatbot node.
|
||||
|
||||
## 6. Compile the graph
|
||||
|
||||
Before running the graph, we'll need to compile it. We can do so by calling `compile()`
|
||||
on the graph builder. This creates a `CompiledStateGraph` we can invoke on our state.
|
||||
on the graph builder. This creates a `CompiledGraph` we can invoke on our state.
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
graph = graph_builder.compile()
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript hl_lines="12"
|
||||
import { StateGraph, MessagesZodState, START, END } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
const State = z.object({ messages: MessagesZodState.shape.messages });
|
||||
|
||||
const graph = new StateGraph(State)
|
||||
.addNode("chatbot", async (state: z.infer<typeof State>) => {
|
||||
return { messages: [await llm.invoke(state.messages)] };
|
||||
})
|
||||
.addEdge(START, "chatbot")
|
||||
.addEdge("chatbot", END)
|
||||
.compile();
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 7. Visualize the graph (optional)
|
||||
|
||||
:::python
|
||||
You can visualize the graph using the `get_graph` method and one of the "draw" methods, like `draw_ascii` or `draw_png`. The `draw` methods each require additional dependencies.
|
||||
|
||||
```python
|
||||
@@ -132,17 +294,35 @@ 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);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||

|
||||
|
||||
## 8. Run the chatbot
|
||||
|
||||
Now run the chatbot!
|
||||
Now run the chatbot!
|
||||
|
||||
!!! tip
|
||||
|
||||
You can exit the chat loop at any time by typing `quit`, `exit`, or `q`.
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
def stream_graph_updates(user_input: str):
|
||||
for event in graph.stream({"messages": [{"role": "user", "content": user_input}]}):
|
||||
@@ -165,13 +345,86 @@ while True:
|
||||
break
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import { HumanMessage } from "@langchain/core/messages";
|
||||
|
||||
async function streamGraphUpdates(userInput: string) {
|
||||
const stream = await graph.stream({
|
||||
messages: [new HumanMessage(userInput)],
|
||||
});
|
||||
|
||||
import * as readline from "node:readline/promises";
|
||||
import { StateGraph, MessagesZodState, START, END } from "@langchain/langgraph";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { z } from "zod";
|
||||
|
||||
const llm = new ChatOpenAI({ model: "gpt-4o-mini" });
|
||||
|
||||
const State = z.object({ messages: MessagesZodState.shape.messages });
|
||||
|
||||
const graph = new StateGraph(State)
|
||||
.addNode("chatbot", async (state: z.infer<typeof State>) => {
|
||||
return { messages: [await llm.invoke(state.messages)] };
|
||||
})
|
||||
.addEdge(START, "chatbot")
|
||||
.addEdge("chatbot", END)
|
||||
.compile();
|
||||
|
||||
async function generateText(content: string) {
|
||||
const stream = await graph.stream(
|
||||
{ messages: [{ type: "human", content }] },
|
||||
{ streamMode: "values" }
|
||||
);
|
||||
|
||||
for await (const event of stream) {
|
||||
for (const value of Object.values(event)) {
|
||||
console.log(
|
||||
"Assistant:",
|
||||
value.messages[value.messages.length - 1].content
|
||||
);
|
||||
const lastMessage = event.messages.at(-1);
|
||||
if (lastMessage?.getType() === "ai") {
|
||||
console.log(`Assistant: ${lastMessage.text}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const prompt = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
while (true) {
|
||||
const human = await prompt.question("User: ");
|
||||
if (["quit", "exit", "q"].includes(human.trim())) break;
|
||||
await generateText(human || "What do you know about LangGraph?");
|
||||
}
|
||||
|
||||
prompt.close();
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
```
|
||||
Assistant: LangGraph is a library designed to help build stateful multi-agent applications using language models. It provides tools for creating workflows and state machines to coordinate multiple AI agents or language model interactions. LangGraph is built on top of LangChain, leveraging its components while adding graph-based coordination capabilities. It's particularly useful for developing more complex, stateful AI applications that go beyond simple query-response interactions.
|
||||
```
|
||||
|
||||
:::python
|
||||
|
||||
```
|
||||
Goodbye!
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
**Congratulations!** You've built your first chatbot using LangGraph. This bot can engage in basic conversation by taking user input and generating responses using an LLM. You can inspect a [LangSmith Trace](https://smith.langchain.com/public/7527e308-9502-4894-b347-f34385740d5a/r) for the call above.
|
||||
|
||||
:::python
|
||||
|
||||
Below is the full code for this tutorial:
|
||||
|
||||
```python
|
||||
@@ -207,8 +460,36 @@ graph_builder.add_edge("chatbot", END)
|
||||
graph = graph_builder.compile()
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import { StateGraph, START, END, MessagesZodState } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
|
||||
const llm = new ChatOpenAI({
|
||||
model: "gpt-4o",
|
||||
temperature: 0,
|
||||
});
|
||||
|
||||
const State = z.object({ messages: MessagesZodState.shape.messages });
|
||||
|
||||
const graph = new StateGraph(State);
|
||||
// The first argument is the unique node name
|
||||
// The second argument is the function or object that will be called whenever
|
||||
// the node is used.
|
||||
.addNode("chatbot", async (state) => {
|
||||
return { messages: [await llm.invoke(state.messages)] };
|
||||
});
|
||||
.addEdge(START, "chatbot");
|
||||
.addEdge("chatbot", END)
|
||||
.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.
|
||||
|
||||
|
||||
|
||||
@@ -10,35 +10,84 @@ 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
|
||||
```python
|
||||
def _set_env(var: str):
|
||||
if not os.environ.get(var):
|
||||
os.environ[var] = getpass.getpass(f"{var}: ")
|
||||
import os
|
||||
|
||||
_set_env("TAVILY_API_KEY")
|
||||
os.environ["TAVILY_API_KEY"] = "tvly-..."
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
process.env.TAVILY_API_KEY = "tvly-...";
|
||||
```
|
||||
|
||||
```
|
||||
os.environ["TAVILY_API_KEY"]: "········"
|
||||
```
|
||||
:::
|
||||
|
||||
## 3. Define the tool
|
||||
|
||||
Define the web search tool:
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
from langchain_tavily import TavilySearch
|
||||
|
||||
@@ -47,8 +96,25 @@ tools = [tool]
|
||||
tool.invoke("What's a 'node' in LangGraph?")
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import { TavilySearch } from "@langchain/tavily";
|
||||
|
||||
const tool = new TavilySearch({ maxResults: 2 });
|
||||
const tools = [tool];
|
||||
|
||||
await tool.invoke({ query: "What's a 'node' in LangGraph?" });
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
The results are page summaries our chat bot can use to answer questions:
|
||||
|
||||
:::python
|
||||
|
||||
```
|
||||
{'query': "What's a 'node' in LangGraph?",
|
||||
'follow_up_questions': None,
|
||||
@@ -67,12 +133,51 @@ The results are page summaries our chat bot can use to answer questions:
|
||||
'response_time': 1.38}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "What's a 'node' in LangGraph?",
|
||||
"follow_up_questions": null,
|
||||
"answer": null,
|
||||
"images": [],
|
||||
"results": [
|
||||
{
|
||||
"url": "https://blog.langchain.dev/langgraph/",
|
||||
"title": "LangGraph - LangChain Blog",
|
||||
"content": "TL;DR: LangGraph is module built on top of LangChain to better enable creation of cyclical graphs, often needed for agent runtimes. This state is updated by nodes in the graph, which return operations to attributes of this state (in the form of a key-value store). After adding nodes, you can then add edges to create the graph. An example of this may be in the basic agent runtime, where we always want the model to be called after we call a tool. The state of this graph by default contains concepts that should be familiar to you if you've used LangChain agents: `input`, `chat_history`, `intermediate_steps` (and `agent_outcome` to represent the most recent agent outcome)",
|
||||
"score": 0.7407191,
|
||||
"raw_content": null
|
||||
},
|
||||
{
|
||||
"url": "https://medium.com/@cplog/introduction-to-langgraph-a-beginners-guide-14f9be027141",
|
||||
"title": "Introduction to LangGraph: A Beginner's Guide - Medium",
|
||||
"content": "* **Stateful Graph:** LangGraph revolves around the concept of a stateful graph, where each node in the graph represents a step in your computation, and the graph maintains a state that is passed around and updated as the computation progresses. LangGraph supports conditional edges, allowing you to dynamically determine the next node to execute based on the current state of the graph. Image 10: Introduction to AI Agent with LangChain and LangGraph: A Beginner’s Guide Image 18: How to build LLM Agent with LangGraph — StateGraph and Reducer Image 20: Simplest Graphs using LangGraph Framework Image 24: Building a ReAct Agent with Langgraph: A Step-by-Step Guide Image 28: Building an Agentic RAG with LangGraph: A Step-by-Step Guide",
|
||||
"score": 0.65279555,
|
||||
"raw_content": null
|
||||
}
|
||||
],
|
||||
"response_time": 1.34
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 4. Define the graph
|
||||
|
||||
:::python
|
||||
For the `StateGraph` you created in the [first tutorial](./1-build-basic-chatbot.md), add `bind_tools` on the LLM. This lets the LLM know the correct JSON format to use if it wants to use the search engine.
|
||||
:::
|
||||
|
||||
:::js
|
||||
For the `StateGraph` you created in the [first tutorial](./1-build-basic-chatbot.md), add `bindTools` on the LLM. This lets the LLM know the correct JSON format to use if it wants to use the search engine.
|
||||
:::
|
||||
|
||||
Let's first select our LLM:
|
||||
|
||||
:::python
|
||||
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
|
||||
|
||||
<!---
|
||||
@@ -83,9 +188,23 @@ 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 hl_lines="15"
|
||||
:::python
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
@@ -108,9 +227,31 @@ def chatbot(state: State):
|
||||
graph_builder.add_node("chatbot", chatbot)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript hl_lines="7-8"
|
||||
import { StateGraph, MessagesZodState } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
const State = z.object({ messages: MessagesZodState.shape.messages });
|
||||
|
||||
const chatbot = async (state: z.infer<typeof State>) => {
|
||||
// Modification: tell the LLM which tools it can call
|
||||
const llmWithTools = llm.bindTools(tools);
|
||||
|
||||
return { messages: [await llmWithTools.invoke(state.messages)] };
|
||||
};
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 5. Create a function to run the tools
|
||||
|
||||
Now, create a function to run the tools if they are called. Do this by adding the tools to a new node called`BasicToolNode` that checks the most recent message in the state and calls tools if the message contains `tool_calls`. It relies on the LLM's `tool_calling` support, which is available in Anthropic, OpenAI, Google Gemini, and a number of other LLM providers.
|
||||
:::python
|
||||
|
||||
Now, create a function to run the tools if they are called. Do this by adding the tools to a new node called `BasicToolNode` that checks the most recent message in the state and calls tools if the message contains `tool_calls`. It relies on the LLM's `tool_calling` support, which is available in Anthropic, OpenAI, Google Gemini, and a number of other LLM providers.
|
||||
|
||||
```python
|
||||
import json
|
||||
@@ -152,16 +293,80 @@ graph_builder.add_node("tools", tool_node)
|
||||
|
||||
If you do not want to build this yourself in the future, you can use LangGraph's prebuilt [ToolNode](https://langchain-ai.github.io/langgraph/reference/agents/#langgraph.prebuilt.tool_node.ToolNode).
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
Now, create a function to run the tools if they are called. Do this by adding the tools to a new node called `"tools"` that checks the most recent message in the state and calls tools if the message contains `tool_calls`. It relies on the LLM's tool calling support, which is available in Anthropic, OpenAI, Google Gemini, and a number of other LLM providers.
|
||||
|
||||
```typescript
|
||||
import type { StructuredToolInterface } from "@langchain/core/tools";
|
||||
import { isAIMessage, ToolMessage } from "@langchain/core/messages";
|
||||
|
||||
function createToolNode(tools: StructuredToolInterface[]) {
|
||||
const toolByName: Record<string, StructuredToolInterface> = {};
|
||||
for (const tool of tools) {
|
||||
toolByName[tool.name] = tool;
|
||||
}
|
||||
|
||||
return async (inputs: z.infer<typeof State>) => {
|
||||
const { messages } = inputs;
|
||||
if (!messages || messages.length === 0) {
|
||||
throw new Error("No message found in input");
|
||||
}
|
||||
|
||||
const message = messages.at(-1);
|
||||
if (!message || !isAIMessage(message) || !message.tool_calls) {
|
||||
throw new Error("Last message is not an AI message with tool calls");
|
||||
}
|
||||
|
||||
const outputs: ToolMessage[] = [];
|
||||
for (const toolCall of message.tool_calls) {
|
||||
if (!toolCall.id) throw new Error("Tool call ID is required");
|
||||
|
||||
const tool = toolByName[toolCall.name];
|
||||
if (!tool) throw new Error(`Tool ${toolCall.name} not found`);
|
||||
|
||||
const result = await tool.invoke(toolCall.args);
|
||||
|
||||
outputs.push(
|
||||
new ToolMessage({
|
||||
content: JSON.stringify(result),
|
||||
name: toolCall.name,
|
||||
tool_call_id: toolCall.id,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return { messages: outputs };
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
If you do not want to build this yourself in the future, you can use LangGraph's prebuilt [ToolNode](https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph_prebuilt.ToolNode.html).
|
||||
|
||||
:::
|
||||
|
||||
## 6. Define the `conditional_edges`
|
||||
|
||||
With the tool node added, now you can define the `conditional_edges`.
|
||||
With the tool node added, now you can define the `conditional_edges`.
|
||||
|
||||
**Edges** route the control flow from one node to the next. **Conditional edges** start from a single node and usually contain "if" statements to route to different nodes depending on the current graph state. These functions receive the current graph `state` and return a string or list of strings indicating which node(s) to call next.
|
||||
|
||||
Next, define a router function called `route_tools` that checks for `tool_calls` in the chatbot's output. Provide this function to the graph by calling `add_conditional_edges`, which tells the graph that whenever the `chatbot` node completes to check this function to see where to go next.
|
||||
:::python
|
||||
Next, define a router function called `route_tools` that checks for `tool_calls` in the chatbot's output. Provide this function to the graph by calling `add_conditional_edges`, which tells the graph that whenever the `chatbot` node completes to check this function to see where to go next.
|
||||
:::
|
||||
|
||||
:::js
|
||||
Next, define a router function called `routeTools` that checks for `tool_calls` in the chatbot's output. Provide this function to the graph by calling `addConditionalEdges`, which tells the graph that whenever the `chatbot` node completes to check this function to see where to go next.
|
||||
:::
|
||||
|
||||
The condition will route to `tools` if tool calls are present and `END` if not. Because the condition can return `END`, you do not need to explicitly set a `finish_point` this time.
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
def route_tools(
|
||||
state: State,
|
||||
@@ -201,10 +406,61 @@ graph = graph_builder.compile()
|
||||
|
||||
!!! note
|
||||
|
||||
You can replace this with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition) to be more concise.
|
||||
You can replace this with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition) to be more concise.
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import { END, START } from "@langchain/langgraph";
|
||||
|
||||
const routeTools = (state: z.infer<typeof State>) => {
|
||||
/**
|
||||
* Use as conditional edge to route to the ToolNode if the last message
|
||||
* has tool calls.
|
||||
*/
|
||||
const lastMessage = state.messages.at(-1);
|
||||
if (
|
||||
lastMessage &&
|
||||
isAIMessage(lastMessage) &&
|
||||
lastMessage.tool_calls?.length
|
||||
) {
|
||||
return "tools";
|
||||
}
|
||||
|
||||
/** Otherwise, route to the end. */
|
||||
return END;
|
||||
};
|
||||
|
||||
const graph = new StateGraph(State)
|
||||
.addNode("chatbot", chatbot)
|
||||
|
||||
// The `routeTools` function returns "tools" if the chatbot asks to use a tool, and "END" if
|
||||
// it is fine directly responding. This conditional routing defines the main agent loop.
|
||||
.addNode("tools", createToolNode(tools))
|
||||
|
||||
// Start the graph with the chatbot
|
||||
.addEdge(START, "chatbot")
|
||||
|
||||
// The `routeTools` function returns "tools" if the chatbot asks to use a tool, and "END" if
|
||||
// it is fine directly responding.
|
||||
.addConditionalEdges("chatbot", routeTools, ["tools", END])
|
||||
|
||||
// Any time a tool is called, we need to return to the chatbot
|
||||
.addEdge("tools", "chatbot")
|
||||
.compile();
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
You can replace this with the prebuilt [toolsCondition](https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph_prebuilt.toolsCondition.html) to be more concise.
|
||||
|
||||
:::
|
||||
|
||||
## 7. Visualize the graph (optional)
|
||||
|
||||
:::python
|
||||
You can visualize the graph using the `get_graph` method and one of the "draw" methods, like `draw_ascii` or `draw_png`. The `draw` methods each require additional dependencies.
|
||||
|
||||
```python
|
||||
@@ -217,12 +473,31 @@ except Exception:
|
||||
pass
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
You can visualize the graph using the `getGraph` method and render the graph with the `drawMermaidPng` method.
|
||||
|
||||
```typescript
|
||||
import * as fs from "node:fs/promises";
|
||||
|
||||
const drawableGraph = await graph.getGraphAsync();
|
||||
const image = await drawableGraph.drawMermaidPng();
|
||||
const imageBuffer = new Uint8Array(await image.arrayBuffer());
|
||||
|
||||
await fs.writeFile("chatbot-with-tools.png", imageBuffer);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||

|
||||
|
||||
## 8. Ask the bot questions
|
||||
|
||||
Now you can ask the chatbot questions outside its training data:
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
def stream_graph_updates(user_input: str):
|
||||
for event in graph.stream({"messages": [{"role": "user", "content": user_input}]}):
|
||||
@@ -245,7 +520,7 @@ while True:
|
||||
break
|
||||
```
|
||||
|
||||
```
|
||||
```
|
||||
Assistant: [{'text': "To provide you with accurate and up-to-date information about LangGraph, I'll need to search for the latest details. Let me do that for you.", 'type': 'text'}, {'id': 'toolu_01Q588CszHaSvvP2MxRq9zRD', 'input': {'query': 'LangGraph AI tool information'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]
|
||||
Assistant: [{"url": "https://www.langchain.com/langgraph", "content": "LangGraph sets the foundation for how we can build and scale AI workloads \u2014 from conversational agents, complex task automation, to custom LLM-backed experiences that 'just work'. The next chapter in building complex production-ready features with LLMs is agentic, and with LangGraph and LangSmith, LangChain delivers an out-of-the-box solution ..."}, {"url": "https://github.com/langchain-ai/langgraph", "content": "Overview. LangGraph is a library for building stateful, multi-actor applications with LLMs, used to create agent and multi-agent workflows. Compared to other LLM frameworks, it offers these core benefits: cycles, controllability, and persistence. LangGraph allows you to define flows that involve cycles, essential for most agentic architectures ..."}]
|
||||
Assistant: Based on the search results, I can provide you with information about LangGraph:
|
||||
@@ -276,18 +551,107 @@ Assistant: Based on the search results, I can provide you with information about
|
||||
|
||||
LangGraph appears to be a significant tool in the evolving landscape of LLM-based application development, offering developers new ways to create more complex, stateful, and interactive AI systems.
|
||||
Goodbye!
|
||||
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import readline from "node:readline/promises";
|
||||
|
||||
const prompt = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
async function generateText(content: string) {
|
||||
const stream = await graph.stream(
|
||||
{ messages: [{ type: "human", content }] },
|
||||
{ streamMode: "values" }
|
||||
);
|
||||
|
||||
for await (const event of stream) {
|
||||
const lastMessage = event.messages.at(-1);
|
||||
|
||||
if (lastMessage?.getType() === "ai" || lastMessage?.getType() === "tool") {
|
||||
console.log(`Assistant: ${lastMessage?.text}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const human = await prompt.question("User: ");
|
||||
if (["quit", "exit", "q"].includes(human.trim())) break;
|
||||
await generateText(human || "What do you know about LangGraph?");
|
||||
}
|
||||
|
||||
prompt.close();
|
||||
```
|
||||
|
||||
```
|
||||
User: What do you know about LangGraph?
|
||||
Assistant: I'll search for the latest information about LangGraph for you.
|
||||
Assistant: [{"title":"Introduction to LangGraph: A Beginner's Guide - Medium","url":"https://medium.com/@cplog/introduction-to-langgraph-a-beginners-guide-14f9be027141","content":"..."}]
|
||||
Assistant: Based on the search results, I can provide you with information about LangGraph:
|
||||
|
||||
LangGraph is a library within the LangChain ecosystem designed for building stateful, multi-actor applications with Large Language Models (LLMs). Here are the key aspects:
|
||||
|
||||
**Core Purpose:**
|
||||
- LangGraph is specifically designed for creating agent and multi-agent workflows
|
||||
- It provides a framework for defining, coordinating, and executing multiple LLM agents in a structured manner
|
||||
|
||||
**Key Features:**
|
||||
1. **Stateful Graph Architecture**: LangGraph revolves around a stateful graph where each node represents a step in computation, and the graph maintains state that is passed around and updated as the computation progresses
|
||||
|
||||
2. **Conditional Edges**: It supports conditional edges, allowing you to dynamically determine the next node to execute based on the current state of the graph
|
||||
|
||||
3. **Cycles**: Unlike other LLM frameworks, LangGraph allows you to define flows that involve cycles, which is essential for most agentic architectures
|
||||
|
||||
4. **Controllability**: It offers enhanced control over the application flow
|
||||
|
||||
5. **Persistence**: The library provides ways to maintain state and persistence in LLM-based applications
|
||||
|
||||
**Use Cases:**
|
||||
- Conversational agents
|
||||
- Complex task automation
|
||||
- Custom LLM-backed experiences
|
||||
- Multi-agent systems that perform complex tasks
|
||||
|
||||
**Benefits:**
|
||||
LangGraph allows developers to focus on the high-level logic of their applications rather than the intricacies of agent coordination, making it easier to build complex, production-ready features with LLMs.
|
||||
|
||||
This makes LangGraph a significant tool in the evolving landscape of LLM-based application development.
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 9. Use prebuilts
|
||||
|
||||
For ease of use, adjust your code to replace the following with LangGraph prebuilt components. These have built in functionality like parallel API execution.
|
||||
|
||||
:::python
|
||||
|
||||
- `BasicToolNode` is replaced with the prebuilt [ToolNode](https://langchain-ai.github.io/langgraph/reference/prebuilt/#toolnode)
|
||||
- `route_tools` is replaced with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition)
|
||||
|
||||
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
|
||||
|
||||
<!---
|
||||
```python
|
||||
from langchain.chat_models import init_chat_model
|
||||
|
||||
llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
|
||||
```
|
||||
-->
|
||||
|
||||
<!---
|
||||
```python
|
||||
from langchain.chat_models import init_chat_model
|
||||
|
||||
llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
|
||||
```
|
||||
-->
|
||||
|
||||
```python hl_lines="25 30"
|
||||
from typing import Annotated
|
||||
@@ -327,7 +691,46 @@ graph_builder.add_edge(START, "chatbot")
|
||||
graph = graph_builder.compile()
|
||||
```
|
||||
|
||||
**Congratulations!** You've created a conversational agent in LangGraph that can use a search engine to retrieve updated information when needed. Now it can handle a wider range of user queries. To inspect all the steps your agent just took, check out this [LangSmith trace](https://smith.langchain.com/public/4fbd7636-25af-4638-9587-5a02fdbb0172/r).
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
- `createToolNode` is replaced with the prebuilt [ToolNode](https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph_prebuilt.ToolNode.html)
|
||||
- `routeTools` is replaced with the prebuilt [toolsCondition](https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph_prebuilt.toolsCondition.html)
|
||||
|
||||
```typescript
|
||||
import { TavilySearch } from "@langchain/tavily";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { StateGraph, START, MessagesZodState, END } from "@langchain/langgraph";
|
||||
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
|
||||
import { z } from "zod";
|
||||
|
||||
const State = z.object({ messages: MessagesZodState.shape.messages });
|
||||
|
||||
const tools = [new TavilySearch({ maxResults: 2 })];
|
||||
|
||||
const llm = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools(tools);
|
||||
|
||||
const graph = new StateGraph(State)
|
||||
.addNode("chatbot", async (state) => ({
|
||||
messages: [await llm.invoke(state.messages)],
|
||||
}))
|
||||
.addNode("tools", new ToolNode(tools))
|
||||
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
|
||||
.addEdge("tools", "chatbot")
|
||||
.addEdge(START, "chatbot")
|
||||
.compile();
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
**Congratulations!** You've created a conversational agent in LangGraph that can use a search engine to retrieve updated information when needed. Now it can handle a wider range of user queries.
|
||||
|
||||
:::python
|
||||
|
||||
To inspect all the steps your agent just took, check out this [LangSmith trace](https://smith.langchain.com/public/4fbd7636-25af-4638-9587-5a02fdbb0172/r).
|
||||
|
||||
:::
|
||||
|
||||
## Next steps
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -10,47 +10,83 @@ We will see later that **checkpointing** is _much_ more powerful than simple cha
|
||||
|
||||
This tutorial builds on [Add tools](./2-add-tools.md).
|
||||
|
||||
## 1. Create a `InMemorySaver` checkpointer
|
||||
## 1. Create a `MemorySaver` checkpointer
|
||||
|
||||
Create a `InMemorySaver` checkpointer:
|
||||
Create a `MemorySaver` checkpointer:
|
||||
|
||||
``` python
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
:::python
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
memory = InMemorySaver()
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import { MemorySaver } from "@langchain/langgraph";
|
||||
|
||||
const memory = new MemorySaver();
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
This is in-memory checkpointer, which is convenient for the tutorial. However, in a production application, you would likely change this to use `SqliteSaver` or `PostgresSaver` and connect a database.
|
||||
|
||||
## 2. Compile the graph
|
||||
|
||||
Compile the graph with the provided checkpointer, which will checkpoint the `State` as the graph works through each node:
|
||||
|
||||
``` python
|
||||
:::python
|
||||
|
||||
```python
|
||||
graph = graph_builder.compile(checkpointer=memory)
|
||||
```
|
||||
|
||||
``` python
|
||||
from IPython.display import Image, display
|
||||
:::
|
||||
|
||||
try:
|
||||
display(Image(graph.get_graph().draw_mermaid_png()))
|
||||
except Exception:
|
||||
# This requires some extra dependencies and is optional
|
||||
pass
|
||||
:::js
|
||||
|
||||
```typescript hl_lines="7"
|
||||
const graph = new StateGraph(State)
|
||||
.addNode("chatbot", chatbot)
|
||||
.addNode("tools", new ToolNode(tools))
|
||||
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
|
||||
.addEdge("tools", "chatbot")
|
||||
.addEdge(START, "chatbot")
|
||||
.compile({ checkpointer: memory });
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 3. Interact with your chatbot
|
||||
|
||||
Now you can interact with your bot!
|
||||
|
||||
1. Pick a thread to use as the key for this conversation.
|
||||
1. Pick a thread to use as the key for this conversation.
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
```
|
||||
|
||||
2. Call your chatbot:
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
const config = { configurable: { thread_id: "1" } };
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
2. Call your chatbot:
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
user_input = "Hi there! My name is Will."
|
||||
@@ -74,14 +110,46 @@ 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
|
||||
!!! note
|
||||
|
||||
The config was provided as the **second parameter** when calling our graph. It importantly is _not_ nested within the graph inputs (`{"messages": []}`).
|
||||
|
||||
:::
|
||||
|
||||
## 4. Ask a follow up question
|
||||
|
||||
Ask a follow up question:
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
user_input = "Remember my name?"
|
||||
|
||||
@@ -104,10 +172,37 @@ Remember my name?
|
||||
Of course, I remember your name, Will. I always try to pay attention to important details that users share with me. Is there anything else you'd like to talk about or any questions you have? I'm here to help with a wide range of topics or tasks.
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
const userInput2 = "Remember my name?";
|
||||
|
||||
const events2 = await graph.stream(
|
||||
{ messages: [{ type: "human", content: userInput2 }] },
|
||||
{ configurable: { thread_id: "1" }, streamMode: "values" }
|
||||
);
|
||||
|
||||
for await (const event of events2) {
|
||||
const lastMessage = event.messages.at(-1);
|
||||
console.log(`${lastMessage?.getType()}: ${lastMessage?.text}`);
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
human: Remember my name?
|
||||
ai: Yes, your name is Will. How can I help you today?
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
**Notice** that we aren't using an external list for memory: it's all handled by the checkpointer! You can inspect the full execution in this [LangSmith trace](https://smith.langchain.com/public/29ba22b5-6d40-4fbe-8d27-b369e3329c84/r) to see what's going on.
|
||||
|
||||
Don't believe me? Try this using a different config.
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
# The only difference is we change the `thread_id` here to "2" instead of "1"
|
||||
events = graph.stream(
|
||||
@@ -129,10 +224,36 @@ Remember my name?
|
||||
I apologize, but I don't have any previous context or memory of your name. As an AI assistant, I don't retain information from past conversations. Each interaction starts fresh. Could you please tell me your name so I can address you properly in this conversation?
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript hl_lines="3-4"
|
||||
const events3 = await graph.stream(
|
||||
{ messages: [{ type: "human", content: userInput2 }] },
|
||||
// The only difference is we change the `thread_id` here to "2" instead of "1"
|
||||
{ configurable: { thread_id: "2" }, streamMode: "values" }
|
||||
);
|
||||
|
||||
for await (const event of events3) {
|
||||
const lastMessage = event.messages.at(-1);
|
||||
console.log(`${lastMessage?.getType()}: ${lastMessage?.text}`);
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
human: Remember my name?
|
||||
ai: I don't have the ability to remember personal information about users between interactions. However, I'm here to help you with any questions or topics you want to discuss!
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
**Notice** that the **only** change we've made is to modify the `thread_id` in the config. See this call's [LangSmith trace](https://smith.langchain.com/public/51a62351-2f0a-4058-91cc-9996c5561428/r) for comparison.
|
||||
|
||||
## 5. Inspect the state
|
||||
|
||||
:::python
|
||||
|
||||
By now, we have made a few checkpoints across two different threads. But what goes into a checkpoint? To inspect a graph's `state` for a given config at any time, call `get_state(config)`.
|
||||
|
||||
```python
|
||||
@@ -148,12 +269,94 @@ StateSnapshot(values={'messages': [HumanMessage(content='Hi there! My name is Wi
|
||||
snapshot.next # (since the graph ended this turn, `next` is empty. If you fetch a state from within a graph invocation, next tells which node will execute next)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
By now, we have made a few checkpoints across two different threads. But what goes into a checkpoint? To inspect a graph's `state` for a given config at any time, call `getState(config)`.
|
||||
|
||||
```typescript
|
||||
await graph.getState({ configurable: { thread_id: "1" } });
|
||||
```
|
||||
|
||||
```typescript
|
||||
{
|
||||
values: {
|
||||
messages: [
|
||||
HumanMessage {
|
||||
"id": "32fabcef-b3b8-481f-8bcb-fd83399a5f8d",
|
||||
"content": "Hi there! My name is Will.",
|
||||
"additional_kwargs": {},
|
||||
"response_metadata": {}
|
||||
},
|
||||
AIMessage {
|
||||
"id": "chatcmpl-BrPbTsCJbVqBvXWySlYoTJvM75Kv8",
|
||||
"content": "Hello Will! How can I assist you today?",
|
||||
"additional_kwargs": {},
|
||||
"response_metadata": {},
|
||||
"tool_calls": [],
|
||||
"invalid_tool_calls": []
|
||||
},
|
||||
HumanMessage {
|
||||
"id": "561c3aad-f8fc-4fac-94a6-54269a220856",
|
||||
"content": "Remember my name?",
|
||||
"additional_kwargs": {},
|
||||
"response_metadata": {}
|
||||
},
|
||||
AIMessage {
|
||||
"id": "chatcmpl-BrPbU4BhhsUikGbW37hYuF5vvnnE2",
|
||||
"content": "Yes, I remember your name, Will! How can I help you today?",
|
||||
"additional_kwargs": {},
|
||||
"response_metadata": {},
|
||||
"tool_calls": [],
|
||||
"invalid_tool_calls": []
|
||||
}
|
||||
]
|
||||
},
|
||||
next: [],
|
||||
tasks: [],
|
||||
metadata: {
|
||||
source: 'loop',
|
||||
step: 4,
|
||||
parents: {},
|
||||
thread_id: '1'
|
||||
},
|
||||
config: {
|
||||
configurable: {
|
||||
thread_id: '1',
|
||||
checkpoint_id: '1f05cccc-9bb6-6270-8004-1d2108bcec77',
|
||||
checkpoint_ns: ''
|
||||
}
|
||||
},
|
||||
createdAt: '2025-07-09T13:58:27.607Z',
|
||||
parentConfig: {
|
||||
configurable: {
|
||||
thread_id: '1',
|
||||
checkpoint_ns: '',
|
||||
checkpoint_id: '1f05cccc-78fa-68d0-8003-ffb01a76b599'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
import * as assert from "node:assert";
|
||||
|
||||
// Since the graph ended this turn, `next` is empty.
|
||||
// If you fetch a state from within a graph invocation, next tells which node will execute next)
|
||||
assert.deepEqual(snapshot.next, []);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
The snapshot above contains the current state values, corresponding config, and the `next` node to process. In our case, the graph has reached an `END` state, so `next` is empty.
|
||||
|
||||
**Congratulations!** Your chatbot can now maintain conversation state across sessions thanks to LangGraph's checkpointing system. This opens up exciting possibilities for more natural, contextual interactions. LangGraph's checkpointing even handles **arbitrarily complex graph states**, which is much more expressive and powerful than simple chat memory.
|
||||
|
||||
|
||||
Check out the code snippet below to review the graph from this tutorial:
|
||||
|
||||
:::python
|
||||
|
||||
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
|
||||
|
||||
<!---
|
||||
@@ -204,6 +407,43 @@ memory = InMemorySaver()
|
||||
graph = graph_builder.compile(checkpointer=memory)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript hl_lines="16 26"
|
||||
import { END, MessagesZodState, START } from "@langchain/langgraph";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { TavilySearch } from "@langchain/tavily";
|
||||
|
||||
import { MemorySaver } from "@langchain/langgraph";
|
||||
import { StateGraph } from "@langchain/langgraph";
|
||||
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
|
||||
import { z } from "zod";
|
||||
|
||||
const State = z.object({
|
||||
messages: MessagesZodState.shape.messages,
|
||||
});
|
||||
|
||||
const tools = [new TavilySearch({ maxResults: 2 })];
|
||||
const llm = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools(tools);
|
||||
const memory = new MemorySaver();
|
||||
|
||||
async function generateText(content: string) {
|
||||
|
||||
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({ checkpointer: memory });
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Next steps
|
||||
|
||||
In the next tutorial, you will [add human-in-the-loop to the chatbot](./4-human-in-the-loop.md) to handle situations where it may need guidance or verification before proceeding.
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
|
||||
Agents can be unreliable and may need human input to successfully accomplish tasks. Similarly, for some actions, you may want to require human approval before running to ensure that everything is running as intended.
|
||||
|
||||
LangGraph's [persistence](../../concepts/persistence.md) layer supports **human-in-the-loop** workflows, allowing execution to pause and resume based on user feedback. The primary interface to this functionality is the [`interrupt`](../../how-tos/human_in_the_loop/add-human-in-the-loop.md) function. Calling `interrupt` inside a node will pause execution. Execution can be resumed, together with new input from a human, by passing in a [Command](../../concepts/low_level.md#command). `interrupt` is ergonomically similar to Python's built-in `input()`, [with some caveats](../../how-tos/human_in_the_loop/add-human-in-the-loop.md).
|
||||
LangGraph's [persistence](../../concepts/persistence.md) layer supports **human-in-the-loop** workflows, allowing execution to pause and resume based on user feedback. The primary interface to this functionality is the [`interrupt`](../../how-tos/human_in_the_loop/add-human-in-the-loop.md) function. Calling `interrupt` inside a node will pause execution. Execution can be resumed, together with new input from a human, by passing in a [Command](../../concepts/low_level.md#command).
|
||||
|
||||
:::python
|
||||
`interrupt` is ergonomically similar to Python's built-in `input()`, [with some caveats](../../how-tos/human_in_the_loop/add-human-in-the-loop.md).
|
||||
:::
|
||||
|
||||
:::js
|
||||
`interrupt` is ergonomically similar to Node.js's built-in `readline.question()` function, [with some caveats](../../how-tos/human_in_the_loop/add-human-in-the-loop.md).
|
||||
`interrupt` is ergonomically similar to Node.js's built-in `readline.question()` function, [with some caveats](../../how-tos/human_in_the_loop/add-human-in-the-loop.md).
|
||||
:::
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -14,6 +23,7 @@ Starting with the existing code from the [Add memory to the chatbot](./3-add-mem
|
||||
|
||||
Let's first select a chat model:
|
||||
|
||||
:::python
|
||||
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
|
||||
|
||||
<!---
|
||||
@@ -24,9 +34,22 @@ llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
|
||||
```
|
||||
-->
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
// Add your API key here
|
||||
process.env.ANTHROPIC_API_KEY = "YOUR_API_KEY";
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
We can now incorporate it into our `StateGraph` with an additional tool:
|
||||
|
||||
``` python hl_lines="12 19 20 21 22 23"
|
||||
:::python
|
||||
|
||||
```python hl_lines="12 19 20 21 22 23"
|
||||
from typing import Annotated
|
||||
|
||||
from langchain_tavily import TavilySearch
|
||||
@@ -76,6 +99,60 @@ 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 searchTool = new TavilySearch({ maxResults: 2 });
|
||||
const tools = [searchTool, humanAssistance];
|
||||
|
||||
const llmWithTools = new ChatAnthropic({
|
||||
model: "claude-3-5-sonnet-latest",
|
||||
}).bindTools(tools);
|
||||
const llmWithTools = new ChatAnthropic({
|
||||
model: "claude-3-5-sonnet-latest",
|
||||
}).bindTools(tools);
|
||||
|
||||
async function chatbot(state: z.infer<typeof MessagesZodState>) {
|
||||
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).
|
||||
@@ -84,17 +161,48 @@ graph_builder.add_edge(START, "chatbot")
|
||||
|
||||
We compile the graph with a checkpointer, as before:
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
memory = InMemorySaver()
|
||||
|
||||
graph = graph_builder.compile(checkpointer=memory)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript 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 });
|
||||
const graph = new StateGraph(MessagesZodState)
|
||||
.addNode("chatbot", chatbot)
|
||||
.addNode("tools", new ToolNode(tools))
|
||||
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
|
||||
.addEdge("tools", "chatbot")
|
||||
.addEdge(START, "chatbot")
|
||||
.compile({ checkpointer: memory });
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 3. Visualize the graph (optional)
|
||||
|
||||
Visualizing the graph, you get the same layout as before – just with the added tool!
|
||||
|
||||
``` python
|
||||
:::python
|
||||
|
||||
```python
|
||||
from IPython.display import Image, display
|
||||
|
||||
try:
|
||||
@@ -104,12 +212,34 @@ except Exception:
|
||||
pass
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as fs from "node:fs/promises";
|
||||
|
||||
const drawableGraph = await graph.getGraphAsync();
|
||||
const drawableGraph = await graph.getGraphAsync();
|
||||
const image = await drawableGraph.drawMermaidPng();
|
||||
const imageBuffer = new Uint8Array(await image.arrayBuffer());
|
||||
const imageBuffer = new Uint8Array(await image.arrayBuffer());
|
||||
|
||||
await fs.writeFile("chatbot-with-tools.png", imageBuffer);
|
||||
await fs.writeFile("chatbot-with-tools.png", imageBuffer);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||

|
||||
|
||||
## 4. Prompt the chatbot
|
||||
|
||||
Now, prompt the chatbot with a question that will engage the new `human_assistance` tool:
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
user_input = "I need some expert guidance for building an AI agent. Could you request assistance for me?"
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
@@ -138,8 +268,71 @@ 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" }
|
||||
{ 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
|
||||
) {
|
||||
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.
|
||||
[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?'
|
||||
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'
|
||||
id: 'toolu_01Bpxc8rFVMhSaRosS6b85Ts',
|
||||
type: 'tool_call'
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
The chatbot generated a tool call, but then execution has been interrupted. If you inspect the graph state, you see that it stopped at the tools node:
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
snapshot = graph.get_state(config)
|
||||
snapshot.next
|
||||
@@ -149,8 +342,27 @@ snapshot.next
|
||||
('tools',)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
const snapshot = await graph.getState({ configurable: { thread_id: "1" } });
|
||||
snapshot.next;
|
||||
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
|
||||
@@ -162,12 +374,57 @@ 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"),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
Take a closer look at the `humanAssistance` tool:
|
||||
|
||||
```typescript hl_lines="3"
|
||||
const humanAssistance = tool(
|
||||
async ({ query }) => {
|
||||
const humanResponse = interrupt({ query });
|
||||
return humanResponse.data;
|
||||
},
|
||||
{
|
||||
name: "humanAssistance",
|
||||
description: "Request assistance from a human.",
|
||||
schema: z.object({
|
||||
query: z.string().describe("Human readable question for the human"),
|
||||
}),
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
Calling `interrupt` inside the tool will pause execution. Progress is persisted based on the [checkpointer](../../concepts/persistence.md#checkpointer-libraries); so if it is persisting with Postgres, it can resume at any time as long as the database is alive. In this example, it is persisting with the in-memory checkpointer and can resume any time if the JavaScript runtime is running.
|
||||
:::
|
||||
|
||||
## 5. Resume execution
|
||||
|
||||
To resume execution, pass a [`Command`](../../concepts/low_level.md#command) object containing data expected by the tool. The format of this data can be customized based on needs. For this example, use a dict with a key `"data"`:
|
||||
To resume execution, pass a [`Command`](../../concepts/low_level.md#command) object containing data expected by the tool. The format of this data can be customized based on needs.
|
||||
|
||||
``` python
|
||||
:::python
|
||||
|
||||
For this example, use a dict with a key `"data"`:
|
||||
|
||||
```python
|
||||
human_response = (
|
||||
"We, the experts are here to help! We'd recommend you check out LangGraph to build your agent."
|
||||
" It's much more reliable and extensible than simple autonomous agents."
|
||||
@@ -215,12 +472,67 @@ 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.";
|
||||
(" 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",
|
||||
});
|
||||
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}`);
|
||||
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
|
||||
|
||||
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
|
||||
|
||||
```python
|
||||
@@ -272,6 +584,117 @@ memory = InMemorySaver()
|
||||
graph = graph_builder.compile(checkpointer=memory)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import {
|
||||
interrupt,
|
||||
MessagesZodState,
|
||||
StateGraph,
|
||||
MemorySaver,
|
||||
START,
|
||||
END,
|
||||
} from "@langchain/langgraph";
|
||||
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
|
||||
import { isAIMessage } from "@langchain/core/messages";
|
||||
import { ChatAnthropic } from "@langchain/anthropic";
|
||||
import { TavilySearch } from "@langchain/tavily";
|
||||
import {
|
||||
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 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 searchTool = new TavilySearch({ maxResults: 2 });
|
||||
const tools = [searchTool, humanAssistance];
|
||||
|
||||
const llmWithTools = new ChatAnthropic({
|
||||
model: "claude-3-5-sonnet-latest",
|
||||
}).bindTools(tools);
|
||||
const llmWithTools = new ChatAnthropic({
|
||||
model: "claude-3-5-sonnet-latest",
|
||||
}).bindTools(tools);
|
||||
|
||||
const chatbot = async (state: z.infer<typeof MessagesZodState>) => {
|
||||
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.
|
||||
|
||||
// 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 };
|
||||
|
||||
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 });
|
||||
|
||||
const graph = new StateGraph(MessagesZodState)
|
||||
.addNode("chatbot", chatbot)
|
||||
.addNode("tools", new ToolNode(tools))
|
||||
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
|
||||
.addEdge("tools", "chatbot")
|
||||
.addEdge(START, "chatbot")
|
||||
.compile({ checkpointer: memory });
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Next steps
|
||||
|
||||
So far, the tutorial examples have relied on a simple state with one entry: a list of messages. You can go far with this simple state, but if you want to define complex behavior without relying on the message list, you can [add additional fields to the state](./5-customize-state.md).
|
||||
So far, the tutorial examples have relied on a simple state with one entry: a list of messages. You can go far with this simple state, but if you want to define complex behavior without relying on the message list, you can [add additional fields to the state](./5-customize-state.md).
|
||||
|
||||
@@ -10,6 +10,8 @@ In this tutorial, you will add additional fields to the state to define complex
|
||||
|
||||
Update the chatbot to research the birthday of an entity by adding `name` and `birthday` keys to the state:
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
|
||||
@@ -26,13 +28,34 @@ class State(TypedDict):
|
||||
birthday: str
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import { MessagesZodState } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
const State = z.object({
|
||||
messages: MessagesZodState.shape.messages,
|
||||
// highlight-next-line
|
||||
name: z.string(),
|
||||
// highlight-next-line
|
||||
birthday: z.string(),
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Adding this information to the state makes it easily accessible by other graph nodes (like a downstream node that stores or processes the information), as well as the graph's persistence layer.
|
||||
|
||||
## 2. Update the state inside the tool
|
||||
|
||||
:::python
|
||||
|
||||
Now, populate the state keys inside of the `human_assistance` tool. This allows a human to review the information before it is stored in the state. Use [`Command`](../../concepts/low_level.md#using-inside-tools) to issue a state update from inside the tool.
|
||||
|
||||
``` python
|
||||
```python
|
||||
from langchain_core.messages import ToolMessage
|
||||
from langchain_core.tools import InjectedToolCallId, tool
|
||||
|
||||
@@ -76,10 +99,78 @@ def human_assistance(
|
||||
return Command(update=state_update)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
Now, populate the state keys inside of the `humanAssistance` tool. This allows a human to review the information before it is stored in the state. Use [`Command`](../../concepts/low_level.md#using-inside-tools) to issue a state update from inside the tool.
|
||||
|
||||
```typescript
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { ToolMessage } from "@langchain/core/messages";
|
||||
import { Command, interrupt } from "@langchain/langgraph";
|
||||
|
||||
const humanAssistance = tool(
|
||||
async (input, config) => {
|
||||
// Note that because we are generating a ToolMessage for a state update,
|
||||
// we generally require the ID of the corresponding tool call.
|
||||
// This is available in the tool's config.
|
||||
const toolCallId = config?.toolCall?.id as string | undefined;
|
||||
if (!toolCallId) throw new Error("Tool call ID is required");
|
||||
|
||||
const humanResponse = await interrupt({
|
||||
question: "Is this correct?",
|
||||
name: input.name,
|
||||
birthday: input.birthday,
|
||||
});
|
||||
|
||||
// We explicitly update the state with a ToolMessage inside the tool.
|
||||
const stateUpdate = (() => {
|
||||
// If the information is correct, update the state as-is.
|
||||
if (humanResponse.correct?.toLowerCase().startsWith("y")) {
|
||||
return {
|
||||
name: input.name,
|
||||
birthday: input.birthday,
|
||||
messages: [
|
||||
new ToolMessage({ content: "Correct", tool_call_id: toolCallId }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, receive information from the human reviewer.
|
||||
return {
|
||||
name: humanResponse.name || input.name,
|
||||
birthday: humanResponse.birthday || input.birthday,
|
||||
messages: [
|
||||
new ToolMessage({
|
||||
content: `Made a correction: ${JSON.stringify(humanResponse)}`,
|
||||
tool_call_id: toolCallId,
|
||||
}),
|
||||
],
|
||||
};
|
||||
})();
|
||||
|
||||
// We return a Command object in the tool to update our state.
|
||||
return new Command({ update: stateUpdate });
|
||||
},
|
||||
{
|
||||
name: "humanAssistance",
|
||||
description: "Request assistance from a human.",
|
||||
schema: z.object({
|
||||
name: z.string().describe("The name of the entity"),
|
||||
birthday: z.string().describe("The birthday/release date of the entity"),
|
||||
}),
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
The rest of the graph stays the same.
|
||||
|
||||
## 3. Prompt the chatbot
|
||||
|
||||
:::python
|
||||
Prompt the chatbot to look up the "birthday" of the LangGraph library and direct the chatbot to reach out to the `human_assistance` tool once it has the required information. By setting `name` and `birthday` in the arguments for the tool, you force the chatbot to generate proposals for these fields.
|
||||
|
||||
```python
|
||||
@@ -99,6 +190,51 @@ for event in events:
|
||||
event["messages"][-1].pretty_print()
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
Prompt the chatbot to look up the "birthday" of the LangGraph library and direct the chatbot to reach out to the `humanAssistance` tool once it has the required information. By setting `name` and `birthday` in the arguments for the tool, you force the chatbot to generate proposals for these fields.
|
||||
|
||||
```typescript
|
||||
import { isAIMessage } from "@langchain/core/messages";
|
||||
|
||||
const userInput =
|
||||
"Can you look up when LangGraph was released? " +
|
||||
"When you have the answer, use the humanAssistance tool for review.";
|
||||
|
||||
const events = await graph.stream(
|
||||
{ messages: [{ role: "user", content: userInput }] },
|
||||
{ configurable: { thread_id: "1" }, streamMode: "values" }
|
||||
);
|
||||
|
||||
for await (const event of events) {
|
||||
if ("messages" in event) {
|
||||
const lastMessage = event.messages.at(-1);
|
||||
|
||||
console.log(
|
||||
"=".repeat(32),
|
||||
`${lastMessage?.getType()} Message`,
|
||||
"=".repeat(32)
|
||||
);
|
||||
console.log(lastMessage?.text);
|
||||
|
||||
if (
|
||||
lastMessage &&
|
||||
isAIMessage(lastMessage) &&
|
||||
lastMessage.tool_calls?.length
|
||||
) {
|
||||
console.log("Tool Calls:");
|
||||
for (const call of lastMessage.tool_calls) {
|
||||
console.log(` ${call.name} (${call.id})`);
|
||||
console.log(` Args: ${JSON.stringify(call.args)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
```
|
||||
================================ Human Message =================================
|
||||
|
||||
@@ -126,12 +262,20 @@ Tool Calls:
|
||||
birthday: 2023-01-01
|
||||
```
|
||||
|
||||
:::python
|
||||
We've hit the `interrupt` in the `human_assistance` tool again.
|
||||
:::
|
||||
|
||||
:::js
|
||||
We've hit the `interrupt` in the `humanAssistance` tool again.
|
||||
:::
|
||||
|
||||
## 4. Add human assistance
|
||||
|
||||
The chatbot failed to identify the correct date, so supply it with information:
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
human_command = Command(
|
||||
resume={
|
||||
@@ -146,6 +290,53 @@ for event in events:
|
||||
event["messages"][-1].pretty_print()
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import { Command } from "@langchain/langgraph";
|
||||
|
||||
const humanCommand = new Command({
|
||||
resume: {
|
||||
name: "LangGraph",
|
||||
birthday: "Jan 17, 2024",
|
||||
},
|
||||
});
|
||||
|
||||
const resumeEvents = await graph.stream(humanCommand, {
|
||||
configurable: { thread_id: "1" },
|
||||
streamMode: "values",
|
||||
});
|
||||
|
||||
for await (const event of resumeEvents) {
|
||||
if ("messages" in event) {
|
||||
const lastMessage = event.messages.at(-1);
|
||||
|
||||
console.log(
|
||||
"=".repeat(32),
|
||||
`${lastMessage?.getType()} Message`,
|
||||
"=".repeat(32)
|
||||
);
|
||||
console.log(lastMessage?.text);
|
||||
|
||||
if (
|
||||
lastMessage &&
|
||||
isAIMessage(lastMessage) &&
|
||||
lastMessage.tool_calls?.length
|
||||
) {
|
||||
console.log("Tool Calls:");
|
||||
for (const call of lastMessage.tool_calls) {
|
||||
console.log(` ${call.name} (${call.id})`);
|
||||
console.log(` Args: ${JSON.stringify(call.args)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
```
|
||||
================================== Ai Message ==================================
|
||||
|
||||
@@ -175,6 +366,8 @@ It's worth noting that LangGraph had been in development and use for some time b
|
||||
|
||||
Note that these fields are now reflected in the state:
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
snapshot = graph.get_state(config)
|
||||
|
||||
@@ -185,13 +378,34 @@ snapshot = graph.get_state(config)
|
||||
{'name': 'LangGraph', 'birthday': 'Jan 17, 2024'}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
const snapshot = await graph.getState(config);
|
||||
|
||||
const relevantState = Object.fromEntries(
|
||||
Object.entries(snapshot.values).filter(([k]) =>
|
||||
["name", "birthday"].includes(k)
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
```
|
||||
{ name: 'LangGraph', birthday: 'Jan 17, 2024' }
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
This makes them easily accessible to downstream nodes (e.g., a node that further processes or stores the information).
|
||||
|
||||
## 5. Manually update the state
|
||||
|
||||
:::python
|
||||
LangGraph gives a high degree of control over the application state. For instance, at any point (including when interrupted), you can manually override a key using `graph.update_state`:
|
||||
|
||||
``` python
|
||||
```python
|
||||
graph.update_state(config, {"name": "LangGraph (library)"})
|
||||
```
|
||||
|
||||
@@ -201,11 +415,36 @@ graph.update_state(config, {"name": "LangGraph (library)"})
|
||||
'checkpoint_id': '1efd4ec5-cf69-6352-8006-9278f1730162'}}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
LangGraph gives a high degree of control over the application state. For instance, at any point (including when interrupted), you can manually override a key using `graph.updateState`:
|
||||
|
||||
```typescript
|
||||
await graph.updateState(
|
||||
{ configurable: { thread_id: "1" } },
|
||||
{ name: "LangGraph (library)" }
|
||||
);
|
||||
```
|
||||
|
||||
```typescript
|
||||
{
|
||||
configurable: {
|
||||
thread_id: '1',
|
||||
checkpoint_ns: '',
|
||||
checkpoint_id: '1efd4ec5-cf69-6352-8006-9278f1730162'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 6. View the new value
|
||||
|
||||
:::python
|
||||
If you call `graph.get_state`, you can see the new value is reflected:
|
||||
|
||||
``` python
|
||||
```python
|
||||
snapshot = graph.get_state(config)
|
||||
|
||||
{k: v for k, v in snapshot.values.items() if k in ("name", "birthday")}
|
||||
@@ -215,12 +454,35 @@ snapshot = graph.get_state(config)
|
||||
{'name': 'LangGraph (library)', 'birthday': 'Jan 17, 2024'}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
If you call `graph.getState`, you can see the new value is reflected:
|
||||
|
||||
```typescript
|
||||
const updatedSnapshot = await graph.getState(config);
|
||||
|
||||
const updatedRelevantState = Object.fromEntries(
|
||||
Object.entries(updatedSnapshot.values).filter(([k]) =>
|
||||
["name", "birthday"].includes(k)
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
```typescript
|
||||
{ name: 'LangGraph (library)', birthday: 'Jan 17, 2024' }
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Manual state updates will [generate a trace](https://smith.langchain.com/public/7ebb7827-378d-49fe-9f6c-5df0e90086c8/r) in LangSmith. If desired, they can also be used to [control human-in-the-loop workflows](../../how-tos/human_in_the_loop/add-human-in-the-loop.md). Use of the `interrupt` function is generally recommended instead, as it allows data to be transmitted in a human-in-the-loop interaction independently of state updates.
|
||||
|
||||
**Congratulations!** You've added custom keys to the state to facilitate a more complex workflow, and learned how to generate state updates from inside tools.
|
||||
|
||||
Check out the code snippet below to review the graph from this tutorial:
|
||||
|
||||
:::python
|
||||
|
||||
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
|
||||
|
||||
<!---
|
||||
@@ -305,7 +567,111 @@ memory = InMemorySaver()
|
||||
graph = graph_builder.compile(checkpointer=memory)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Command,
|
||||
interrupt,
|
||||
MessagesZodState,
|
||||
MemorySaver,
|
||||
StateGraph,
|
||||
END,
|
||||
START,
|
||||
} from "@langchain/langgraph";
|
||||
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
|
||||
import { ChatAnthropic } from "@langchain/anthropic";
|
||||
import { TavilySearch } from "@langchain/tavily";
|
||||
import { ToolMessage } from "@langchain/core/messages";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
const State = z.object({
|
||||
messages: MessagesZodState.shape.messages,
|
||||
name: z.string(),
|
||||
birthday: z.string(),
|
||||
});
|
||||
|
||||
const humanAssistance = tool(
|
||||
async (input, config) => {
|
||||
// Note that because we are generating a ToolMessage for a state update, we
|
||||
// generally require the ID of the corresponding tool call. This is available
|
||||
// in the tool's config.
|
||||
const toolCallId = config?.toolCall?.id as string | undefined;
|
||||
if (!toolCallId) throw new Error("Tool call ID is required");
|
||||
|
||||
const humanResponse = await interrupt({
|
||||
question: "Is this correct?",
|
||||
name: input.name,
|
||||
birthday: input.birthday,
|
||||
});
|
||||
|
||||
// We explicitly update the state with a ToolMessage inside the tool.
|
||||
const stateUpdate = (() => {
|
||||
// If the information is correct, update the state as-is.
|
||||
if (humanResponse.correct?.toLowerCase().startsWith("y")) {
|
||||
return {
|
||||
name: input.name,
|
||||
birthday: input.birthday,
|
||||
messages: [
|
||||
new ToolMessage({ content: "Correct", tool_call_id: toolCallId }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, receive information from the human reviewer.
|
||||
return {
|
||||
name: humanResponse.name || input.name,
|
||||
birthday: humanResponse.birthday || input.birthday,
|
||||
messages: [
|
||||
new ToolMessage({
|
||||
content: `Made a correction: ${JSON.stringify(humanResponse)}`,
|
||||
tool_call_id: toolCallId,
|
||||
}),
|
||||
],
|
||||
};
|
||||
})();
|
||||
|
||||
// We return a Command object in the tool to update our state.
|
||||
return new Command({ update: stateUpdate });
|
||||
},
|
||||
{
|
||||
name: "humanAssistance",
|
||||
description: "Request assistance from a human.",
|
||||
schema: z.object({
|
||||
name: z.string().describe("The name of the entity"),
|
||||
birthday: z.string().describe("The birthday/release date of the entity"),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const searchTool = new TavilySearch({ maxResults: 2 });
|
||||
|
||||
const tools = [searchTool, humanAssistance];
|
||||
const llmWithTools = new ChatAnthropic({
|
||||
model: "claude-3-5-sonnet-latest",
|
||||
}).bindTools(tools);
|
||||
|
||||
const memory = new MemorySaver();
|
||||
|
||||
const chatbot = async (state: z.infer<typeof State>) => {
|
||||
const message = await llmWithTools.invoke(state.messages);
|
||||
return { messages: message };
|
||||
};
|
||||
|
||||
const graph = new StateGraph(State)
|
||||
.addNode("chatbot", chatbot)
|
||||
.addNode("tools", new ToolNode(tools))
|
||||
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
|
||||
.addEdge("tools", "chatbot")
|
||||
.addEdge(START, "chatbot")
|
||||
.compile({ checkpointer: memory });
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Next steps
|
||||
|
||||
There's one more concept to review before finishing the LangGraph basics tutorials: connecting `checkpointing` and `state updates` to [time travel](./6-time-travel.md).
|
||||
|
||||
There's one more concept to review before finishing the LangGraph basics tutorials: connecting `checkpointing` and `state updates` to [time travel](./6-time-travel.md).
|
||||
|
||||
@@ -4,7 +4,7 @@ In a typical chatbot workflow, the user interacts with the bot one or more times
|
||||
|
||||
What if you want a user to be able to start from a previous response and explore a different outcome? Or what if you want users to be able to rewind your chatbot's work to fix mistakes or try a different strategy, something that is common in applications like autonomous software engineers?
|
||||
|
||||
You can create these types of experiences using LangGraph's built-in **time travel** functionality.
|
||||
You can create these types of experiences using LangGraph's built-in **time travel** functionality.
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -12,7 +12,15 @@ You can create these types of experiences using LangGraph's built-in **time trav
|
||||
|
||||
## 1. Rewind your graph
|
||||
|
||||
:::python
|
||||
Rewind your graph by fetching a checkpoint using the graph's `get_state_history` method. You can then resume execution at this previous point in time.
|
||||
:::
|
||||
|
||||
:::js
|
||||
Rewind your graph by fetching a checkpoint using the graph's `getStateHistory` method. You can then resume execution at this previous point in time.
|
||||
:::
|
||||
|
||||
:::python
|
||||
|
||||
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
|
||||
|
||||
@@ -64,11 +72,49 @@ memory = InMemorySaver()
|
||||
graph = graph_builder.compile(checkpointer=memory)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import {
|
||||
StateGraph,
|
||||
START,
|
||||
END,
|
||||
MessagesZodState,
|
||||
MemorySaver,
|
||||
} from "@langchain/langgraph";
|
||||
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
|
||||
import { TavilySearch } from "@langchain/tavily";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { z } from "zod";
|
||||
|
||||
const State = z.object({ messages: MessagesZodState.shape.messages });
|
||||
|
||||
const tools = [new TavilySearch({ maxResults: 2 })];
|
||||
const llmWithTools = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools(tools);
|
||||
const memory = new MemorySaver();
|
||||
|
||||
const graph = new StateGraph(State)
|
||||
.addNode("chatbot", async (state) => ({
|
||||
messages: [await llmWithTools.invoke(state.messages)],
|
||||
}))
|
||||
.addNode("tools", new ToolNode(tools))
|
||||
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
|
||||
.addEdge("tools", "chatbot")
|
||||
.addEdge(START, "chatbot")
|
||||
.compile({ checkpointer: memory });
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 2. Add steps
|
||||
|
||||
Add steps to your graph. Every step will be checkpointed in its state history:
|
||||
|
||||
``` python
|
||||
:::python
|
||||
|
||||
```python
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
events = graph.stream(
|
||||
{
|
||||
@@ -159,7 +205,7 @@ Tool Calls:
|
||||
================================= Tool Message =================================
|
||||
Name: tavily_search_results_json
|
||||
|
||||
[{"url": "https://towardsdatascience.com/building-autonomous-multi-tool-agents-with-gemini-2-0-and-langgraph-ad3d7bd5e79d", "content": "Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph | by Youness Mansar | Jan, 2025 | Towards Data Science Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph A practical tutorial with full code examples for building and running multi-tool agents Towards Data Science LLMs are remarkable — they can memorize vast amounts of information, answer general knowledge questions, write code, generate stories, and even fix your grammar. In this tutorial, we are going to build a simple LLM agent that is equipped with four tools that it can use to answer a user’s question. This Agent will have the following specifications: Follow Published in Towards Data Science --------------------------------- Your home for data science and AI. Follow Follow Follow"}, {"url": "https://github.com/anmolaman20/Tools_and_Agents", "content": "GitHub - anmolaman20/Tools_and_Agents: This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository serves as a comprehensive guide for building AI-powered agents using Langchain and Langgraph. It provides hands-on examples, practical tutorials, and resources for developers and AI enthusiasts to master building intelligent systems and workflows. AI Agent Development: Gain insights into creating intelligent systems that think, reason, and adapt in real time. This repository is ideal for AI practitioners, developers exploring language models, or anyone interested in building intelligent systems. This repository provides resources for building AI agents using Langchain and Langgraph."}]
|
||||
[{"url": "https://towardsdatascience.com/building-autonomous-multi-tool-agents-with-gemini-2-0-and-langgraph-ad3d7bd5e79d", "content": "Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph | by Youness Mansar | Jan, 2025 | Towards Data Science Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph A practical tutorial with full code examples for building and running multi-tool agents Towards Data Science LLMs are remarkable — they can memorize vast amounts of information, answer general knowledge questions, write code, generate stories, and even fix your grammar. In this tutorial, we are going to build a simple LLM agent that is equipped with four tools that it can use to answer a user's question. This Agent will have the following specifications: Follow Published in Towards Data Science --------------------------------- Your home for data science and AI. Follow Follow Follow"}, {"url": "https://github.com/anmolaman20/Tools_and_Agents", "content": "GitHub - anmolaman20/Tools_and_Agents: This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository serves as a comprehensive guide for building AI-powered agents using Langchain and Langgraph. It provides hands-on examples, practical tutorials, and resources for developers and AI enthusiasts to master building intelligent systems and workflows. AI Agent Development: Gain insights into creating intelligent systems that think, reason, and adapt in real time. This repository is ideal for AI practitioners, developers exploring language models, or anyone interested in building intelligent systems. This repository provides resources for building AI agents using Langchain and Langgraph."}]
|
||||
================================== Ai Message ==================================
|
||||
|
||||
Great idea! Building an autonomous agent with LangGraph is definitely an exciting project. Based on the latest information I've found, here are some insights and tips for building autonomous agents with LangGraph:
|
||||
@@ -177,11 +223,140 @@ Building an autonomous agent is an iterative process, so be prepared to refine a
|
||||
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import { randomUUID } from "node:crypto";
|
||||
const threadId = randomUUID();
|
||||
|
||||
let iter = 0;
|
||||
|
||||
for (const userInput of [
|
||||
"I'm learning LangGraph. Could you do some research on it for me?",
|
||||
"Ya that's helpful. Maybe I'll build an autonomous agent with it!",
|
||||
]) {
|
||||
iter += 1;
|
||||
|
||||
console.log(`\n--- Conversation Turn ${iter} ---\n`);
|
||||
const events = await graph.stream(
|
||||
{ messages: [{ role: "user", content: userInput }] },
|
||||
{ configurable: { thread_id: threadId }, streamMode: "values" }
|
||||
);
|
||||
|
||||
for await (const event of events) {
|
||||
if ("messages" in event) {
|
||||
const lastMessage = event.messages.at(-1);
|
||||
|
||||
console.log(
|
||||
"=".repeat(32),
|
||||
`${lastMessage?.getType()} Message`,
|
||||
"=".repeat(32)
|
||||
);
|
||||
console.log(lastMessage?.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
--- Conversation Turn 1 ---
|
||||
|
||||
================================ human Message ================================
|
||||
I'm learning LangGraph.js. Could you do some research on it for me?
|
||||
================================ ai Message ================================
|
||||
I'll search for information about LangGraph.js for you.
|
||||
================================ tool Message ================================
|
||||
{
|
||||
"query": "LangGraph.js framework TypeScript langchain what is it tutorial guide",
|
||||
"follow_up_questions": null,
|
||||
"answer": null,
|
||||
"images": [],
|
||||
"results": [
|
||||
{
|
||||
"url": "https://techcommunity.microsoft.com/blog/educatordeveloperblog/an-absolute-beginners-guide-to-langgraph-js/4212496",
|
||||
"title": "An Absolute Beginner's Guide to LangGraph.js",
|
||||
"content": "(...)",
|
||||
"score": 0.79369855,
|
||||
"raw_content": null
|
||||
},
|
||||
{
|
||||
"url": "https://langchain-ai.github.io/langgraphjs/",
|
||||
"title": "LangGraph.js",
|
||||
"content": "(...)",
|
||||
"score": 0.78154784,
|
||||
"raw_content": null
|
||||
}
|
||||
],
|
||||
"response_time": 2.37
|
||||
}
|
||||
================================ ai Message ================================
|
||||
Let me provide you with an overview of LangGraph.js based on the search results:
|
||||
|
||||
LangGraph.js is a JavaScript/TypeScript library that's part of the LangChain ecosystem, specifically designed for creating and managing complex LLM (Large Language Model) based workflows. Here are the key points about LangGraph.js:
|
||||
|
||||
1. Purpose:
|
||||
- It's a low-level orchestration framework for building controllable agents
|
||||
- Particularly useful for creating agentic workflows where LLMs decide the course of action based on current state
|
||||
- Helps model workflows as graphs with nodes and edges
|
||||
|
||||
(...)
|
||||
|
||||
--- Conversation Turn 2 ---
|
||||
|
||||
================================ human Message ================================
|
||||
Ya that's helpful. Maybe I'll build an autonomous agent with it!
|
||||
================================ ai Message ================================
|
||||
Let me search for specific information about building autonomous agents with LangGraph.js.
|
||||
================================ tool Message ================================
|
||||
{
|
||||
"query": "how to build autonomous agents with LangGraph.js examples tutorial react agent",
|
||||
"follow_up_questions": null,
|
||||
"answer": null,
|
||||
"images": [],
|
||||
"results": [
|
||||
{
|
||||
"url": "https://ai.google.dev/gemini-api/docs/langgraph-example",
|
||||
"title": "ReAct agent from scratch with Gemini 2.5 and LangGraph",
|
||||
"content": "(...)",
|
||||
"score": 0.7602419,
|
||||
"raw_content": null
|
||||
},
|
||||
{
|
||||
"url": "https://www.youtube.com/watch?v=ZfjaIshGkmk",
|
||||
"title": "Build Autonomous AI Agents with ReAct and LangGraph Tools",
|
||||
"content": "(...)",
|
||||
"score": 0.7471924,
|
||||
"raw_content": null
|
||||
}
|
||||
],
|
||||
"response_time": 1.98
|
||||
}
|
||||
================================ ai Message ================================
|
||||
Based on the search results, I can provide you with a practical overview of how to build an autonomous agent with LangGraph.js. Here's what you need to know:
|
||||
|
||||
1. Basic Structure for Building an Agent:
|
||||
- LangGraph.js provides a ReAct (Reason + Act) pattern implementation
|
||||
- The basic components include:
|
||||
- State management for conversation history
|
||||
- Nodes for different actions
|
||||
- Edges for decision-making flow
|
||||
- Tools for specific functionalities
|
||||
|
||||
(...)
|
||||
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 3. Replay the full state history
|
||||
|
||||
Now that you have added steps to the chatbot, you can `replay` the full state history to see everything that occurred.
|
||||
|
||||
``` python
|
||||
:::python
|
||||
|
||||
```python
|
||||
to_replay = None
|
||||
for state in graph.get_state_history(config):
|
||||
print("Num Messages: ", len(state.values["messages"]), "Next: ", state.next)
|
||||
@@ -214,11 +389,73 @@ Num Messages: 0 Next: ('__start__',)
|
||||
--------------------------------------------------------------------------------
|
||||
```
|
||||
|
||||
Checkpoints are saved for every step of the graph. This __spans invocations__ so you can rewind across a full thread's history.
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import type { StateSnapshot } from "@langchain/langgraph";
|
||||
|
||||
let toReplay: StateSnapshot | undefined;
|
||||
for await (const state of graph.getStateHistory({
|
||||
configurable: { thread_id: threadId },
|
||||
})) {
|
||||
console.log(
|
||||
`Num Messages: ${state.values.messages.length}, Next: ${JSON.stringify(
|
||||
state.next
|
||||
)}`
|
||||
);
|
||||
console.log("-".repeat(80));
|
||||
if (state.values.messages.length === 6) {
|
||||
// We are somewhat arbitrarily selecting a specific state based on the number of chat messages in the state.
|
||||
toReplay = state;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
Num Messages: 8 Next: []
|
||||
--------------------------------------------------------------------------------
|
||||
Num Messages: 7 Next: ["chatbot"]
|
||||
--------------------------------------------------------------------------------
|
||||
Num Messages: 6 Next: ["tools"]
|
||||
--------------------------------------------------------------------------------
|
||||
Num Messages: 7, Next: ["chatbot"]
|
||||
--------------------------------------------------------------------------------
|
||||
Num Messages: 6, Next: ["tools"]
|
||||
--------------------------------------------------------------------------------
|
||||
Num Messages: 5, Next: ["chatbot"]
|
||||
--------------------------------------------------------------------------------
|
||||
Num Messages: 4, Next: ["__start__"]
|
||||
--------------------------------------------------------------------------------
|
||||
Num Messages: 4, Next: []
|
||||
--------------------------------------------------------------------------------
|
||||
Num Messages: 3, Next: ["chatbot"]
|
||||
--------------------------------------------------------------------------------
|
||||
Num Messages: 2, Next: ["tools"]
|
||||
--------------------------------------------------------------------------------
|
||||
Num Messages: 1, Next: ["chatbot"]
|
||||
--------------------------------------------------------------------------------
|
||||
Num Messages: 0, Next: ["__start__"]
|
||||
--------------------------------------------------------------------------------
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Checkpoints are saved for every step of the graph. This **spans invocations** so you can rewind across a full thread's history.
|
||||
|
||||
## Resume from a checkpoint
|
||||
|
||||
:::python
|
||||
|
||||
Resume from the `to_replay` state, which is after the `chatbot` node in the second graph invocation. Resuming from this point will call the **action** node next.
|
||||
:::
|
||||
|
||||
:::js
|
||||
Resume from the `toReplay` state, which is after a specific node in one of the graph invocations. Resuming from this point will call the next scheduled node.
|
||||
:::
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
print(to_replay.next)
|
||||
@@ -230,12 +467,37 @@ print(to_replay.config)
|
||||
{'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1efd43e3-0c1f-6c4e-8006-891877d65740'}}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
Resume from the `toReplay` state, which is after the `chatbot` node in one of the graph invocations. Resuming from this point will call the next scheduled node.
|
||||
|
||||
```typescript
|
||||
console.log(toReplay.next);
|
||||
console.log(toReplay.config);
|
||||
```
|
||||
|
||||
```
|
||||
["tools"]
|
||||
{
|
||||
configurable: {
|
||||
thread_id: "007708b8-ea9b-4ff7-a7ad-3843364dbf75",
|
||||
checkpoint_ns: "",
|
||||
checkpoint_id: "1efd43e3-0c1f-6c4e-8006-891877d65740"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 4. Load a state from a moment-in-time
|
||||
|
||||
:::python
|
||||
|
||||
The checkpoint's `to_replay.config` contains a `checkpoint_id` timestamp. Providing this `checkpoint_id` value tells LangGraph's checkpointer to **load** the state from that moment in time.
|
||||
|
||||
|
||||
``` python
|
||||
```python
|
||||
# The `checkpoint_id` in the `to_replay.config` corresponds to a state we've persisted to our checkpointer.
|
||||
for event in graph.stream(None, to_replay.config, stream_mode="values"):
|
||||
if "messages" in event:
|
||||
@@ -254,19 +516,16 @@ Tool Calls:
|
||||
================================= Tool Message =================================
|
||||
Name: tavily_search_results_json
|
||||
|
||||
[{"url": "https://towardsdatascience.com/building-autonomous-multi-tool-agents-with-gemini-2-0-and-langgraph-ad3d7bd5e79d", "content": "Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph | by Youness Mansar | Jan, 2025 | Towards Data Science Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph A practical tutorial with full code examples for building and running multi-tool agents Towards Data Science LLMs are remarkable — they can memorize vast amounts of information, answer general knowledge questions, write code, generate stories, and even fix your grammar. In this tutorial, we are going to build a simple LLM agent that is equipped with four tools that it can use to answer a user’s question. This Agent will have the following specifications: Follow Published in Towards Data Science --------------------------------- Your home for data science and AI. Follow Follow Follow"}, {"url": "https://github.com/anmolaman20/Tools_and_Agents", "content": "GitHub - anmolaman20/Tools_and_Agents: This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository serves as a comprehensive guide for building AI-powered agents using Langchain and Langgraph. It provides hands-on examples, practical tutorials, and resources for developers and AI enthusiasts to master building intelligent systems and workflows. AI Agent Development: Gain insights into creating intelligent systems that think, reason, and adapt in real time. This repository is ideal for AI practitioners, developers exploring language models, or anyone interested in building intelligent systems. This repository provides resources for building AI agents using Langchain and Langgraph."}]
|
||||
[{"url": "https://towardsdatascience.com/building-autonomous-multi-tool-agents-with-gemini-2-0-and-langgraph-ad3d7bd5e79d", "content": "Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph | by Youness Mansar | Jan, 2025 | Towards Data Science Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph A practical tutorial with full code examples for building and running multi-tool agents Towards Data Science LLMs are remarkable — they can memorize vast amounts of information, answer general knowledge questions, write code, generate stories, and even fix your grammar. In this tutorial, we are going to build a simple LLM agent that is equipped with four tools that it can use to answer a user's question. This Agent will have the following specifications: Follow Published in Towards Data Science --------------------------------- Your home for data science and AI. Follow Follow Follow"}, {"url": "https://github.com/anmolaman20/Tools_and_Agents", "content": "GitHub - anmolaman20/Tools_and_Agents: This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository serves as a comprehensive guide for building AI-powered agents using Langchain and Langgraph. It provides hands-on examples, practical tutorials, and resources for developers and AI enthusiasts to master building intelligent systems and workflows. AI Agent Development: Gain insights into creating intelligent systems that think, reason, and adapt in real time. This repository is ideal for AI practitioners, developers exploring language models, or anyone interested in building intelligent systems. This repository provides resources for building AI agents using Langchain and Langgraph."}]
|
||||
================================== Ai Message ==================================
|
||||
|
||||
Great idea! Building an autonomous agent with LangGraph is indeed an excellent way to apply and deepen your understanding of the technology. Based on the search results, I can provide you with some insights and resources to help you get started:
|
||||
Great idea! Building an autonomous agent with LangGraph is definitely an exciting project. Based on the latest information I've found, here are some insights and tips for building autonomous agents with LangGraph:
|
||||
|
||||
1. Multi-Tool Agents:
|
||||
LangGraph is well-suited for building autonomous agents that can use multiple tools. This allows your agent to have a variety of capabilities and choose the appropriate tool based on the task at hand.
|
||||
1. Multi-Tool Agents: LangGraph is particularly well-suited for creating autonomous agents that can use multiple tools. This allows your agent to have a diverse set of capabilities and choose the right tool for each task.
|
||||
|
||||
2. Integration with Large Language Models (LLMs):
|
||||
There's a tutorial that specifically mentions using Gemini 2.0 (Google's LLM) with LangGraph to build autonomous agents. This suggests that LangGraph can be integrated with various LLMs, giving you flexibility in choosing the language model that best fits your needs.
|
||||
2. Integration with Large Language Models (LLMs): You can combine LangGraph with powerful LLMs like Gemini 2.0 to create more intelligent and capable agents. The LLM can serve as the "brain" of your agent, making decisions and generating responses.
|
||||
|
||||
3. Practical Tutorials:
|
||||
There are tutorials available that provide full code examples for building and running multi-tool agents. These can be invaluable as you start your project, giving you a concrete starting point and demonstrating best practices.
|
||||
3. Workflow Management: LangGraph excels at managing complex, multi-step AI workflows. This is crucial for autonomous agents that need to break down tasks into smaller steps and execute them in the right order.
|
||||
...
|
||||
|
||||
Remember, building an autonomous agent is an iterative process. Start simple and gradually increase complexity as you become more comfortable with LangGraph and its capabilities.
|
||||
@@ -275,7 +534,83 @@ Would you like more information on any specific aspect of building your autonomo
|
||||
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
|
||||
```
|
||||
|
||||
The graph resumed execution from the `action` node. You can tell this is the case since the first value printed above is the response from our search engine tool.
|
||||
The graph resumed execution from the `tools` node. You can tell this is the case since the first value printed above is the response from our search engine tool.
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
The checkpoint's `toReplay.config` contains a `checkpoint_id` timestamp. Providing this `checkpoint_id` value tells LangGraph's checkpointer to **load** the state from that moment in time.
|
||||
|
||||
```typescript
|
||||
// The `checkpoint_id` in the `toReplay.config` corresponds to a state we've persisted to our checkpointer.
|
||||
for await (const event of await graph.stream(null, {
|
||||
...toReplay?.config,
|
||||
streamMode: "values",
|
||||
})) {
|
||||
if ("messages" in event) {
|
||||
const lastMessage = event.messages.at(-1);
|
||||
|
||||
console.log(
|
||||
"=".repeat(32),
|
||||
`${lastMessage?.getType()} Message`,
|
||||
"=".repeat(32)
|
||||
);
|
||||
console.log(lastMessage?.text);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
================================ ai Message ================================
|
||||
Let me search for specific information about building autonomous agents with LangGraph.js.
|
||||
================================ tool Message ================================
|
||||
{
|
||||
"query": "how to build autonomous agents with LangGraph.js examples tutorial",
|
||||
"follow_up_questions": null,
|
||||
"answer": null,
|
||||
"images": [],
|
||||
"results": [
|
||||
{
|
||||
"url": "https://www.mongodb.com/developer/languages/typescript/build-javascript-ai-agent-langgraphjs-mongodb/",
|
||||
"title": "Build a JavaScript AI Agent With LangGraph.js and MongoDB",
|
||||
"content": "(...)",
|
||||
"score": 0.7672197,
|
||||
"raw_content": null
|
||||
},
|
||||
{
|
||||
"url": "https://medium.com/@lorevanoudenhove/how-to-build-ai-agents-with-langgraph-a-step-by-step-guide-5d84d9c7e832",
|
||||
"title": "How to Build AI Agents with LangGraph: A Step-by-Step Guide",
|
||||
"content": "(...)",
|
||||
"score": 0.7407191,
|
||||
"raw_content": null
|
||||
}
|
||||
],
|
||||
"response_time": 0.82
|
||||
}
|
||||
================================ ai Message ================================
|
||||
Based on the search results, I can share some practical information about building autonomous agents with LangGraph.js. Here are some concrete examples and approaches:
|
||||
|
||||
1. Example HR Assistant Agent:
|
||||
- Can handle HR-related queries using employee information
|
||||
- Features include:
|
||||
- Starting and continuing conversations
|
||||
- Looking up information using vector search
|
||||
- Persisting conversation state using checkpoints
|
||||
- Managing threaded conversations
|
||||
|
||||
2. Energy Savings Calculator Agent:
|
||||
- Functions as a lead generation tool for solar panel sales
|
||||
- Capabilities include:
|
||||
- Calculating potential energy savings
|
||||
- Handling multi-step conversations
|
||||
- Processing user inputs for personalized estimates
|
||||
- Managing conversation state
|
||||
|
||||
(...)
|
||||
```
|
||||
|
||||
The graph resumed execution from the `tools` node. You can tell this is the case since the first value printed above is the response from our search engine tool.
|
||||
:::
|
||||
|
||||
**Congratulations!** You've now used time-travel checkpoint traversal in LangGraph. Being able to rewind and explore alternative paths opens up a world of possibilities for debugging, experimentation, and interactive applications.
|
||||
|
||||
@@ -285,4 +620,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.
|
||||
|
||||
@@ -10,57 +10,69 @@ Before you begin, ensure you have the following:
|
||||
|
||||
## 1. Install the LangGraph CLI
|
||||
|
||||
=== "Python server"
|
||||
:::python
|
||||
|
||||
Python >= 3.11 is required.
|
||||
```shell
|
||||
# Python >= 3.11 is required.
|
||||
|
||||
```shell
|
||||
pip install --upgrade "langgraph-cli[inmem]"
|
||||
```
|
||||
pip install --upgrade "langgraph-cli[inmem]"
|
||||
```
|
||||
|
||||
=== "Node server"
|
||||
:::
|
||||
|
||||
```shell
|
||||
npx @langchain/langgraph-cli
|
||||
```
|
||||
:::js
|
||||
|
||||
```shell
|
||||
npx @langchain/langgraph-cli
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 2. Create a LangGraph app 🌱
|
||||
|
||||
Create a new app from the [`new-langgraph-project-python` template](https://github.com/langchain-ai/new-langgraph-project) or [`new-langgraph-project-js` template](https://github.com/langchain-ai/new-langgraphjs-project). This template demonstrates a single-node application you can extend with your own logic.
|
||||
:::python
|
||||
Create a new app from the [`new-langgraph-project-python` template](https://github.com/langchain-ai/new-langgraph-project). This template demonstrates a single-node application you can extend with your own logic.
|
||||
|
||||
=== "Python server"
|
||||
|
||||
```shell
|
||||
langgraph new path/to/your/app --template new-langgraph-project-python
|
||||
```
|
||||
|
||||
=== "Node server"
|
||||
|
||||
```shell
|
||||
langgraph new path/to/your/app --template new-langgraph-project-js
|
||||
```
|
||||
```shell
|
||||
langgraph new path/to/your/app --template new-langgraph-project-python
|
||||
```
|
||||
|
||||
!!! tip "Additional templates"
|
||||
|
||||
If you use `langgraph new` without specifying a template, you will be presented with an interactive menu that will allow you to choose from a list of available templates.
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
Create a new app from the [`new-langgraph-project-js` template](https://github.com/langchain-ai/new-langgraphjs-project). This template demonstrates a single-node application you can extend with your own logic.
|
||||
|
||||
```shell
|
||||
npm create langgraph
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 3. 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:
|
||||
|
||||
=== "Python server"
|
||||
:::python
|
||||
|
||||
```shell
|
||||
cd path/to/your/app
|
||||
pip install -e .
|
||||
```
|
||||
```shell
|
||||
cd path/to/your/app
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
=== "Node server"
|
||||
:::
|
||||
|
||||
```shell
|
||||
cd path/to/your/app
|
||||
yarn install
|
||||
```
|
||||
:::js
|
||||
|
||||
```shell
|
||||
cd path/to/your/app
|
||||
npm install
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 4. Create a `.env` file
|
||||
|
||||
@@ -74,17 +86,21 @@ LANGSMITH_API_KEY=lsv2...
|
||||
|
||||
Start the LangGraph API server locally:
|
||||
|
||||
=== "Python server"
|
||||
:::python
|
||||
|
||||
```shell
|
||||
langgraph dev
|
||||
```
|
||||
```shell
|
||||
langgraph dev
|
||||
```
|
||||
|
||||
=== "Node server"
|
||||
:::
|
||||
|
||||
```shell
|
||||
npx @langchain/langgraph-cli dev
|
||||
```
|
||||
:::js
|
||||
|
||||
```shell
|
||||
npx @langchain/langgraph-cli dev
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Sample output:
|
||||
|
||||
@@ -120,6 +136,7 @@ For a LangGraph Server running on a custom host/port, update the baseURL paramet
|
||||
|
||||
## 7. Test the API
|
||||
|
||||
:::python
|
||||
=== "Python SDK (async)"
|
||||
|
||||
1. Install the LangGraph Python SDK:
|
||||
@@ -185,7 +202,29 @@ For a LangGraph Server running on a custom host/port, update the baseURL paramet
|
||||
print("\n\n")
|
||||
```
|
||||
|
||||
=== "Rest API"
|
||||
|
||||
```bash
|
||||
curl -s --request POST \
|
||||
--url "http://localhost:2024/runs/stream" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {
|
||||
\"messages\": [
|
||||
{
|
||||
\"role\": \"human\",
|
||||
\"content\": \"What is LangGraph?\"
|
||||
}
|
||||
]
|
||||
},
|
||||
\"stream_mode\": \"messages-tuple\"
|
||||
}"
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
=== "Javascript SDK"
|
||||
|
||||
1. Install the LangGraph JS SDK:
|
||||
@@ -242,6 +281,8 @@ For a LangGraph Server running on a custom host/port, update the baseURL paramet
|
||||
}"
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Next steps
|
||||
|
||||
Now that you have a LangGraph app running locally, take your journey further by exploring deployment and advanced features:
|
||||
@@ -249,5 +290,13 @@ Now that you have a LangGraph app running locally, take your journey further by
|
||||
- [Deployment quickstart](../../cloud/quick_start.md): Deploy your LangGraph app using LangGraph Platform.
|
||||
- [LangGraph Platform overview](../../concepts/langgraph_platform.md): Learn about foundational LangGraph Platform concepts.
|
||||
- [LangGraph Server API Reference](../../cloud/reference/api/api_ref.html): Explore the LangGraph Server API documentation.
|
||||
|
||||
:::python
|
||||
|
||||
- [Python SDK Reference](../../cloud/reference/sdk/python_sdk_ref.md): Explore the Python SDK API Reference.
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
- [JS/TS SDK Reference](../../cloud/reference/sdk/js_ts_sdk_ref.md): Explore the JS/TS SDK API Reference.
|
||||
:::
|
||||
|
||||
@@ -306,7 +306,7 @@ Name: math_agent
|
||||
|
||||
## 2. Create supervisor with `langgraph-supervisor`
|
||||
|
||||
To implement out multi-agent system, we will use [`create_supervisor`][langgraph_supervisor.supervisor.create_supervisor] from the prebuilt `langgraph-supervisor` library:
|
||||
To implement out multi-agent system, we will use @[`create_supervisor`][create_supervisor] from the prebuilt `langgraph-supervisor` library:
|
||||
|
||||
```python
|
||||
from langgraph_supervisor import create_supervisor
|
||||
@@ -478,7 +478,7 @@ assign_to_math_agent = create_handoff_tool(
|
||||
|
||||
### Create supervisor agent
|
||||
|
||||
Then, let's create the supervisor agent with the handoff tools we just defined. We will use the prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent]:
|
||||
Then, let's create the supervisor agent with the handoff tools we just defined. We will use the prebuilt @[`create_react_agent`][create_react_agent]:
|
||||
|
||||
```python
|
||||
supervisor_agent = create_react_agent(
|
||||
@@ -654,7 +654,7 @@ Name: tavily_search
|
||||
!!! important
|
||||
You can see that the supervisor system appends **all** of the individual agent messages (i.e., their internal tool-calling loop) to the full message history. This means that on every supervisor turn, supervisor agent sees this full history. If you want more control over:
|
||||
|
||||
* **how inputs are passed to agents**: you can use LangGraph [`Send()`][langgraph.types.Send] primitive to directly send data to the worker agents during the handoff. See the [task delegation](#4-create-delegation-tasks) example below
|
||||
* **how inputs are passed to agents**: you can use LangGraph @[`Send()`][Send] primitive to directly send data to the worker agents during the handoff. See the [task delegation](#4-create-delegation-tasks) example below
|
||||
* **how agent outputs are added**: you can control how much of the agent's internal message history is added to the overall supervisor message history by wrapping the agent in a separate node function:
|
||||
|
||||
```python
|
||||
@@ -742,7 +742,7 @@ supervisor_with_description = (
|
||||
```
|
||||
|
||||
!!! note
|
||||
We're using [`Send()`][langgraph.types.Send] primitive in the `handoff_tool`. This means that instead of receiving the full `supervisor` graph state as input, each worker agent only sees the contents of the `Send` payload. In this example, we're sending the task description as a single "human" message.
|
||||
We're using @[`Send()`][Send] primitive in the `handoff_tool`. This means that instead of receiving the full `supervisor` graph state as input, each worker agent only sees the contents of the `Send` payload. In this example, we're sending the task description as a single "human" message.
|
||||
|
||||
Let's now running it with the same input query:
|
||||
|
||||
|
||||
+1009
-20
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user