Merge branch 'main' into cc/many_tools_guide

This commit is contained in:
Nuno Campos
2024-07-25 11:15:05 -07:00
committed by GitHub
43 changed files with 235 additions and 11 deletions
+28
View File
@@ -0,0 +1,28 @@
name: Check File Size
on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
jobs:
file-size-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v44
- name: Filter by size
run: |
large_added_files=$(find ${{ steps.changed-files.outputs.added_files }} -maxdepth 0 -size +1M)
if [ -n "$large_added_files" ]; then
echo "Large files added: $large_added_files"
echo "# Large files added:" >> $GITHUB_STEP_SUMMARY
echo "$large_added_files" >> $GITHUB_STEP_SUMMARY
exit 1
fi
+146
View File
@@ -0,0 +1,146 @@
# Rebuild Graph at Runtime
You might need to rebuild your graph with a different configuration for a new run. For example, you might need to use a different graph state or graph structure depending on the config. This guide shows how you can do this.
!!! note "Note"
In most cases, customizing behavior based on the config should be handled by a single graph where each node can read a config and change its behavior based on it
## Prerequisites
Make sure to check out [this how-to guide](./setup.md) on setting up your app for deployment first.
## Define graphs
Let's say you have an app with a simple graph that calls an LLM and returns the response to the user. The app file directory looks like the following:
```
my-app/
|-- requirements.txt
|-- .env
|-- openai_agent.py # code for your graph
```
where the graph is defined in `openai_agent.py`.
### No rebuild
In the standard LangGraph API configuration, the server uses the compiled graph instance that's defined at the top level of `openai_agent.py`, which looks like the following:
```python
from langchain_openai import ChatOpenAI
from langgraph.graph import END, MessageGraph
model = ChatOpenAI(temperature=0)
graph_workflow = MessageGraph()
graph_workflow.add_node("agent", model)
graph_workflow.add_edge("agent", END)
graph_workflow.set_entry_point("agent")
agent = graph_workflow.compile()
```
To make the server aware of your graph, you need to specify a path to the variable that contains the `CompiledStateGraph` instance in your LangGraph API configuration (`langgraph.json`), e.g.:
```
{
"dependencies": ["."],
"graphs": {
"openai_agent": "./openai_agent.py:agent",
},
"env": "./.env"
}
```
### Rebuild
To make your graph rebuild on each new run with custom configuration, you need to rewrite `openai_agent.py` to instead provide a _function_ that takes a config and returns a graph (or compiled graph) instance. Let's say we want to return our existing graph for user ID '1', and a tool-calling agent for other users. We can modify `openai_agent.py` as follows:
```python
from typing import Annotated, TypedDict
from langchain_openai import ChatOpenAI
from langgraph.graph import END, MessageGraph
from langgraph.graph.state import StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
from langchain_core.tools import tool
from langchain_core.messages import BaseMessage
from langchain_core.runnables import RunnableConfig
class State(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
model = ChatOpenAI(temperature=0)
def make_default_graph():
"""Make a simple LLM agent"""
graph_workflow = StateGraph(State)
def call_model(state):
return {"messages": [model.invoke(state["messages"])]}
graph_workflow.add_node("agent", call_model)
graph_workflow.add_edge("agent", END)
graph_workflow.set_entry_point("agent")
agent = graph_workflow.compile()
return agent
def make_alternative_graph():
"""Make a tool-calling agent"""
@tool
def add(a: float, b: float):
"""Adds two numbers."""
return a + b
tool_node = ToolNode([add])
model_with_tools = model.bind_tools([add])
def call_model(state):
return {"messages": [model_with_tools.invoke(state["messages"])]}
def should_continue(state: State):
if state["messages"][-1].tool_calls:
return "tools"
else:
return END
graph_workflow = StateGraph(State)
graph_workflow.add_node("agent", call_model)
graph_workflow.add_node("tools", tool_node)
graph_workflow.add_edge("tools", "agent")
graph_workflow.set_entry_point("agent")
graph_workflow.add_conditional_edges("agent", should_continue)
agent = graph_workflow.compile()
return agent
# this is the graph making function that will decide which graph to
# build based on the provided config
def make_graph(config: RunnableConfig):
user_id = config.get("configurable", {}).get("user_id")
# route to different graph state / structure based on the user ID
if user_id == "1":
return make_default_graph()
else:
return make_alternative_graph()
```
Finally, you need to specify the path to your graph-making function (`make_graph`) in `langgraph.json`:
```
{
"dependencies": ["."],
"graphs": {
"openai_agent": "./openai_agent.py:make_graph",
},
"env": "./.env"
}
```
See more info on LangGraph API configuration file [here](../reference/cli.md#configuration-file)
+1 -1
View File
@@ -88,7 +88,7 @@ agent = graph_workflow.compile()
```
!!! warning "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 Python module.
The build process for LangGraph Cloud requires that the `CompiledGraph` object be assigned to a variable at the top-level of a Python module (alternatively, you can provide [a function that creates a graph](./graph_rebuild.md)).
Example file directory:
```
+2 -2
View File
@@ -13,7 +13,7 @@ The LangGraph CLI requires a JSON configuration file with the following keys:
| Key | Description |
| --- | ----------- |
| `dependencies` | **Required**. Array of dependencies for LangGraph Cloud API server. Dependencies can be one of the following: (1) `"."`, which will look for local Python packages, (2) `pyproject.toml`, `setup.py` or `requirements.txt` in the app directory `"./local_package"`, or (3) a package name. |
| `graphs` | **Required**. Mapping from graph ID to path where the compiled graph is defined. Example: `./your_package/your_file.py:variable`, where `variable` is an instance of `langgraph.graph.graph.CompiledGraph`. |
| `graphs` | **Required**. Mapping from graph ID to path where the compiled graph or a function that makes a graph is defined. Example: <ul><li>`./your_package/your_file.py:variable`, where `variable` is an instance of `langgraph.graph.state.CompiledStateGraph`</li><li>`./your_package/your_file.py:make_graph`, where `make_graph` is a function that takes a config dictionary (`langchain_core.runnables.RunnableConfig`) and creates an instance of `langgraph.graph.state.StateGraph` / `langgraph.graph.state.CompiledStateGraph`.</li></ul> |
| `env` | Path to `.env` file or a mapping from environment variable to its value. |
| `python_version` | `3.11` or `3.12`. Defaults to `3.11`. |
| `pip_config_file`| Path to `pip` config file. |
@@ -49,7 +49,7 @@ Example:
"."
],
"graphs": {
"my_graph_id": "./your_package/your_file.py:variable"
"my_graph_id": "./your_package/your_file.py:make_graph"
},
"env": {
"OPENAI_API_KEY": "secret-key"
+7
View File
@@ -61,6 +61,13 @@ These guides show how to use different streaming modes.
- [How to pass graph state to tools](pass-run-time-values-to-tools.ipynb)
- [How to pass config to tools](pass-config-to-tools.ipynb)
## State Management
- [Use Pydantic model as state](state-model.ipynb)
- [Use a context object in state](state-context-key.ipynb)
- [Have a separate input and output schema](input_output_schema.ipynb)
- [Pass private state between nodes inside the graph](pass_private_state.ipynb)
## Other
- [How to run graph asynchronously](async.ipynb)
+1
View File
@@ -192,6 +192,7 @@ nav:
- Deployment:
- Setup App: "cloud/deployment/setup.md"
- Setup App (pyproject.toml): "cloud/deployment/setup_pyproject.md"
- Rebuild Graph at Runtime: "cloud/deployment/graph_rebuild.md"
- Test App Locally: "cloud/deployment/test_locally.md"
- Deploy to Cloud: "cloud/deployment/cloud.md"
- Self-Host: "cloud/deployment/self_hosted.md"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 KiB

After

Width:  |  Height:  |  Size: 194 KiB

@@ -50,7 +50,7 @@
"metadata": {},
"outputs": [
{
"name": "stdin",
"name": "stdout",
"output_type": "stream",
"text": [
"OPENAI_API_KEY: ········\n"
@@ -991,7 +991,7 @@
"id": "08996d90-a3ff-4655-9763-1dd4971344d4",
"metadata": {},
"source": [
"### With LangGraph Clound"
"### With LangGraph Cloud"
]
},
{
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 607 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 863 KiB

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 809 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 914 KiB

After

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1003 KiB

After

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 KiB

After

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

+1 -1
View File
@@ -11,7 +11,7 @@
"source": [
"# How to create subgraphs\n",
"\n",
"For more complex systems, sub-graphs are a useful design principle. Sub-graphs allow you to create and manage different states in different parts of your graph. This allows you build things like [multi-agent teams](./multi_agent/hierarchical_agent_teams.ipynb), where each team can track its own separate state.\n",
"For more complex systems, sub-graphs are a useful design principle. Sub-graphs allow you to create and manage different states in different parts of your graph. This allows you build things like [multi-agent teams](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/hierarchical_agent_teams/), where each team can track its own separate state.\n",
"\n",
"![Screenshot 2024-07-11 at 1.01.28 PM.png](attachment:71516aef-9c00-4730-a676-a54e90cb6472.png)"
]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 501 KiB

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 KiB

After

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 MiB

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 MiB

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 MiB

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 MiB

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

+4 -2
View File
@@ -722,8 +722,10 @@ class Pregel(
if config and config.get("configurable", {}).get(CONFIG_KEY_READ) is not None:
# if being called as a node in another graph, always use values mode
stream_mode = ["values"]
if config is not None and config.get("configurable", {}).get(
CONFIG_KEY_CHECKPOINTER
if (
config is not None
and config.get("configurable", {}).get(CONFIG_KEY_CHECKPOINTER)
and (interrupt_after or interrupt_before)
):
checkpointer: Optional[BaseCheckpointSaver] = config["configurable"][
CONFIG_KEY_CHECKPOINTER
+3
View File
@@ -257,6 +257,9 @@ def prepare_next_tasks(
if not isinstance(packet, Send):
logger.warn(f"Ignoring invalid packet type {type(packet)} in pending sends")
continue
if packet.node not in processes:
logger.warn(f"Ignoring unknown node name {packet.node} in pending sends")
continue
if for_execution:
proc = processes[packet.node]
if node := proc.get_node():
+4 -1
View File
@@ -102,6 +102,7 @@ class PregelLoop:
checkpoint_pending_writes: List[PendingWrite]
step: int
stop: int
status: Literal[
"pending", "done", "interrupt_before", "interrupt_after", "out_of_steps"
]
@@ -203,7 +204,7 @@ class PregelLoop:
return False
# check if iteration limit is reached
if self.step > self.config["recursion_limit"]:
if self.step > self.stop:
self.status = "out_of_steps"
return False
@@ -419,6 +420,7 @@ class SyncPregelLoop(PregelLoop, ContextManager):
)
self.status = "pending"
self.step = self.checkpoint_metadata["step"] + 1
self.stop = self.step + self.config["recursion_limit"] + 1
return self
@@ -497,6 +499,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
)
self.status = "pending"
self.step = self.checkpoint_metadata["step"] + 1
self.stop = self.step + self.config["recursion_limit"] + 1
return self
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
version = "0.1.12"
version = "0.1.14"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
+9
View File
@@ -7,6 +7,7 @@ from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
Checkpoint,
CheckpointMetadata,
CheckpointTuple,
SerializerProtocol,
copy_checkpoint,
)
@@ -119,3 +120,11 @@ class MemorySaverAssertCheckpointMetadata(MemorySaver):
return await asyncio.get_running_loop().run_in_executor(
None, self.put, config, checkpoint, metadata
)
class MemorySaverNoPending(MemorySaver):
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
result = super().get_tuple(config)
if result:
return CheckpointTuple(result.config, result.checkpoint, result.metadata)
return result
+25 -1
View File
@@ -66,6 +66,7 @@ from tests.any_str import AnyStr
from tests.memory_assert import (
MemorySaverAssertCheckpointMetadata,
MemorySaverAssertImmutable,
MemorySaverNoPending,
NoopSerializer,
)
@@ -1149,10 +1150,32 @@ def test_cond_edge_after_send() -> None:
builder.add_conditional_edges("1", send_for_fun)
builder.add_conditional_edges("2", route_to_three)
graph = builder.compile()
assert graph.invoke(["0"]) == ["0", "1", "2", "3"]
async def test_checkpointer_null_pending_writes() -> None:
class Node:
def __init__(self, name: str):
self.name = name
setattr(self, "__name__", name)
def __call__(self, state):
return [self.name]
builder = StateGraph(Annotated[list, operator.add])
builder.add_node(Node("1"))
builder.add_edge(START, "1")
graph = builder.compile(checkpointer=MemorySaverNoPending())
assert graph.invoke([], {"configurable": {"thread_id": "foo"}}) == ["1"]
assert graph.invoke([], {"configurable": {"thread_id": "foo"}}) == ["1"] * 2
assert (await graph.ainvoke([], {"configurable": {"thread_id": "foo"}})) == [
"1"
] * 3
assert (await graph.ainvoke([], {"configurable": {"thread_id": "foo"}})) == [
"1"
] * 4
def test_invoke_checkpoint_sqlite(mocker: MockerFixture) -> None:
adder = mocker.Mock(side_effect=lambda x: x["total"] + x["input"])
@@ -8570,6 +8593,7 @@ def test_nested_graph_interrupts_parallel(checkpointer: BaseCheckpointSaver) ->
checkpointer.__exit__(None, None, None)
@pytest.mark.skip
@pytest.mark.parametrize(
"checkpointer",
[
@@ -7099,6 +7099,7 @@ async def test_nested_graph_interrupts_parallel(
await checkpointer.__aexit__(None, None, None)
@pytest.mark.skip
@pytest.mark.parametrize(
"checkpointer",
[