mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-10-02 14:35:18 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fddba5106 | ||
|
|
ad6dd59d53 | ||
|
|
3038a4ff12 | ||
|
|
d221e7ced9 | ||
|
|
d136ab5df7 |
@@ -2,5 +2,4 @@ JS_LINK_MAP = {
|
||||
"langgraph.types.interrupt": "https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph.interrupt-2.html",
|
||||
"create_react_agent": "https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph_prebuilt.createReactAgent.html",
|
||||
"langgraph.types.Command": "https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph.Command.html",
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ from mkdocs.structure.files import Files, File
|
||||
from mkdocs.structure.pages import Page
|
||||
|
||||
from _scripts.generate_api_reference_links import update_markdown_with_imports
|
||||
from _scripts.notebook_convert import convert_notebook
|
||||
from _scripts.link_map import JS_LINK_MAP
|
||||
from _scripts.notebook_convert import convert_notebook
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig()
|
||||
@@ -187,7 +187,7 @@ def _resolve_cross_references(md_text: str, link_map: dict[str, str]) -> str:
|
||||
|
||||
|
||||
def _apply_conditional_rendering(md_text: str, target_language: str) -> str:
|
||||
if target_language not in {"python", "js"}:
|
||||
if target_language not in {"python", "js", "switcher"}:
|
||||
raise ValueError("target_language must be 'python' or 'js'")
|
||||
|
||||
pattern = re.compile(
|
||||
@@ -201,10 +201,16 @@ def _apply_conditional_rendering(md_text: str, target_language: str) -> str:
|
||||
language = match.group("language")
|
||||
content = match.group("content")
|
||||
|
||||
if language not in {"python", "js"}:
|
||||
if language not in {"python", "js", "switcher"}:
|
||||
# If the language is not supported, return the original block
|
||||
return match.group(0)
|
||||
|
||||
if target_language == "switcher":
|
||||
# Both Python and JavaScript blocks are wrapped in a tag that
|
||||
# allows the user to switch between them.
|
||||
standardized_language = "javascript" if language == "js" else "python"
|
||||
return f'<div class="lang-{standardized_language}">\n' + content + "\n</div>"
|
||||
|
||||
if language == target_language:
|
||||
return content
|
||||
|
||||
@@ -316,7 +322,7 @@ def _on_page_markdown_with_config(
|
||||
|
||||
# Apply conditional rendering for code blocks
|
||||
target_language = kwargs.get("target_language", "js")
|
||||
markdown = _apply_conditional_rendering(markdown, target_language)
|
||||
markdown = _apply_conditional_rendering(markdown, "switcher")
|
||||
if target_language == "js":
|
||||
markdown = _resolve_cross_references(markdown, JS_LINK_MAP)
|
||||
elif target_language == "python":
|
||||
|
||||
@@ -11,8 +11,6 @@ hide:
|
||||
|
||||
This guide shows you how to set up and use LangGraph's **prebuilt**, **reusable** components, which are designed to help you construct agentic systems quickly and reliably.
|
||||
|
||||
:::python
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you start this tutorial, ensure you have the following:
|
||||
@@ -230,244 +228,3 @@ response["structured_response"]
|
||||
- [Deploy your agent locally](../tutorials/langgraph-platform/local-server.md)
|
||||
- [Learn more about prebuilt agents](../agents/overview.md)
|
||||
- [LangGraph Platform quickstart](../cloud/quick_start.md)
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you start this tutorial, ensure you have the following:
|
||||
|
||||
- An [Anthropic](https://console.anthropic.com/settings/keys) API key
|
||||
|
||||
## 1. Install dependencies
|
||||
|
||||
If you haven't already, install LangGraph and LangChain:
|
||||
|
||||
```
|
||||
npm install langchain @langchain/langgraph @langchain/anthropic
|
||||
```
|
||||
|
||||
## 2. Create an agent
|
||||
|
||||
Use [`createReactAgent`][create_react_agent] to instantiate an agent:
|
||||
|
||||
```ts
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
const getWeather = tool( // (1)!
|
||||
async (input: { city: string }) => {
|
||||
return `It's always sunny in ${input.city}!`;
|
||||
},
|
||||
{
|
||||
name: "getWeather",
|
||||
schema: z.object({
|
||||
city: z.string().describe("The city to get the weather for"),
|
||||
}),
|
||||
description: "Get weather for a given city.",
|
||||
}
|
||||
);
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest"); // (2)!
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getWeather], // (3)!
|
||||
prompt: "You are a helpful assistant", // (4)!
|
||||
});
|
||||
|
||||
// Run the agent
|
||||
await agent.invoke({
|
||||
messages: [{ role: "user", content: "what is the weather in sf" }],
|
||||
});
|
||||
```
|
||||
|
||||
1. Define a tool for the agent to use. For more advanced tool usage and customization, check the [tools](./tools.md) page.
|
||||
2. Provide a language model for the agent to use. To learn more about configuring language models for the agents, check the [models](./models.md) page.
|
||||
3. Provide a list of tools for the model to use.
|
||||
4. Provide a system prompt (instructions) to the language model used by the agent.
|
||||
|
||||
## 3. Configure an LLM
|
||||
|
||||
Use [`initChatModel`](https://api.js.langchain.com/functions/langchain.chat_models_universal.initChatModel.html) to configure an LLM with specific parameters, such as temperature:
|
||||
|
||||
```ts
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
|
||||
// highlight-next-line
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest", {
|
||||
// highlight-next-line
|
||||
temperature: 0,
|
||||
});
|
||||
|
||||
const agent = createReactAgent({
|
||||
// highlight-next-line
|
||||
llm,
|
||||
tools: [getWeather],
|
||||
});
|
||||
```
|
||||
|
||||
See the [models](./models.md) page for more information on how to configure LLMs.
|
||||
|
||||
## 4. Add a custom prompt
|
||||
|
||||
Prompts instruct the LLM how to behave. They can be:
|
||||
|
||||
- **Static**: A string is interpreted as a **system message**
|
||||
- **Dynamic**: a list of messages generated at **runtime** based on input or configuration
|
||||
|
||||
=== "Static prompt"
|
||||
|
||||
Define a fixed prompt string or list of messages.
|
||||
|
||||
```ts
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getWeather],
|
||||
// A static prompt that never changes
|
||||
// highlight-next-line
|
||||
prompt: "Never answer questions about the weather.",
|
||||
});
|
||||
|
||||
await agent.invoke({
|
||||
messages: "what is the weather in sf",
|
||||
});
|
||||
```
|
||||
|
||||
=== "Dynamic prompt"
|
||||
|
||||
Define a function that returns a message list based on the agent's state and configuration:
|
||||
|
||||
```ts
|
||||
import { BaseMessageLike } from "@langchain/core/messages";
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { MessagesAnnotation } from "@langchain/langgraph";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
const prompt = (
|
||||
state: typeof MessagesAnnotation.State,
|
||||
config: RunnableConfig
|
||||
): BaseMessageLike[] => { // (1)!
|
||||
const userName = config.configurable?.userName;
|
||||
const systemMsg = `You are a helpful assistant. Address the user as ${userName}.`;
|
||||
return [{ role: "system", content: systemMsg }, ...state.messages];
|
||||
};
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getWeather],
|
||||
// highlight-next-line
|
||||
prompt,
|
||||
});
|
||||
|
||||
await agent.invoke(
|
||||
{ messages: [{ role: "user", content: "what is the weather in sf" }] },
|
||||
// highlight-next-line
|
||||
{ configurable: { userName: "John Smith" } }
|
||||
);
|
||||
```
|
||||
|
||||
1. Dynamic prompts allow including non-message [context](./context.md) when constructing an input to the LLM, such as:
|
||||
|
||||
- Information passed at runtime, like a `userId` or API credentials (using `config`).
|
||||
- Internal agent state updated during a multi-step reasoning process (using `state`).
|
||||
|
||||
Dynamic prompts can be defined as functions that take `state` and `config` and return a list of messages to send to the LLM.
|
||||
|
||||
For more information, see [Context](./context.md).
|
||||
|
||||
## 5. Add memory
|
||||
|
||||
To allow multi-turn conversations with an agent, you need to enable [persistence](../concepts/persistence.md) by providing a `checkpointer` when creating an agent. At runtime you need to provide a config containing `thread_id` — a unique identifier for the conversation (session):
|
||||
|
||||
```ts
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { MemorySaver } from "@langchain/langgraph-checkpoint";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
|
||||
// highlight-next-line
|
||||
const checkpointer = new MemorySaver();
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getWeather],
|
||||
// highlight-next-line
|
||||
checkpointer, // (1)!
|
||||
});
|
||||
|
||||
// Run the agent
|
||||
// highlight-next-line
|
||||
const config = { configurable: { thread_id: "1" } };
|
||||
const sfResponse = await agent.invoke(
|
||||
{ messages: [{ role: "user", content: "what is the weather in sf" }] },
|
||||
config // (2)!
|
||||
);
|
||||
const nyResponse = await agent.invoke(
|
||||
{ messages: [{ role: "user", content: "what about new york?" }] },
|
||||
config
|
||||
);
|
||||
```
|
||||
|
||||
1. `checkpointer` allows the agent to store its state at every step in the tool calling loop. This enables [short-term memory](./memory.md#short-term-memory) and [human-in-the-loop](./human-in-the-loop.md) capabilities.
|
||||
2. Pass configuration with `thread_id` to be able to resume the same conversation on future agent invocations.
|
||||
|
||||
When you enable the checkpointer, it stores agent state at every step in the provided checkpointer database (or in memory, if using `InMemorySaver`).
|
||||
|
||||
Note that in the above example, when the agent is invoked the second time with the same `thread_id`, the original message history from the first conversation is automatically included, together with the new user input.
|
||||
|
||||
For more information, see [Memory](./memory.md).
|
||||
|
||||
## 6. Configure structured output
|
||||
|
||||
To produce structured responses conforming to a schema, use the `responseFormat` parameter. The schema can be defined with a `zod` schema. The result will be accessible via the `structuredResponse` field.
|
||||
|
||||
```ts
|
||||
import { z } from "zod";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
|
||||
const WeatherResponse = z.object({
|
||||
conditions: z.string(),
|
||||
});
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getWeather],
|
||||
// highlight-next-line
|
||||
responseFormat: WeatherResponse, // (1)!
|
||||
});
|
||||
|
||||
const response = await agent.invoke({
|
||||
messages: [{ role: "user", content: "what is the weather in sf" }],
|
||||
});
|
||||
// highlight-next-line
|
||||
response.structuredResponse;
|
||||
```
|
||||
|
||||
1. When `responseFormat` is provided, a separate step is added at the end of the agent loop: agent message history is passed to an LLM with structured output to generate a structured response.
|
||||
|
||||
To provide a system prompt to this LLM, use an object `{ prompt, schema }`, e.g., `responseFormat: { prompt, schema: WeatherResponse }`.
|
||||
|
||||
!!! Note "LLM post-processing"
|
||||
|
||||
Structured output requires an additional call to the LLM to format the response according to the schema.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Deploy your agent locally](../tutorials/langgraph-platform/local-server.md)
|
||||
- [Learn more about prebuilt agents](../agents/overview.md)
|
||||
- [LangGraph Platform quickstart](../cloud/quick_start.md)
|
||||
|
||||
:::
|
||||
@@ -43,8 +43,6 @@ when you have values that don't change mid-run.
|
||||
Specify configuration using a key called **"configurable"** which is reserved
|
||||
for this purpose:
|
||||
|
||||
|
||||
:::python
|
||||
```python
|
||||
agent.invoke(
|
||||
{"messages": [{"role": "user", "content": "hi!"}]},
|
||||
@@ -52,23 +50,11 @@ agent.invoke(
|
||||
config={"configurable": {"user_id": "user_123"}}
|
||||
)
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```ts
|
||||
await agent.invoke(
|
||||
{ messages: "hi!" },
|
||||
// highlight-next-line
|
||||
{ configurable: { userId: "user_123" } }
|
||||
)
|
||||
```
|
||||
:::
|
||||
|
||||
### State (mutable context)
|
||||
|
||||
State acts as short-term memory during a run. It holds dynamic data that can evolve during execution, such as values derived from tools or LLM outputs.
|
||||
|
||||
:::python
|
||||
```python
|
||||
class CustomState(AgentState):
|
||||
# highlight-next-line
|
||||
@@ -85,29 +71,6 @@ agent.invoke({
|
||||
"user_name": "Jane"
|
||||
})
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
```ts
|
||||
const CustomState = Annotation.Root({
|
||||
...MessagesAnnotation.spec,
|
||||
userName: Annotation<string>,
|
||||
});
|
||||
|
||||
const agent = createReactAgent({
|
||||
// Other agent parameters...
|
||||
// highlight-next-line
|
||||
stateSchema: CustomState,
|
||||
})
|
||||
|
||||
await agent.invoke(
|
||||
// highlight-next-line
|
||||
{ messages: "hi!", userName: "Jane" }
|
||||
)
|
||||
```
|
||||
:::
|
||||
|
||||
|
||||
|
||||
!!! tip "Turning on memory"
|
||||
|
||||
@@ -130,8 +93,6 @@ Common use cases:
|
||||
- Role or goal customization
|
||||
- Conditional behavior (e.g., user is admin)
|
||||
|
||||
:::python
|
||||
|
||||
=== "Using config"
|
||||
|
||||
```python
|
||||
@@ -201,90 +162,8 @@ Common use cases:
|
||||
})
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
=== "Using config"
|
||||
|
||||
```ts
|
||||
import { BaseMessageLike } from "@langchain/core/messages";
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { MessagesAnnotation } from "@langchain/langgraph";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
const prompt = (
|
||||
state: typeof MessagesAnnotation.State,
|
||||
// highlight-next-line
|
||||
config: RunnableConfig
|
||||
): BaseMessageLike[] => {
|
||||
// highlight-next-line
|
||||
const userName = config.configurable?.userName;
|
||||
const systemMsg = `You are a helpful assistant. Address the user as ${userName}.`;
|
||||
return [{ role: "system", content: systemMsg }, ...state.messages];
|
||||
};
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getWeather],
|
||||
// highlight-next-line
|
||||
prompt
|
||||
});
|
||||
|
||||
await agent.invoke(
|
||||
{ messages: "hi!" },
|
||||
// highlight-next-line
|
||||
{ configurable: { userName: "John Smith" } }
|
||||
);
|
||||
```
|
||||
|
||||
=== "Using state"
|
||||
|
||||
```ts
|
||||
import { BaseMessageLike } from "@langchain/core/messages";
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { Annotation, MessagesAnnotation } from "@langchain/langgraph";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
const CustomState = Annotation.Root({
|
||||
...MessagesAnnotation.spec,
|
||||
// highlight-next-line
|
||||
userName: Annotation<string>,
|
||||
});
|
||||
|
||||
const prompt = (
|
||||
// highlight-next-line
|
||||
state: typeof CustomState.State,
|
||||
): BaseMessageLike[] => {
|
||||
// highlight-next-line
|
||||
const userName = state.userName;
|
||||
const systemMsg = `You are a helpful assistant. Address the user as ${userName}.`;
|
||||
return [{ role: "system", content: systemMsg }, ...state.messages];
|
||||
};
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getWeather],
|
||||
// highlight-next-line
|
||||
prompt,
|
||||
// highlight-next-line
|
||||
stateSchema: CustomState,
|
||||
});
|
||||
|
||||
await agent.invoke(
|
||||
// highlight-next-line
|
||||
{ messages: "hi!", userName: "John Smith" },
|
||||
);
|
||||
```
|
||||
:::
|
||||
|
||||
|
||||
## Accessing Context in Tools { #tools }
|
||||
|
||||
:::python
|
||||
Tools can access context through special parameter **annotations**.
|
||||
|
||||
* Use `RunnableConfig` for config access
|
||||
@@ -351,167 +230,7 @@ Tools can access context through special parameter **annotations**.
|
||||
"user_id": "user_123"
|
||||
})
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
Tools can access context through:
|
||||
|
||||
* Use `RunnableConfig` for config access
|
||||
* Use `getCurrentTaskInput()` for agent state
|
||||
|
||||
=== "Using config"
|
||||
|
||||
```ts
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
const getUserInfo = tool(
|
||||
async (input: Record<string, any>, config: RunnableConfig) => {
|
||||
// highlight-next-line
|
||||
const userId = config.configurable?.userId;
|
||||
return userId === "user_123" ? "User is John Smith" : "Unknown user";
|
||||
},
|
||||
{
|
||||
name: "get_user_info",
|
||||
description: "Look up user info.",
|
||||
schema: z.object({}),
|
||||
}
|
||||
);
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getUserInfo],
|
||||
});
|
||||
|
||||
await agent.invoke(
|
||||
{ messages: "look up user information" },
|
||||
// highlight-next-line
|
||||
{ configurable: { userId: "user_123" } }
|
||||
);
|
||||
```
|
||||
|
||||
=== "Using state"
|
||||
|
||||
```ts
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { Annotation, MessagesAnnotation, getCurrentTaskInput } from "@langchain/langgraph";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
const CustomState = Annotation.Root({
|
||||
...MessagesAnnotation.spec,
|
||||
// highlight-next-line
|
||||
userId: Annotation<string>(),
|
||||
});
|
||||
|
||||
const getUserInfo = tool(
|
||||
async (
|
||||
input: Record<string, any>,
|
||||
) => {
|
||||
// highlight-next-line
|
||||
const state = getCurrentTaskInput() as typeof CustomState.State;
|
||||
// highlight-next-line
|
||||
const userId = state.userId;
|
||||
return userId === "user_123" ? "User is John Smith" : "Unknown user";
|
||||
},
|
||||
{
|
||||
name: "get_user_info",
|
||||
description: "Look up user info.",
|
||||
schema: z.object({})
|
||||
}
|
||||
);
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getUserInfo],
|
||||
// highlight-next-line
|
||||
stateSchema: CustomState,
|
||||
});
|
||||
|
||||
await agent.invoke(
|
||||
// highlight-next-line
|
||||
{ messages: "look up user information", userId: "user_123" }
|
||||
);
|
||||
```
|
||||
:::
|
||||
|
||||
### Update Context from Tools
|
||||
|
||||
:::python
|
||||
Tools can update agent's context (state and long-term memory) during execution. This is useful for persisting intermediate results or making information accessible to subsequent tools or prompts. See [Memory](./memory.md#read-short-term) guide for more information.
|
||||
:::
|
||||
|
||||
:::js
|
||||
Tools can modify the agent's state during execution. This is useful for persisting intermediate results or making information accessible to subsequent tools or prompts.
|
||||
|
||||
```ts
|
||||
import { Annotation, MessagesAnnotation, LangGraphRunnableConfig, Command } from "@langchain/langgraph";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
import { ToolMessage } from "@langchain/core/messages";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
const CustomState = Annotation.Root({
|
||||
...MessagesAnnotation.spec,
|
||||
// highlight-next-line
|
||||
userName: Annotation<string>(), // Will be updated by the tool
|
||||
});
|
||||
|
||||
const getUserInfo = tool(
|
||||
async (
|
||||
_input: Record<string, never>,
|
||||
config: LangGraphRunnableConfig
|
||||
): Promise<Command> => {
|
||||
const userId = config.configurable?.userId;
|
||||
if (!userId) {
|
||||
throw new Error("Please provide a user id in config.configurable");
|
||||
}
|
||||
|
||||
const toolCallId = config.toolCall?.id;
|
||||
|
||||
const name = userId === "user_123" ? "John Smith" : "Unknown user";
|
||||
// Return command to update state
|
||||
return new Command({
|
||||
update: {
|
||||
// highlight-next-line
|
||||
userName: name,
|
||||
// Update the message history
|
||||
// highlight-next-line
|
||||
messages: [
|
||||
new ToolMessage({
|
||||
content: "Successfully looked up user information",
|
||||
tool_call_id: toolCallId,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
name: "get_user_info",
|
||||
description: "Look up user information.",
|
||||
schema: z.object({}),
|
||||
}
|
||||
);
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [getUserInfo],
|
||||
// highlight-next-line
|
||||
stateSchema: CustomState,
|
||||
});
|
||||
|
||||
await agent.invoke(
|
||||
{ messages: "look up user information" },
|
||||
// highlight-next-line
|
||||
{ configurable: { userId: "user_123" } }
|
||||
);
|
||||
```
|
||||
:::
|
||||
@@ -27,8 +27,6 @@ A human can review and edit the output from the agent before proceeding. This is
|
||||
</figure>
|
||||
|
||||
|
||||
:::python
|
||||
|
||||
## Review tool calls
|
||||
|
||||
To add a human approval step to a tool:
|
||||
@@ -36,7 +34,6 @@ To add a human approval step to a tool:
|
||||
1. Use `interrupt()` in the tool to pause execution.
|
||||
2. Resume with a `Command(resume=...)` to continue based on human input.
|
||||
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.types import interrupt
|
||||
@@ -236,110 +233,6 @@ for chunk in agent.stream(
|
||||
print("\n")
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
## Review tool calls
|
||||
|
||||
To add a human approval step to a tool:
|
||||
|
||||
1. Use `interrupt()` in the tool to pause execution.
|
||||
2. Resume with a `Command({ resume: ... })` to continue based on human input.
|
||||
|
||||
```ts
|
||||
import { MemorySaver } from "@langchain/langgraph-checkpoint";
|
||||
import { interrupt } from "@langchain/langgraph";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
// An example of a sensitive tool that requires human review / approval
|
||||
const bookHotel = tool(
|
||||
async (input: { hotelName: string; }) => {
|
||||
let hotelName = input.hotelName;
|
||||
// highlight-next-line
|
||||
const response = interrupt( // (1)!
|
||||
`Trying to call \`book_hotel\` with args {'hotel_name': ${hotelName}}. ` +
|
||||
`Please approve or suggest edits.`
|
||||
)
|
||||
if (response.type === "accept") {
|
||||
// proceed to execute the tool logic
|
||||
} else if (response.type === "edit") {
|
||||
hotelName = response.args["hotel_name"]
|
||||
} else {
|
||||
throw new Error(`Unknown response type: ${response.type}`)
|
||||
}
|
||||
return `Successfully booked a stay at ${hotelName}.`;
|
||||
},
|
||||
{
|
||||
name: "bookHotel",
|
||||
schema: z.object({
|
||||
hotelName: z.string().describe("Hotel to book"),
|
||||
}),
|
||||
description: "Book a hotel.",
|
||||
}
|
||||
);
|
||||
|
||||
// highlight-next-line
|
||||
const checkpointer = new MemorySaver(); // (2)!
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [bookHotel],
|
||||
// highlight-next-line
|
||||
checkpointer // (3)!
|
||||
});
|
||||
```
|
||||
|
||||
1. The [`interrupt` function][langgraph.types.interrupt] pauses the agent graph at a specific node. In this case, we call `interrupt()` at the beginning of the tool function, which pauses the graph at the node that executes the tool. The information inside `interrupt()` (e.g., tool calls) can be presented to a human, and the graph can be resumed with the user input (tool call approval, edit or feedback).
|
||||
2. The `InMemorySaver` is used to store the agent state at every step in the tool calling loop. This enables [short-term memory](./memory.md#short-term-memory) and [human-in-the-loop](./human-in-the-loop.md) capabilities. In this example, we use `InMemorySaver` to store the agent state in memory. In a production application, the agent state will be stored in a database.
|
||||
3. Initialize the agent with the `checkpointer`.
|
||||
|
||||
Run the agent with the `stream()` method, passing the `config` object to specify the thread ID. This allows the agent to resume the same conversation on future invocations.
|
||||
|
||||
```ts
|
||||
const config = {
|
||||
configurable: {
|
||||
// highlight-next-line
|
||||
"thread_id": "1"
|
||||
}
|
||||
}
|
||||
|
||||
for await (const chunk of await agent.stream(
|
||||
{ messages: "book a stay at McKittrick hotel" },
|
||||
// highlight-next-line
|
||||
config
|
||||
)) {
|
||||
console.log(chunk);
|
||||
console.log("\n");
|
||||
};
|
||||
```
|
||||
|
||||
> You should see that the agent runs until it reaches the `interrupt()` call, at which point it pauses and waits for human input.
|
||||
|
||||
Resume the agent with a `Command({ resume: ... })` to continue based on human input.
|
||||
|
||||
```ts
|
||||
import { Command } from "@langchain/langgraph";
|
||||
|
||||
for await (const chunk of await agent.stream(
|
||||
new Command({ resume: { type: "accept" } }), // (1)!
|
||||
// new Command({ resume: { type: "edit", args: { "hotel_name": "McKittrick Hotel" } } }),
|
||||
// highlight-next-line
|
||||
config
|
||||
)) {
|
||||
console.log(chunk);
|
||||
console.log("\n");
|
||||
};
|
||||
```
|
||||
|
||||
1. The [`interrupt` function][langgraph.types.interrupt] is used in conjunction with the [`Command`][langgraph.types.Command] object to resume the graph with a value provided by the human.
|
||||
|
||||
:::
|
||||
|
||||
## Additional resources
|
||||
|
||||
* [Human-in-the-loop in LangGraph](../concepts/human_in_the_loop.md)
|
||||
|
||||
+1
-54
@@ -13,8 +13,6 @@ hide:
|
||||
|
||||

|
||||
|
||||
:::python
|
||||
|
||||
Install the `langchain-mcp-adapters` library to use MCP tools in LangGraph:
|
||||
|
||||
```bash
|
||||
@@ -60,57 +58,6 @@ weather_response = await agent.ainvoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in nyc?"}]}
|
||||
)
|
||||
```
|
||||
:::
|
||||
|
||||
:::js
|
||||
Install the `@langchain/mcp-adapters` library to use MCP tools in LangGraph:
|
||||
```bash
|
||||
npm install @langchain/mcp-adapters
|
||||
```
|
||||
|
||||
## Use MCP tools
|
||||
|
||||
The `@langchain/mcp-adapters` package enables agents to use tools defined across one or more MCP servers.
|
||||
|
||||
```ts
|
||||
// highlight-next-line
|
||||
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
// highlight-next-line
|
||||
const client = new MultiServerMCPClient({
|
||||
mcpServers: {
|
||||
"math": {
|
||||
command: "python",
|
||||
// Replace with absolute path to your math_server.py file
|
||||
args: ["/path/to/math_server.py"],
|
||||
transport: "stdio",
|
||||
},
|
||||
"weather": {
|
||||
// Ensure your start your weather server on port 8000
|
||||
url: "http://localhost:8000/sse",
|
||||
transport: "sse",
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const llm = await initChatModel("anthropic:claude-3-7-sonnet-latest");
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
// highlight-next-line
|
||||
tools: await client.getTools()
|
||||
});
|
||||
|
||||
const mathResponse = await agent.invoke(
|
||||
{ messages: [ { role: "user", content: "what's (3 + 5) x 12?" } ] }
|
||||
);
|
||||
const weatherResponse = await agent.invoke(
|
||||
{ messages: [ { role: "user", content: "what is the weather in nyc?" } ] }
|
||||
);
|
||||
await client.close();
|
||||
```
|
||||
:::
|
||||
|
||||
## Custom MCP servers
|
||||
|
||||
@@ -159,4 +106,4 @@ if __name__ == "__main__":
|
||||
## Additional resources
|
||||
|
||||
- [MCP documentation](https://modelcontextprotocol.io/introduction)
|
||||
- [MCP Transport documentation](https://modelcontextprotocol.io/docs/concepts/transports)
|
||||
- [MCP Transport documentation](https://modelcontextprotocol.io/docs/concepts/transports)
|
||||
|
||||
@@ -40,8 +40,6 @@ LangGraph comes with a set of prebuilt components that implement common agent be
|
||||
|
||||
Using LangGraph for agent development allows you to focus on your application's logic and behavior, instead of building and maintaining the supporting infrastructure for state, memory, and human feedback.
|
||||
|
||||
|
||||
:::python
|
||||
## Package ecosystem
|
||||
|
||||
The high-level components are organized into several packages, each with a specific focus.
|
||||
@@ -191,161 +189,3 @@ function initializeWidget() {
|
||||
window.addEventListener("DOMContentLoaded", initializeWidget);
|
||||
document$.subscribe(initializeWidget);
|
||||
</script>
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
## Package ecosystem
|
||||
|
||||
The high-level components are organized into several packages, each with a specific focus.
|
||||
|
||||
| Package | Description | Installation |
|
||||
|--------------------------|-----------------------------------------------------------------------------|----------------------------------------------------|
|
||||
| `langgraph` | Prebuilt components to [**create agents**](./agents.md) | `npm install @langchain/langgraph @langchain/core` |
|
||||
| `langgraph-supervisor` | Tools for building [**supervisor**](./multi-agent.md#supervisor) agents | `npm install @langchain/langgraph-supervisor` |
|
||||
| `langgraph-swarm` | Tools for building a [**swarm**](./multi-agent.md#swarm) multi-agent system | `npm install @langchain/langgraph-swarm` |
|
||||
| `langchain-mcp-adapters` | Interfaces to [**MCP servers**](./mcp.md) for tool and resource integration | `npm install @langchain/mcp-adapters` |
|
||||
| `agentevals` | Utilities to [**evaluate agent performance**](./evals.md) | `npm install agentevals` |
|
||||
|
||||
## Visualize an agent graph
|
||||
|
||||
Use the following tool to visualize the graph generated by [`createReactAgent`][create_react_agent] and to view an outline of the corresponding code. It allows you to explore the infrastructure of the agent as defined by the presence of:
|
||||
|
||||
- [`tools`](./tools.md): A list of tools (functions, APIs, or other callable objects) that the agent can use to perform tasks.
|
||||
- `preModelHook`: A function that is called before the model is invoked. It can be used to condense messages or perform other preprocessing tasks.
|
||||
- `postModelHook`: A function that is called after the model is invoked. It can be used to implement guardrails, human-in-the-loop flows, or other postprocessing tasks.
|
||||
- [`responseFormat`](./agents.md#6-configure-structured-output): A data structure used to constrain the type of the final output (via Zod schemas).
|
||||
|
||||
<div class="agent-layout">
|
||||
<div class="agent-graph-features-container">
|
||||
<div class="agent-graph-features">
|
||||
<h3 class="agent-section-title">Features</h3>
|
||||
<label><input type="checkbox" id="tools" checked> <code>tools</code></label>
|
||||
<label><input type="checkbox" id="preModelHook"> <code>preModelHook</code></label>
|
||||
<label><input type="checkbox" id="postModelHook"> <code>postModelHook</code></label>
|
||||
<label><input type="checkbox" id="responseFormat"> <code>responseFormat</code></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="agent-graph-container">
|
||||
<h3 class="agent-section-title">Graph</h3>
|
||||
<img id="agent-graph-img" src="../assets/react_agent_graphs/0001.svg" alt="graph image" style="max-width: 100%;"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
The following code snippet shows how to create the above agent (and underlying graph) with [`createReactAgent`][create_react_agent]:
|
||||
|
||||
```typescript
|
||||
|
||||
<div class="language-typescript">
|
||||
<pre><code id="agent-code" class="language-typescript"></code></pre>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function getCheckedValue(id) {
|
||||
return document.getElementById(id).checked ? "1" : "0";
|
||||
}
|
||||
|
||||
function getKey() {
|
||||
return [
|
||||
getCheckedValue("responseFormat"),
|
||||
getCheckedValue("postModelHook"),
|
||||
getCheckedValue("preModelHook"),
|
||||
getCheckedValue("tools")
|
||||
].join("");
|
||||
}
|
||||
|
||||
function dedent(strings, ...values) {
|
||||
const str = String.raw({ raw: strings }, ...values)
|
||||
const [space] = str.split("\n").filter(Boolean).at(0).match(/^(\s*)/)
|
||||
const spaceLen = space.length
|
||||
return str.split("\n").map(line => line.slice(spaceLen)).join("\n").trim()
|
||||
}
|
||||
|
||||
Object.assign(dedent, {
|
||||
offset: (size) => (strings, ...values) => {
|
||||
return dedent(strings, ...values).split("\n").map(line => " ".repeat(size) + line).join("\n")
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
function generateCodeSnippet({ tools, pre, post, response }) {
|
||||
const lines = []
|
||||
|
||||
lines.push(dedent`
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
`)
|
||||
|
||||
if (tools) lines.push(`import { tool } from "@langchain/core/tools";`);
|
||||
if (response || tools) lines.push(`import { z } from "zod";`);
|
||||
|
||||
lines.push("", dedent`
|
||||
const agent = createReactAgent({
|
||||
llm: new ChatOpenAI({ model: "o4-mini" }),
|
||||
`)
|
||||
|
||||
if (tools) {
|
||||
lines.push(dedent.offset(2)`
|
||||
tools: [
|
||||
tool(() => "Sample tool output", {
|
||||
name: "sampleTool",
|
||||
schema: z.object({}),
|
||||
}),
|
||||
],
|
||||
`)
|
||||
}
|
||||
|
||||
if (pre) {
|
||||
lines.push(dedent.offset(2)`
|
||||
preModelHook: (state) => ({ llmInputMessages: state.messages }),
|
||||
`)
|
||||
}
|
||||
|
||||
if (post) {
|
||||
lines.push(dedent.offset(2)`
|
||||
postModelHook: (state) => state,
|
||||
`)
|
||||
}
|
||||
|
||||
if (response) {
|
||||
lines.push(dedent.offset(2)`
|
||||
responseFormat: z.object({ result: z.string() }),
|
||||
`)
|
||||
}
|
||||
|
||||
lines.push(`});`);
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function render() {
|
||||
const key = getKey();
|
||||
document.getElementById("agent-graph-img").src = `../assets/react_agent_graphs/${key}.svg`;
|
||||
|
||||
const state = {
|
||||
tools: document.getElementById("tools").checked,
|
||||
pre: document.getElementById("preModelHook").checked,
|
||||
post: document.getElementById("postModelHook").checked,
|
||||
response: document.getElementById("responseFormat").checked
|
||||
};
|
||||
|
||||
document.getElementById("agent-code").textContent = generateCodeSnippet(state);
|
||||
}
|
||||
|
||||
function initializeWidget() {
|
||||
render(); // no need for `await` here
|
||||
document.querySelectorAll(".agent-graph-features input").forEach((input) => {
|
||||
input.addEventListener("change", render);
|
||||
});
|
||||
}
|
||||
|
||||
// Init for both full reload and SPA nav (used by MkDocs Material)
|
||||
window.addEventListener("DOMContentLoaded", initializeWidget);
|
||||
document$.subscribe(initializeWidget);
|
||||
</script>
|
||||
|
||||
:::
|
||||
@@ -23,7 +23,6 @@ Then, navigate to [Agent Chat UI](https://agentchat.vercel.app), or clone the re
|
||||
|
||||
UI has out-of-box support for rendering tool calls, and tool result messages. To customize what messages are shown, see the [Hiding Messages in the Chat](https://github.com/langchain-ai/agent-chat-ui?tab=readme-ov-file#hiding-messages-in-the-chat) section in the Agent Chat UI documentation.
|
||||
|
||||
:::python
|
||||
## Add human-in-the-loop
|
||||
|
||||
Agent Chat UI has full support for [human-in-the-loop](../concepts/human_in_the_loop.md) workflows. To try it out, replace the agent code in `src/agent/graph.py` (from the [deployment](./deployment.md) guide) with this [agent implementation](./human-in-the-loop.md#using-with-agent-inbox):
|
||||
@@ -33,7 +32,6 @@ Agent Chat UI has full support for [human-in-the-loop](../concepts/human_in_the_
|
||||
!!! Important
|
||||
|
||||
Agent Chat UI works best if your LangGraph agent interrupts using the [`HumanInterrupt` schema][langgraph.prebuilt.interrupt.HumanInterrupt]. If you do not use that schema, the Agent Chat UI will be able to render the input passed to the `interrupt` function, but it will not have full support for resuming your graph.
|
||||
:::
|
||||
|
||||
## Generative UI
|
||||
|
||||
|
||||
+3
-1
@@ -398,4 +398,6 @@ extra_css:
|
||||
- stylesheets/logos.css
|
||||
- stylesheets/sticky_navigation.css
|
||||
- stylesheets/agent_graph_widget.css
|
||||
|
||||
- language-switcher.css
|
||||
extra_javascript:
|
||||
- language-switcher.js
|
||||
@@ -0,0 +1,41 @@
|
||||
.lang-python,
|
||||
.lang-javascript {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.language-switcher-global {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Style the select to match the header */
|
||||
.language-switcher-global select {
|
||||
appearance: none;
|
||||
font: inherit;
|
||||
border: none;
|
||||
padding: 0.25rem 0.6rem;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/* Hover/focus effect */
|
||||
.language-switcher-global select:hover,
|
||||
.language-switcher-global select:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Theme-specific overrides */
|
||||
html[data-md-color-scheme="default"] .language-switcher-global select,
|
||||
html[data-md-color-scheme="default"] .language-switcher-global option {
|
||||
color: #333;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
html[data-md-color-scheme="slate"] .language-switcher-global select,
|
||||
html[data-md-color-scheme="slate"] .language-switcher-global option {
|
||||
color: #eee;
|
||||
background-color: transparent;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
function applyLanguageSwitching() {
|
||||
const selector = document.getElementById("global-language-selector");
|
||||
|
||||
const langBlocks = {
|
||||
python: document.querySelectorAll(".lang-python"),
|
||||
javascript: document.querySelectorAll(".lang-javascript"),
|
||||
};
|
||||
|
||||
const setLanguage = (lang) => {
|
||||
for (const [key, blocks] of Object.entries(langBlocks)) {
|
||||
blocks.forEach((block) => {
|
||||
block.style.display = key === lang ? "block" : "none";
|
||||
});
|
||||
}
|
||||
localStorage.setItem("preferredLang", lang);
|
||||
};
|
||||
|
||||
const saved = localStorage.getItem("preferredLang") || "python";
|
||||
|
||||
if (selector) {
|
||||
selector.value = saved;
|
||||
selector.addEventListener("change", (e) => setLanguage(e.target.value));
|
||||
}
|
||||
|
||||
setLanguage(saved);
|
||||
}
|
||||
|
||||
// Run on initial load
|
||||
document.addEventListener("DOMContentLoaded", applyLanguageSwitching);
|
||||
|
||||
// Re-run after client-side navigation (MkDocs Material)
|
||||
document.addEventListener("pjax:success", applyLanguageSwitching);
|
||||
|
||||
// Optional: observe DOM changes (e.g., for late-loaded content)
|
||||
if (window.MutationObserver) {
|
||||
const observer = new MutationObserver(() => applyLanguageSwitching());
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{#-
|
||||
This file was automatically generated - do not edit
|
||||
-#}
|
||||
{% set class = "md-header" %}
|
||||
{% if "navigation.tabs.sticky" in features %}
|
||||
{% set class = class ~ " md-header--shadow md-header--lifted" %}
|
||||
{% elif "navigation.tabs" not in features %}
|
||||
{% set class = class ~ " md-header--shadow" %}
|
||||
{% endif %}
|
||||
<header class="{{ class }}" data-md-component="header">
|
||||
<nav class="md-header__inner md-grid" aria-label="{{ lang.t('header') }}">
|
||||
<a href="{{ config.extra.homepage | d(nav.homepage.url, true) | url }}" title="{{ config.site_name | e }}" class="md-header__button md-logo" aria-label="{{ config.site_name }}" data-md-component="logo">
|
||||
{% include "partials/logo.html" %}
|
||||
</a>
|
||||
<label class="md-header__button md-icon" for="__drawer">
|
||||
{% set icon = config.theme.icon.menu or "material/menu" %}
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
</label>
|
||||
<div class="md-header__title" data-md-component="header-title">
|
||||
<div class="md-header__ellipsis">
|
||||
<div class="md-header__topic">
|
||||
<span class="md-ellipsis">
|
||||
{{ config.site_name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="md-header__topic" data-md-component="header-topic">
|
||||
<span class="md-ellipsis">
|
||||
{% if page.meta and page.meta.title %}
|
||||
{{ page.meta.title }}
|
||||
{% else %}
|
||||
{{ page.title }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if config.theme.palette %}
|
||||
{% if not config.theme.palette is mapping %}
|
||||
{% include "partials/palette.html" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if not config.theme.palette is mapping %}
|
||||
{% include "partials/javascripts/palette.html" %}
|
||||
{% endif %}
|
||||
{% if config.extra.alternate %}
|
||||
{% include "partials/alternate.html" %}
|
||||
{% endif %}
|
||||
{% if "material/search" in config.plugins %}
|
||||
{% set search = config.plugins["material/search"] | attr("config") %}
|
||||
{% if search.enabled %}
|
||||
<label class="md-header__button md-icon" for="__search">
|
||||
{% set icon = config.theme.icon.search or "material/magnify" %}
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
</label>
|
||||
{% include "partials/search.html" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if config.repo_url %}
|
||||
<div class="md-header__source">
|
||||
{% include "partials/source.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include "partials/language-toggle.html" %}
|
||||
</nav>
|
||||
{% if "navigation.tabs.sticky" in features %}
|
||||
{% if "navigation.tabs" in features %}
|
||||
{% include "partials/tabs.html" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</header>
|
||||
@@ -0,0 +1,6 @@
|
||||
<div class="md-header__button language-switcher-global" title="Select Language">
|
||||
<select id="global-language-selector" aria-label="Select Language">
|
||||
<option value="python">🐍 Python</option>
|
||||
<option value="javascript">⚡️ JavaScript</option>
|
||||
</select>
|
||||
</div>
|
||||
Reference in New Issue
Block a user