Compare commits

..
Author SHA1 Message Date
Lance Martin 6ed63ba8fc Updates 2025-01-29 15:23:26 -08:00
Lance Martin 2da005b4bd rename, update 2025-01-28 15:24:35 -08:00
Lance Martin 58e2824ea3 Update with time travel 2025-01-28 12:49:02 -08:00
Lance Martin f239b39060 fxn api w agent 2025-01-28 06:31:27 -08:00
Vadym BardaandGitHub 0a861815b7 docs: replace 'state_modifier' with 'prompt' (#3190) 2025-01-28 02:51:40 +00:00
Eugene YurtsevandGitHub 928126f2d7 docs: functional api concepts paraphrase (#3223) 2025-01-27 21:26:13 -05:00
14 changed files with 1095 additions and 38 deletions
+42 -16
View File
@@ -7,7 +7,7 @@
The Functional API is an alternative to [Graph API (StateGraph)](low_level.md#stategraph) for development in LangGraph.
It allows you to take advantage of LangGraph's key features for [persistence](persistence.md), [human-in-the-loop](human_in_the_loop.md) workflows, and [streaming](streaming.md) without explicitly specifying state, or control flow in terms of nodes and edges.
If modeling your application with explicit nodes and edges is not useful to you, the Functional API allows you to take advantage of LangGraph's key features for [persistence](persistence.md), [human-in-the-loop](human_in_the_loop.md) workflows, and [streaming](streaming.md) without explicitly specifying state, or control flow in terms of nodes and edges.
The **Functional API** and the **[Graph API](./low_level.md)** can be used together in the same application, allowing you to intermix the two paradigms if needed.
@@ -120,6 +120,15 @@ def workflow(topic: str) -> dict:
The workflow has been completed and the review has been added to the essay.
## Functional API vs. Graph API
The **Functional API** and the [Graph APIs (StateGraph)](./low_level.md#stategraph) provide two different paradigms to create in LangGraph. Here are some key differences:
- **Control flow**: The Functional API does not require thinking about graph structure. You can use standard Python constructs to define workflows. This will usually trim the amount of code you need to write.
- **State management**: The **GraphAPI** requires declaring a [**State**](./low_level.md#state) and may require defining [**reducers**](./low_level.md#reducers) to manage updates to the graph state. `@entrypoint` and `@tasks` do not require explicit state management as their state is scoped to the function and is not shared across functions.
- **Checkpointing**: Both APIs generate and use checkpoints. In the **Graph API** a new checkpoint is generated after every [superstep](./low_level.md). In the **Functional API**, when tasks are executed, their results are saved to an existing checkpoint associated with the given entrypoint instead of creating a new checkpoint.
- **Visualization**: The Graph API makes it easy to visualize the workflow as a graph which can be useful for debugging, understanding the workflow, and sharing with others. The Functional API does not support visualization as the graph is dynamically generated during runtime.
## Building Blocks
The **Functional API** provides two primitives for building workflows:
@@ -331,6 +340,8 @@ Resuming an execution after an [interrupt][langgraph.types.interrupt] can be don
To resume after an error, run the `entrypoint` with a `None` and the same **thread id** (config).
This assumes that the underlying **error** has been resolved and execution can proceed successfully.
=== "Invoke"
```python
@@ -436,7 +447,10 @@ my_workflow.invoke(1, config) # 6 (previous was 3 * 2 from the previous invocat
## Task
A **task** represents a discrete unit of work, such as an API call or data processing step, that can be executed asynchronously. Invoking a **task** returns a future, which can be waited on to obtain the result.
A **task** represents a discrete unit of work, such as an API call or data processing step. It has two key characteristics:
* **Asynchronous Execution**: Tasks are designed to be executed asynchronously, allowing multiple operations to run concurrently without blocking.
* **Checkpointing**: Task results are saved to a checkpoint, enabling resumption of the workflow from the last saved state. (See [persistence](persistence.md) for more details).
### Definition
@@ -458,7 +472,14 @@ def slow_computation(input_value):
### Execution
**Tasks** can only be called from within an **entrypoint**, another **task**, or a [state graph node](./low_level.md#nodes). They **cannot** be called directly from the main application code. Calling a **task** produces a future-like object that can be awaited or resolved to obtain the result.
**Tasks** can only be called from within an **entrypoint**, another **task**, or a [state graph node](./low_level.md#nodes).
Tasks *cannot* be called directly from the main application code.
When you call a **task**, it returns *immediately* with a future object. A future is a placeholder for a result that will be available later.
To obtain the result of a **task**, you can either wait for it synchronously (using `result()`) or await it asynchronously (using `await`).
=== "Synchronous Invocation"
@@ -481,9 +502,11 @@ def slow_computation(input_value):
**Tasks** are useful in the following scenarios:
- **Resumable Graph Execution**: When graph execution may need to be **resumed** after being **interrupted** (e.g., for **human-in-the-loop**), **tasks** can encapsulate any source of non-determinism, such as API calls, database queries, or random number generation. See the [determinism](#determinism) for more details.
- **Retryable Work**: When work needs to be retried to handle failures or inconsistencies, **tasks** provide a way to encapsulate and manage the retry logic.
- **Checkpointing**: When you need to save the result of a long-running operation to a checkpoint, so you don't need to recompute it when resuming the workflow.
- **Human-in-the-loop**: If you're building a workflow that requires human intervention, you MUST use **tasks** to encapsulate any randomness (e.g., API calls) to ensure that the workflow can be resumed correctly. See the [determinism](#determinism) section for more details.
- **Parallel Execution**: For I/O-bound tasks, **tasks** enable parallel execution, allowing multiple operations to run concurrently without blocking (e.g., calling multiple APIs).
- **Observability**: Wrapping operations in **tasks** provides a way to track the progress of the workflow and monitor the execution of individual operations using [LangSmith](https://docs.smith.langchain.com/).
- **Retryable Work**: When work needs to be retried to handle failures or inconsistencies, **tasks** provide a way to encapsulate and manage the retry logic.
## Serialization
@@ -511,24 +534,16 @@ While different runs of a workflow can produce different results, resuming a **s
Idempotency ensures that running the same operation multiple times produces the same result. This helps prevent duplicate API calls and redundant processing if a step is rerun due to a failure. Always place API calls inside **tasks** functions for checkpointing, and design them to be idempotent in case of re-execution. Re-execution can occur if a **task** starts, but does not complete successfully. Then, if the workflow is resumed, the **task** will run again. Use idempotency keys or verify existing results to avoid duplication.
## Functional API vs. Graph API
The **Functional API** and the **Graph APIs** provide two different paradigms to create workflows in LangGraph. Here are some key differences:
- **Control flow**: The Functional API does not require thinking about graph structure. You can use standard Python constructs to define workflows.
- **State management**: The **GraphAPI** requires declaring a [**State**](./low_level.md#state) and may require defining [**reducers**](./low_level.md#reducers) to manage updates to the graph state. `@entrypoint` and `@tasks` do not require explicit state management as their state is scoped to the function and is not shared across functions.
- **Checkpointing**: Both APIs generate and use checkpoints. In the **Graph API** a new checkpoint is generated after every [superstep](./low_level.md). In the **Functional API**, when tasks are executed, their results are saved to an existing checkpoint associated with the given entrypoint instead of creating a new checkpoint.
- **Visualization**: The Graph API makes it easy to visualize the workflow as a graph which can be useful for debugging, understanding the workflow, and sharing with others. The Functional API does not support visualization as the graph is dynamically generated during runtime.
## Common Pitfalls
### Handling side effects
Side effects, such as writing to a file or sending an email, should be encapsulated in tasks to ensure consistent execution upon resumption.
Encapsulate side effects (e.g., writing to a file, sending an email) in tasks to ensure they are not executed multiple times when resuming a workflow.
=== "Incorrect"
In this example, a side effect (writing to a file) is directly included in the workflow, making resumption inconsistent.
In this example, a side effect (writing to a file) is directly included in the workflow, so it will be executed a second time when resuming the workflow.
```python
@entrypoint(checkpointer=checkpointer)
@@ -567,7 +582,18 @@ Side effects, such as writing to a file or sending an email, should be encapsula
### Non-deterministic control flow
[Non-deterministic control flow](#determinism) can lead to inconsistent results when resuming a workflow. To ensure correct behavior, encapsulate non-deterministic operations (e.g., random number generation, time-based logic) inside **tasks**.
Operations that might give different results each time (like getting current time or random numbers) should be encapsulated in tasks to ensure that on resume, the same result is returned.
* In a task: Get random number (5) → interrupt → resume → (returns 5 again) → ...
* Not in a task: Get random number (5) → interrupt → resume → get new random number (7) → ...
This is especially important when using **human-in-the-loop** workflows with multiple interrupts calls. LangGraph keeps a list
of resume values for each task/entrypoint. When an interrupt is encountered, it's matched with the corresponding resume value.
This matching is strictly **index-based**, so the order of the resume values should match the order of the interrupts.
If order of execution is not maintained when resuming, one `interrupt` call may be matched with the wrong `resume` value, leading to incorrect results.
Please read the section on [determinism](#determinism) for more details.
=== "Incorrect"
+2 -2
View File
@@ -828,13 +828,13 @@
"addition_expert = create_react_agent(\n",
" model,\n",
" [add, make_handoff_tool(agent_name=\"multiplication_expert\")],\n",
" state_modifier=\"You are an addition expert, you can ask the multiplication expert for help with multiplication.\",\n",
" prompt=\"You are an addition expert, you can ask the multiplication expert for help with multiplication.\",\n",
")\n",
"\n",
"multiplication_expert = create_react_agent(\n",
" model,\n",
" [multiply, make_handoff_tool(agent_name=\"addition_expert\")],\n",
" state_modifier=\"You are a multiplication expert, you can ask an addition expert for help with addition.\",\n",
" prompt=\"You are a multiplication expert, you can ask an addition expert for help with addition.\",\n",
")\n",
"\n",
"builder = StateGraph(MessagesState)\n",
+1 -1
View File
@@ -134,7 +134,7 @@
"model = ChatOpenAI(model=\"gpt-4o\")\n",
"tools = [TavilySearchResults(max_results=1)]\n",
"web_search_agent = create_react_agent(\n",
" model, tools, state_modifier=\"You are an agent specializing in web search\"\n",
" model, tools, prompt=\"You are an agent specializing in web search\"\n",
")"
]
},
@@ -39,7 +39,7 @@
"\n",
"This tutorial will show how to add a custom system prompt to the [prebuilt ReAct agent](https://langchain-ai.github.io/langgraph/reference/prebuilt/#langgraph.prebuilt.chat_agent_executor.create_react_agent). Please see [this tutorial](../create-react-agent) for how to get started with the prebuilt ReAct agent\n",
"\n",
"You can add a custom system prompt by passing a string to the `state_modifier` param.\n"
"You can add a custom system prompt by passing a string to the `prompt` param.\n"
]
},
{
@@ -144,7 +144,7 @@
"\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"graph = create_react_agent(model, tools=tools, state_modifier=prompt)"
"graph = create_react_agent(model, tools=tools, prompt=prompt)"
]
},
{
@@ -195,7 +195,7 @@
"source": [
"## Using in `create_react_agent`\n",
"\n",
"Add semantic search to your tool calling agent by injecting the store in the `state_modifier`. You can also use the store in a tool to let your agent manually store or search for memories."
"Add semantic search to your tool calling agent by injecting the store in the `prompt` function. You can also use the store in a tool to let your agent manually store or search for memories."
]
},
{
@@ -248,9 +248,9 @@
"agent = create_react_agent(\n",
" init_chat_model(\"openai:gpt-4o-mini\"),\n",
" tools=[upsert_memory],\n",
" # The state_modifier is run to prepare the messages for the LLM. It is called\n",
" # The 'prompt' function is run to prepare the messages for the LLM. It is called\n",
" # right before each LLM call\n",
" state_modifier=prepare_messages,\n",
" prompt=prepare_messages,\n",
" store=store,\n",
")"
]
@@ -524,7 +524,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.2"
"version": "3.12.3"
}
},
"nbformat": 4,
@@ -229,7 +229,7 @@
"travel_advisor = create_react_agent(\n",
" model,\n",
" travel_advisor_tools,\n",
" state_modifier=(\n",
" prompt=(\n",
" \"You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc). \"\n",
" \"If you need hotel recommendations, ask 'hotel_advisor' for help. \"\n",
" \"You MUST include human-readable response before transferring to another agent.\"\n",
@@ -254,7 +254,7 @@
"hotel_advisor = create_react_agent(\n",
" model,\n",
" hotel_advisor_tools,\n",
" state_modifier=(\n",
" prompt=(\n",
" \"You are a hotel expert that can provide hotel recommendations for a given destination. \"\n",
" \"If you need help picking travel destinations, ask 'travel_advisor' for help.\"\n",
" \"You MUST include human-readable response before transferring to another agent.\"\n",
+2 -2
View File
@@ -555,7 +555,7 @@
"travel_advisor = create_react_agent(\n",
" model,\n",
" travel_advisor_tools,\n",
" state_modifier=(\n",
" prompt=(\n",
" \"You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc). \"\n",
" \"If you need hotel recommendations, ask 'hotel_advisor' for help. \"\n",
" \"You MUST include human-readable response before transferring to another agent.\"\n",
@@ -579,7 +579,7 @@
"hotel_advisor = create_react_agent(\n",
" model,\n",
" hotel_advisor_tools,\n",
" state_modifier=(\n",
" prompt=(\n",
" \"You are a hotel expert that can provide hotel recommendations for a given destination. \"\n",
" \"If you need help picking travel destinations, ask 'travel_advisor' for help.\"\n",
" \"You MUST include human-readable response before transferring to another agent.\"\n",
@@ -180,7 +180,7 @@
" state: AgentState,\n",
" config: RunnableConfig,\n",
"):\n",
" # this is similar to customizing the create_react_agent with state_modifier, but is a lot more flexible\n",
" # this is similar to customizing the create_react_agent with 'prompt' parameter, but is more flexible\n",
" system_prompt = SystemMessage(\n",
" \"You are a helpful AI assistant, please respond to the users query to the best of your ability!\"\n",
" )\n",
@@ -220,7 +220,7 @@
"metadata": {},
"outputs": [],
"source": [
"def state_modifier(state: State):\n",
"def prompt(state: State):\n",
" user_info = state.get(\"user_info\")\n",
" if user_info is None:\n",
" return state[\"messages\"]\n",
@@ -265,7 +265,7 @@
" [lookup_user_info],\n",
" state_schema=State,\n",
" # pass dynamic prompt function\n",
" state_modifier=state_modifier,\n",
" prompt=prompt,\n",
")"
]
},
File diff suppressed because it is too large Load Diff
@@ -201,7 +201,7 @@
"\n",
"\n",
"research_agent = create_react_agent(\n",
" llm, tools=[tavily_tool], state_modifier=\"You are a researcher. DO NOT do any math.\"\n",
" llm, tools=[tavily_tool], prompt=\"You are a researcher. DO NOT do any math.\"\n",
")\n",
"\n",
"\n",
@@ -525,7 +525,7 @@
"doc_writer_agent = create_react_agent(\n",
" llm,\n",
" tools=[write_document, edit_document, read_document],\n",
" state_modifier=(\n",
" prompt=(\n",
" \"You can read, write and edit documents based on note-taker's outlines. \"\n",
" \"Don't ask follow-up questions.\"\n",
" ),\n",
@@ -548,7 +548,7 @@
"note_taking_agent = create_react_agent(\n",
" llm,\n",
" tools=[create_outline, read_document],\n",
" state_modifier=(\n",
" prompt=(\n",
" \"You can read documents and create outlines for the document writer. \"\n",
" \"Don't ask follow-up questions.\"\n",
" ),\n",
@@ -190,7 +190,7 @@
"research_agent = create_react_agent(\n",
" llm,\n",
" tools=[tavily_tool],\n",
" state_modifier=make_system_prompt(\n",
" prompt=make_system_prompt(\n",
" \"You can only do research. You are working with a chart generator colleague.\"\n",
" ),\n",
")\n",
@@ -220,7 +220,7 @@
"chart_agent = create_react_agent(\n",
" llm,\n",
" [python_repl_tool],\n",
" state_modifier=make_system_prompt(\n",
" prompt=make_system_prompt(\n",
" \"You can only generate charts. You are working with a researcher colleague.\"\n",
" ),\n",
")\n",
@@ -143,7 +143,7 @@
"# Choose the LLM that will drive the agent\n",
"llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n",
"prompt = \"You are a helpful assistant.\"\n",
"agent_executor = create_react_agent(llm, tools, state_modifier=prompt)"
"agent_executor = create_react_agent(llm, tools, prompt=prompt)"
]
},
{