diff --git a/docs/docs/cloud/deployment/setup_javascript.md b/docs/docs/cloud/deployment/setup_javascript.md new file mode 100644 index 000000000..fa4c7b823 --- /dev/null +++ b/docs/docs/cloud/deployment/setup_javascript.md @@ -0,0 +1,200 @@ +# How to Set Up a LangGraph.js Application for Deployment + +A [LangGraph.js](https://langchain-ai.github.io/langgraphjs/) application must be configured with a [LangGraph API configuration file](../reference/cli.md#configuration-file) in order to be deployed to LangGraph Cloud (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph.js application for deployment using `package.json` to specify project dependencies. + +This walkthrough is based on [this repository](https://github.com/langchain-ai/langgraphjs-studio-starter), which you can play around with to learn more about how to setup your LangGraph application for deployment. + +The final repo structure will look something like this: + +```bash +my-app/ +├── src # all project code lies within here +│ ├── utils # optional utilities for your graph +│ │ ├── tools.ts # tools for your graph +│ │ ├── nodes.ts # node functions for you graph +│ │ └── state.ts # state definition of your graph +│   └── agent.ts # code for constructing your graph +├── package.json # package dependencies +├── .env # environment variables +└── langgraph.json # configuration file for LangGraph +``` + +After each step, an example file directory is provided to demonstrate how code can be organized. + +## Specify Dependencies + +Dependencies can be specified in a `package.json`. If none of these files is created, then dependencies can be specified later in the [LangGraph API configuration file](#create-langgraph-api-config). + +Example `package.json` file: + +```json +{ + "name": "langgraphjs-studio-starter", + "packageManager": "yarn@1.22.22", + "dependencies": { + "@langchain/community": "^0.2.31", + "@langchain/core": "^0.2.31", + "@langchain/langgraph": "^0.2.0", + "@langchain/openai": "^0.2.8" + } +} +``` + +Example file directory: + +```bash +my-app/ +└── package.json # package dependencies +``` + +## Specify Environment Variables + +Environment variables can optionally be specified in a file (e.g. `.env`). See the [Environment Variables reference](../reference/env_var.md) to configure additional variables for a deployment. + +Example `.env` file: + +``` +MY_ENV_VAR_1=foo +MY_ENV_VAR_2=bar +OPENAI_API_KEY=key +TAVILY_API_KEY=key_2 +``` + +Example file directory: + +```bash +my-app/ +├── package.json +└── .env # environment variables +``` + +## Define Graphs + +Implement your graphs! Graphs can be defined in a single file or multiple files. Make note of the variable names of each compiled graph to be included in the LangGraph application. The variable names will be used later when creating the [LangGraph API configuration file](../reference/cli.md#configuration-file). + +Here is an example `agent.ts`: + +```ts +import type { AIMessage } from "@langchain/core/messages"; +import { TavilySearchResults } from "@langchain/community/tools/tavily_search"; +import { ChatOpenAI } from "@langchain/openai"; + +import { MessagesAnnotation, StateGraph } from "@langchain/langgraph"; +import { ToolNode } from "@langchain/langgraph/prebuilt"; + +const tools = [ + new TavilySearchResults({ maxResults: 3, }), +]; + +// Define the function that calls the model +async function callModel( + state: typeof MessagesAnnotation.State, +) { + /** + * Call the LLM powering our agent. + * Feel free to customize the prompt, model, and other logic! + */ + const model = new ChatOpenAI({ + model: "gpt-4o", + }).bindTools(tools); + + const response = await model.invoke([ + { + role: "system", + content: `You are a helpful assistant. The current date is ${new Date().getTime()}.` + }, + ...state.messages + ]); + + // MessagesAnnotation supports returning a single message or array of messages + return { messages: response }; +} + +// Define the function that determines whether to continue or not +function routeModelOutput(state: typeof MessagesAnnotation.State) { + const messages = state.messages; + const lastMessage: AIMessage = messages[messages.length - 1]; + // If the LLM is invoking tools, route there. + if ((lastMessage?.tool_calls?.length ?? 0) > 0) { + return "tools"; + } + // Otherwise end the graph. + return "__end__"; +} + +// Define a new graph. +// See https://langchain-ai.github.io/langgraphjs/how-tos/define-state/#getting-started for +// more on defining custom graph states. +const workflow = new StateGraph(MessagesAnnotation) + // Define the two nodes we will cycle between + .addNode("callModel", callModel) + .addNode("tools", new ToolNode(tools)) + // Set the entrypoint as `callModel` + // This means that this node is the first one called + .addEdge("__start__", "callModel") + .addConditionalEdges( + // First, we define the edges' source node. We use `callModel`. + // This means these are the edges taken after the `callModel` node is called. + "callModel", + // Next, we pass in the function that will determine the sink node(s), which + // will be called after the source node is called. + routeModelOutput, + // List of the possible destinations the conditional edge can route to. + // Required for conditional edges to properly render the graph in Studio + [ + "tools", + "__end__" + ], + ) + // This means that after `tools` is called, `callModel` node is called next. + .addEdge("tools", "callModel"); + +// Finally, we compile it! +// This compiles it into a graph you can invoke and deploy. +export const graph = workflow.compile(); +``` + +!!! info "Assign `CompiledGraph` to Variable" + The build process for LangGraph Cloud requires that the `CompiledGraph` object be assigned to a variable at the top-level of a JavaScript module (alternatively, you can provide [a function that creates a graph](./graph_rebuild.md)). + +Example file directory: + +```bash +my-app/ +├── src # all project code lies within here +│ ├── utils # optional utilities for your graph +│ │ ├── tools.ts # tools for your graph +│ │ ├── nodes.ts # node functions for you graph +│ │ └── state.ts # state definition of your graph +│   └── agent.ts # code for constructing your graph +├── package.json # package dependencies +├── .env # environment variables +└── langgraph.json # configuration file for LangGraph +``` + +## Create LangGraph API Config + +Create a [LangGraph API configuration file](../reference/cli.md#configuration-file) called `langgraph.json`. See the [LangGraph CLI reference](../reference/cli.md#configuration-file) for detailed explanations of each key in the JSON object of the configuration file. + +Example `langgraph.json` file: + +```json +{ + "node_version": "20", + "dockerfile_lines": [], + "dependencies": ["."], + "graphs": { + "agent": "./src/agent.ts:graph" + }, + "env": ".env" +} +``` + +Note that the variable name of the `CompiledGraph` appears at the end of the value of each subkey in the top-level `graphs` key (i.e. `:`). + +!!! info "Configuration Location" + The LangGraph API configuration file must be placed in a directory that is at the same level or higher than the TypeScript files that contain compiled graphs and associated dependencies. + +## Next + +After you setup your project and place it in a github repo, it's time to [deploy your app](./cloud.md). diff --git a/docs/docs/cloud/faq/studio.md b/docs/docs/cloud/faq/studio.md index cef962f34..070014ce7 100644 --- a/docs/docs/cloud/faq/studio.md +++ b/docs/docs/cloud/faq/studio.md @@ -43,15 +43,23 @@ If you don't define your conditional edges carefully, you might notice extra edg ### Solution 1: Include a path map -The first way to solve this is to add path maps to your conditional edges. A path map is just a dictionary that maps the possible outputs of your router function with the names of the nodes that each output corresponds to. The path map is passed as the third argument to the `add_conditional_edges` function like so: +The first way to solve this is to add path maps to your conditional edges. A path map is just a dictionary or array that maps the possible outputs of your router function with the names of the nodes that each output corresponds to. The path map is passed as the third argument to the `add_conditional_edges` function like so: -```python -graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"}) -``` +=== "Python" + + ```python + graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"}) + ``` + +=== "Javascript" + + ```ts + graph.addConditionalEdges("node_a", routingFunction, ["node_b", "node_c"]); + ``` In this case, the routing function returns either True or False, which map to `node_b` and `node_c` respectively. -### Solution 2: Update the typing of the router +### Solution 2: Update the typing of the router (Python only) Instead of passing a path map, you can also be explicit about the typing of your routing function by specifying the nodes it can map to using the `Literal` python definition. Here is an example of how to define a routing function in that way: diff --git a/docs/docs/cloud/how-tos/interrupt_concurrent.md b/docs/docs/cloud/how-tos/interrupt_concurrent.md index e67fb4ce5..b09f908ef 100644 --- a/docs/docs/cloud/how-tos/interrupt_concurrent.md +++ b/docs/docs/cloud/how-tos/interrupt_concurrent.md @@ -44,7 +44,7 @@ Now, let's import our required packages and instantiate our client, assistant, a const thread = await client.threads.create(); ``` -Now we can start our two runs and join the second on euntil it has completed: +Now we can start our two runs and join the second one until it has completed: === "Python" diff --git a/docs/docs/cloud/quick_start.md b/docs/docs/cloud/quick_start.md index c59a6fb85..755ce0df4 100644 --- a/docs/docs/cloud/quick_start.md +++ b/docs/docs/cloud/quick_start.md @@ -14,13 +14,25 @@ This tutorial will use: 1. Create a new application with the following directory and files: +=== "Python" + / |-- agent.py # code for your LangGraph agent |-- requirements.txt # Python packages required for your graph |-- langgraph.json # configuration file for LangGraph |-- .env # environment files with API keys -2. The `agent.py` file should contain Python code for defining your graph. The following code is a simple example, the important thing is that at some point in your file you compile your graph and assign the compiled graph to a variable (in this case the `graph` variable). This example code uses `create_react_agent`, a prebuilt agent, read more about it [here](..//concepts/agentic_concepts.md#react-agent). +=== "Javascript" + + / + |-- agent.ts # code for your LangGraph agent + |-- package.json # Javascript packages required for your graph + |-- langgraph.json # configuration file for LangGraph + |-- .env # environment files with API keys + +2. The `agent.py`/`agent.ts` file should contain code for defining your graph. The following code is a simple example, the important thing is that at some point in your file you compile your graph and assign the compiled graph to a variable (in this case the `graph` variable). This example code uses `create_react_agent`, a prebuilt agent. You can read more about it [here](../concepts/agentic_concepts.md#react-agent). + +=== "Python" ```python from langchain_anthropic import ChatAnthropic @@ -34,14 +46,53 @@ This tutorial will use: graph = create_react_agent(model, tools) ``` -3. The `requirements.txt` file should contain any dependencies for your graph(s). In this case we only require four packages for our graph to run: +=== "Javascript" - langgraph - langchain_anthropic - tavily-python - langchain_community + ```ts + import { ChatAnthropic } from "@langchain/anthropic"; + import { TavilySearchResults } from "@langchain/community/tools/tavily_search"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; -4. The [`langgraph.json`][langgraph.json] file is a configuration file that describes what graph(s) you are going to host. In this case we only have one graph to host: the compiled `graph` object from `agent.py`. + const model = new ChatAnthropic({ + model: "claude-3-5-sonnet-20240620", + }); + + const tools = [ + new TavilySearchResults({ maxResults: 3, }), + ]; + + export const graph = createReactAgent({ llm: model, tools }); + ``` + +3. The `requirements.txt`/`package.json` file should contain any dependencies for your graph(s). In this case we only require four packages for our graph to run: + +=== "Python" + + ```python + langgraph + langchain_anthropic + tavily-python + langchain_community + ``` + +=== "Javascript" + + ```js + { + "name": "my-app", + "packageManager": "yarn@1.22.22", + "dependencies": { + "@langchain/community": "^0.2.31", + "@langchain/core": "^0.2.31", + "@langchain/langgraph": "0.2.0", + "@langchain/openai": "^0.2.8" + } + } + ``` + +4. The [`langgraph.json`][langgraph.json] file is a configuration file that describes what graph(s) you are going to host. In this case we only have one graph to host: the compiled `graph` object from `agent.py`/`agent.ts`. + +=== "Python" ```json { @@ -53,7 +104,21 @@ This tutorial will use: } ``` - Learn more about the LangGraph CLI configuration file [here](./reference/cli.md#configuration-file). +=== "Javascript" + + ```json + { + "node_version": "20", + "dockerfile_lines": [], + "dependencies": ["."], + "graphs": { + "agent": "./src/agent.ts:graph" + }, + "env": ".env" + } + ``` + +Learn more about the LangGraph CLI configuration file [here](./reference/cli.md#configuration-file). 5. The `.env` file should have any environment variables needed to run your graph. This will only be used for local testing, so if you are not testing locally you can skip this step. NOTE: if you do add this, you should NOT check this into git. For this graph, we need two environment variables: @@ -206,36 +271,108 @@ export LANGSMITH_API_KEY=... The first thing to do when using the SDK is to setup our client, access our assistant, and create a thread to execute a run on: -```python -from langgraph_sdk import get_client +=== "Python" -# Replace this with the URL of your own deployed graph -URL = "https://chatbot-23a570f3210f52a7b167f09f6158e3b3-ffoprvkqsa-uc.a.run.app" -client = get_client(url=URL) + ```python + from langgraph_sdk import get_client -# Search all hosted graphs -assistants = await client.assistants.search() -# In this example we select the first assistant since we are only hosting a single graph -assistant = assistants[0] + client = get_client(url=) + # get default assistant + assistants = await client.assistants.search() + assistant = [a for a in assistants if not a["config"]][0] + # create thread + thread = await client.threads.create() + print(thread) + ``` -# We create a thread for tracking the state of our run -thread = await client.threads.create() -``` +=== "Javascript" + + ```js + import { Client } from "@langchain/langgraph-sdk"; + + const client = new Client({ apiUrl: }); + // get default assistant + const assistants = await client.assistants.search(); + const assistant = assistants.find(a => !a.config); + // create thread + const thread = await client.threads.create(); + console.log(thread) + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /assistants/search \ + --header 'Content-Type: application/json' \ + --data '{ + "limit": 10, + "offset": 0 + }' | jq -c 'map(select(.config == null or .config == {})) | .[0]' && \ + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' + ``` We can then execute a run on the thread: -```python -input = {"messages":[{"role": "user", "content": "Hello! My name is Bagatur and I am 26 years old."}]} +=== "Python" -async for chunk in client.runs.stream( - thread['thread_id'], - assistant["assistant_id"], - input=input, - stream_mode="updates", - ): - if chunk.data and chunk.event != "metadata": - print(chunk.data) -``` + ```python + input = {"messages":[{"role": "user", "content": "Hello! My name is Bagatur and I am 26 years old."}]} + + async for chunk in client.runs.stream( + thread['thread_id'], + assistant["assistant_id"], + input=input, + stream_mode="updates", + ): + if chunk.data and chunk.event != "metadata": + print(chunk.data) + ``` + +=== "Javascript" + + ```js + const input = { "messages":[{ "role": "user", "content": "Hello! My name is Bagatur and I am 26 years old." }] }; + + const streamResponse = client.runs.stream( + thread["thread_id"], + assistant["assistant_id"], + { + input, + } + ); + for await (const chunk of streamResponse) { + if (chunk.data && chunk.event !== "metadata" ) { + console.log(chunk.data); + } + } + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": , + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"Hello! My name is Bagatur and I am 26 years old.\"}]}, + }" | sed 's/\r$//' | awk ' + /^event:/ { event = $2 } + /^data:/ { + json_data = substr($0, index($0, $2)) + + if (event != "metadata") { + print json_data + } + }' + ``` + + +Output: {'agent': {'messages': [{'content': "Hi Bagatur! It's nice to meet you. How can I assist you today?", 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_9cb5d38cf7'}, 'type': 'ai', 'name': None, 'id': 'run-c89118b7-1b1e-42b9-a85d-c43fe99881cd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}} diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index eb6d5d36f..56a9435b2 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -197,6 +197,7 @@ nav: - Setup: - Setup App: "cloud/deployment/setup.md" - Setup App (pyproject.toml): "cloud/deployment/setup_pyproject.md" + - Setup App (JavaScript): "cloud/deployment/setup_javascript.md" - Rebuild Graph at Runtime: "cloud/deployment/graph_rebuild.md" - Test App Locally: "cloud/deployment/test_locally.md" - Deployment: