Compare commits

...
107 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
Vadym BardaandGitHub 0d8fea847a langgraph: release 0.2.68 (#3224) 2025-01-27 21:08:06 -05:00
Eugene YurtsevandGitHub b4dc3a851f functional api: remove generator support (#3220)
Remove generator support in entrypoint.
2025-01-27 19:42:35 -05:00
Vadym BardaandGitHub 7568862013 langgraph: more unittests for functional API (#3221) 2025-01-28 00:41:15 +00:00
Andrew NguonlyandGitHub 69dc5f326e docs: Add section for Automatic Deletion of LangGraph Cloud SaaS deployments (#3222) 2025-01-27 15:42:36 -08:00
90f1e66748 docs: functional api (#3125)
* Concepts page for the functional API
* How-to guides that show functional API implementations
* API reference for entrypoint, task, entrypoint.final
* Add functional API version to the workflows

---------

Co-authored-by: Vadym Barda <vadym@langchain.dev>
Co-authored-by: ccurme <chester.curme@gmail.com>
2025-01-27 16:31:43 -05:00
Vadym BardaandGitHub a910a1a341 langgraph: actually fix flaky test (#3219) 2025-01-27 16:06:31 -05:00
Vadym BardaandGitHub 554e763994 langgraph: use 'prompt' param for model input preprocessing in create_react_agent (#3173) 2025-01-27 19:03:01 +00:00
Vadym BardaandGitHub adff1df813 langgraph: fix flaky test (#3218) 2025-01-27 13:11:17 -05:00
Vadym BardaandGitHub 64b828def6 langgraph: handle task naming for reused class methods (#3216) 2025-01-27 17:05:39 +00:00
Jacob LeeandGitHub 9196cc2da8 docs: Fix broken links in README (#3211) 2025-01-27 09:15:17 -05:00
David DuongandGitHub e13a8bc320 fix: missing pgvector in CLI (#3165)
Fixes https://github.com/langchain-ai/langgraph/issues/3164
2025-01-25 04:27:42 +01:00
Tat Dat Duong 4b5b309152 Bump to 0.1.69 2025-01-25 04:18:18 +01:00
Tat Dat Duong 9d5166a0d3 Fix tests 2025-01-25 04:16:52 +01:00
Nuno CamposandGitHub 5b11683f4e Fix type hints for entrypoint decorator (#3198) 2025-01-24 15:07:21 -08:00
Vadym BardaandGitHub 3fae8d47c3 langgraph: handle more callable types in tasks (#3203) 2025-01-24 22:50:42 +00:00
Nuno Campos be9cb03dfa Lint 2025-01-24 13:51:16 -08:00
Vadym BardaandGitHub 6926c4bcc0 langgraph: add names for tasks (#3202) 2025-01-24 21:42:54 +00:00
Nuno Campos c04802a344 Fix type hints for entrypoint decorator 2025-01-24 09:31:27 -08:00
Nuno CamposandGitHub cf7f6691cf Improve typings for task, it now returns a mixed sync/async future protocol (#3186)
- note this type is never instantiated, it is only used for typing (we
cannot make it a protocol as it inherits from concurrent.futures.Future)
2025-01-24 09:19:51 -08:00
Vadym BardaandGitHub e1140f4fad ci: always test notebooks on all library versions (#3191) 2025-01-23 22:26:32 -05:00
Eugene YurtsevandGitHub ae8afb7677 reference: document entrypoint more (#3189)
add more documentation to the entrypoint primitive
2025-01-23 22:17:20 -05:00
Vadym BardaandGitHub 48040d8ea5 ci: run notebooks in 'development' for PRs (#3188) 2025-01-23 21:59:03 -05:00
Vadym BardaandGitHub ad51bfdf71 ci: only test changed notebooks using 'development' version of the library (#3187) 2025-01-23 21:41:47 -05:00
Nuno Campos 996b120613 Undo 2025-01-23 16:59:27 -08:00
Nuno Campos 1093dd55c8 Improve typings for task, it now returns a mixed sync/async future protocol
- note this type is never instantiated, it is only used for typing (we cannot make it a protocol as it inherits from concurrent.futures.Future)
2025-01-23 16:57:34 -08:00
Nuno CamposandGitHub d794875b32 Undo change to test watch mode (#3184) 2025-01-23 16:54:03 -08:00
Nuno Campos 6948cf5eb8 Undo change to test watch mode 2025-01-23 16:18:53 -08:00
Nuno CamposandGitHub bacc2955ae Remove implementation of get_graph for @entrypoint (#3183) 2025-01-23 15:48:22 -08:00
Nuno Campos f014d96d1c Remove implementation of get_graph for @entrypoint 2025-01-23 15:39:36 -08:00
RadiandGitHub 7b552ebf4b Update tool-calling.ipynb (#3178)
Typo fix
2025-01-23 18:02:43 -05:00
Vadym BardaandGitHub 1e61ddfdbe langgraph: release 0.2.67 (#3182) 2025-01-23 18:01:43 -05:00
Vadym BardaandGitHub d34846dc08 docs: fix a hub prompt (#3180) 2025-01-23 17:21:37 -05:00
Vadym BardaandGitHub 06823e327f langgraph: update docstrings/api ref for functional api (#3176) 2025-01-23 16:48:55 -05:00
Vadym BardaandGitHub 1059ef55d1 langgraph: handle node return annotations with unions (#3170) 2025-01-23 14:41:22 -05:00
39552255c8 langgraph: add support for BaseModel updates to Command (#2747)
Simple update that adds support for the `update` attribute of the
`Command` class to support Pydantic `BaseModel` type.

LangGraph already supports [Pydantic models for graph
states](https://langchain-ai.github.io/langgraph/how-tos/state-model/).

Extending support to the `update` attribute allows users to pass custom
BaseModel instances. Additionally, updates defined as `BaseModel` types
are type-validated when created.

https://github.com/langchain-ai/langgraph/issues/2804

---------

Co-authored-by: vbarda <vadym@langchain.dev>
2025-01-23 14:31:44 -05:00
Vadym BardaandGitHub 38bbe67469 langgraph: remove print (#3167) 2025-01-23 11:24:18 -05:00
Nuno CamposandGitHub 6335963674 Make scratchpad counters thread-safe (#3158)
- Same solution as used in python stdlib to name threads and asyncio
tasks
2025-01-23 07:37:04 -08:00
Nuno Campos 8a4c452317 Make test less flaky 2025-01-23 07:27:34 -08:00
Eugene YurtsevandGitHub 5dc5853161 Add entrypoint.final to decouple return value from save value (#3135)
* Introduce `entrypoint.final` that allows decoupling what's returned
from the state update.
* moving decorator to class object w/ call to support defining `final`
as a property on it -- this should play nicely w/ IDE tooling / type
checking.


```python
    previous_ = None

    @entrypoint(checkpointer=MemorySaver())
    def foo(msg: str, *, previous: Any) -> entrypoint.final[int, list[str]]:
        nonlocal previous_
        previous_ = previous
        previous = previous or []
        return entrypoint.final(value=len(previous), save=previous + [msg])

    assert foo.get_output_schema().model_json_schema() == {
        "title": "LangGraphOutput",
        "type": "integer",
    }

    config = {"configurable": {"thread_id": "1"}}
    assert foo.invoke("hello", config) == 0
    assert previous_ is None
    assert foo.invoke("goodbye", config) == 1
    assert previous_ == ["hello"]
    assert foo.invoke("definitely", config) == 2
    assert previous_ == ["hello", "goodbye"]

```
2025-01-23 10:18:23 -05:00
Vadym BardaandGitHub 9e066554ba langgraph: allow async state modifier in create_react_agent (#3161)
Fixes https://github.com/langchain-ai/langgraph/issues/2875
2025-01-23 09:29:25 -05:00
Tat Dat Duong 6ce9354ee4 fix: missing pgvector in CLI
Fixes https://github.com/langchain-ai/langgraph/issues/3164
2025-01-23 15:27:39 +01:00
Nuno Campos 211fd4337d Undo 2025-01-22 16:44:13 -08:00
Nuno Campos 44bf97ac0e Fix 2025-01-22 16:32:59 -08:00
Nuno Campos 23c73ae719 Update tests 2025-01-22 16:30:43 -08:00
Nuno CamposandGitHub 4165d479e9 langgraph: add test for interrupting multiple times from a task (#3148) 2025-01-22 16:25:03 -08:00
Nuno Campos c43a9a4bd0 Make scratchpad counters thread-safe
- Same solution as used in python stdlib to name threads and asyncio tasks
2025-01-22 16:07:59 -08:00
Nuno Campos c9613927dc Lint 2025-01-22 16:05:34 -08:00
Nuno Campos c697c2aa04 Undo 2025-01-22 16:02:09 -08:00
Nuno Campos 3f2557c9c9 Fix 2025-01-22 16:02:09 -08:00
Nuno Campos cbad17fa7d Fix 2025-01-22 16:02:09 -08:00
Nuno Campos 17dacb83a2 Fix 2025-01-22 16:02:09 -08:00
Nuno Campos 3a997be088 Fix 2025-01-22 16:02:09 -08:00
Chester CurmeandNuno Campos 020d10138d add test case 2025-01-22 16:02:09 -08:00
Vadym BardaandGitHub 303587c4ff docs: bring back image for concept doc (#3152) 2025-01-22 16:08:24 -05:00
Andrew NguonlyandGitHub 7d4e636313 docs: Add note about how to share Postgres instance for self-hosted deployments (#3139)
### Screenshot

![image](https://github.com/user-attachments/assets/1caa7581-9dda-4ffc-9fee-ffb0f72487ea)
2025-01-22 12:14:24 -08:00
Lance MartinandGitHub 86913caf89 Add agents and workflows overview (#3040) 2025-01-22 12:07:47 -08:00
Nuno CamposandGitHub 041faefe29 Remove write to RETURN channel for entrypoint func (#3149) 2025-01-22 11:46:14 -08:00
Nuno Campos b704cf30cc Lint 2025-01-22 11:34:30 -08:00
Nuno Campos 3915b44180 Format 2025-01-22 11:32:09 -08:00
Nuno Campos ac1407b23c Remove write to RETURN channel for entrypoint func 2025-01-22 09:44:52 -08:00
Vadym BardaandGitHub 44840aa23f docs: add missing api key to cross-thread persistence howto (#3141) 2025-01-21 21:24:01 -05:00
Vadym BardaandGitHub 51242e2a32 ci: pin codespell (#3140) 2025-01-21 21:22:23 -05:00
Vadym BardaandGitHub b358e2e7cd docs: fix typo in persistence howto (#3138) 2025-01-21 21:03:00 -05:00
Nuno CamposandGitHub 0d91ab1474 Disable recursing on runnables for task/entrypoint decorated funcs (#3136) 2025-01-21 15:47:51 -08:00
Nuno Campos 12ae297194 Disable recursing on runnables for task/entrypoint decorated funcs 2025-01-21 15:36:09 -08:00
Nuno Campos 0177565c6b Spell check 2025-01-21 15:14:00 -08:00
Bhavya DhimanandGitHub c48d495031 fix(requirements.txt): Set requirements path to posix expression so that it can work in windows OS as well. (#3101)
When I cloned langgraph example, I was not able to run the code because
of the requirements.txt file. The path was set to posix expression which
was not working in windows OS.
I have updated the path to posix expression so that it can work in
windows OS as well.

Try running `langgraph-example` in windows using the langgraph-cli in
windows OS. It was working for linux not in windows.
2025-01-21 14:30:13 -05:00
Vadym BardaandGitHub 22e6468af5 langgraph: release 0.2.66 (#3128) 2025-01-21 13:26:29 -05:00
ccurmeandGitHub 24bc0c0630 docs: update readme / docs intro page (#3082) 2025-01-21 13:19:30 -05:00
Nuno CamposandGitHub 802e6df8df Enable async tests that were being skipped (#3109)
- async tests are placed in test_pregel_async, not in test_pregel
- to avoid tests placed in wrong file being accidentally skipped i've
added the auto-async mark to sync test file
2025-01-21 10:16:20 -08:00
Nuno CamposandGitHub 3ec55b008d Fix timing issue where a sync task would finish before the other one was registered in futures dict (#3110)
- this was not possible in async where all done callbacks are called in
next tick
- in sync case this would manifest as the first task done callback
seeing counter == 1 and thus setting event
- the fix is to unset the event whenever a task is scheduled
2025-01-21 10:16:09 -08:00
Nuno CamposandGitHub e10b7c1391 Re-enable support for running sync tasks from async entrypoints (#3108)
- When using an async entrypoint you can now freely mix and match sync
and async tasks with a uniform api (ie all tasks return a sync or async
future depending on context)
- Fix issues with scheduling deeply nested tasks (use threadsafe methods
to schedule coroutines and create futures)
2025-01-21 10:15:49 -08:00
Nuno CamposandGitHub 31cc6b9f1d Fix tracing of args for @task decorated functions (#3107)
- now using same logic as in langsmith sdk, treating as single args
dict, based on function signature
2025-01-21 10:09:13 -08:00
Nuno Campos 2cafb4905b Lint 2025-01-21 10:06:02 -08:00
Nuno Campos fe46576d98 Fix 2025-01-21 09:59:49 -08:00
Nuno Campos 16c86c9de6 Add test 2025-01-21 09:49:46 -08:00
fca0d2d5bb Update libs/langgraph/langgraph/utils/future.py
Co-authored-by: William FH <13333726+hinthornw@users.noreply.github.com>
2025-01-21 09:43:12 -08:00
Nuno Campos c7c62f5587 Fix 2025-01-21 09:42:25 -08:00
Vadym BardaandGitHub 956c5f68fc docs: add a how-to guide for structured outputs in react agent (#3121) 2025-01-21 17:30:32 +00:00
ccurmeandGitHub 4908caf522 docs: how-to guides nits (#3123) 2025-01-21 12:17:37 -05:00
Chester Curme f926eada24 fix warnings 2025-01-21 12:06:42 -05:00
Chester Curme adc1e47028 nits 2025-01-21 12:00:12 -05:00
Vadym BardaandGitHub 7820b5c765 docs: add docs for Command.PARENT (#3081) 2025-01-21 16:37:39 +00:00
Chester Curme ca6f8e2042 Merge branch 'main' into cc/update_readme 2025-01-21 10:31:29 -05:00
Chester Curme ee61d06f8d add hyperlink 2025-01-21 10:31:27 -05:00
Roy BarberandGitHub 6eeb9de46a DOCS: Incorrect reference to JS/TS SDK as Python SDK (#3103)
Small typo in the Local Server page:
https://langchain-ai.github.io/langgraph/tutorials/langgraph-platform/local-server/
2025-01-21 09:34:57 -05:00
viren-viiandGitHub 19a91f4677 Update persistence_postgres.ipynb to remove typo in markdown cell. (#3119)
Fixed a typo that was interrupting the markdown.
2025-01-21 09:33:31 -05:00
Nuno Campos b7c3ac4501 Fix tracing output 2025-01-20 13:55:13 -08:00
Nuno Campos 09e8516689 Fix test 2025-01-20 11:57:19 -08:00
Nuno Campos 3ca75d69b8 Lint 2025-01-20 11:57:19 -08:00
Nuno Campos d48dec5452 Enable async tests that were being skipped
- async tests are placed in test_pregel_async, not in test_pregel
- to avoid tests placed in wrong file being accidentally skipped i've added the auto-async mark to sync test file
2025-01-20 11:57:19 -08:00
Nuno Campos d48b25420b Fix timing issue where a sync task would finish before the other one was registered in futures dict
- this was not possible in async where all done callbacks are called in next tick
- in sync case this would manifest as the first task done callback seeing counter == 1 and thus setting event
- the fix is to unset the event whenever a task is scheduled
2025-01-20 11:40:54 -08:00
Nuno Campos 12be3fac33 Lint 2025-01-20 11:13:37 -08:00
Nuno Campos aed1f0ba18 Re-enable support for running sync tasks from async entrypoints
- When using an async entrypoint you can now freely mix and match sync and async tasks with a uniform api (ie all tasks return a sync or async future depending on context)
- Fix issues with scheduling deeply nested tasks (use threadsafe methods to schedule coroutines and create futures)
2025-01-20 11:03:51 -08:00
Nuno Campos 07695f5c5a Lint 2025-01-20 10:53:37 -08:00
Nuno Campos 204c9c83f8 Fix tracing of args for @task decorated functions
- now using same logic as in langsmith sdk, treating as single args dict, based on function signature
2025-01-20 10:49:27 -08:00
Chester CurmeandNuno Campos 31578cbe0e move code block 2025-01-17 16:07:16 -08:00
Chester CurmeandNuno Campos 44970640d5 cr 2025-01-17 16:07:16 -08:00
Chester CurmeandNuno Campos 0f4e42474f cr 2025-01-17 16:07:16 -08:00
Chester CurmeandNuno Campos 832f9ad64e copy changes to libs/langgraph/README.md 2025-01-17 16:07:16 -08:00
Chester CurmeandNuno Campos 318de5bb81 update readme 2025-01-17 16:07:16 -08:00
109 changed files with 8725 additions and 5626 deletions
+1 -1
View File
@@ -21,7 +21,7 @@
- name: Install Dependencies
run: |
pip install toml codespell jupytext
pip install toml codespell==2.3.0 jupytext
- name: Extract Ignore Words List
run: |
+121 -71
View File
@@ -12,25 +12,48 @@
## Overview
[LangGraph](https://langchain-ai.github.io/langgraph/) is a library for building stateful, multi-actor applications with LLMs, used to create agent and multi-agent workflows. Compared to other LLM frameworks, it offers these core benefits: cycles, controllability, and persistence. LangGraph allows you to define flows that involve cycles, essential for most agentic architectures, differentiating it from DAG-based solutions. As a very low-level framework, it provides fine-grained control over both the flow and state of your application, crucial for creating reliable agents. Additionally, LangGraph includes built-in persistence, enabling advanced human-in-the-loop and memory features.
[LangGraph](https://langchain-ai.github.io/langgraph/) is a library for building
stateful, multi-actor applications with LLMs, used to create agent and multi-agent
workflows. Check out an introductory tutorial [here](https://langchain-ai.github.io/langgraph/tutorials/introduction/).
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform) is infrastructure for deploying LangGraph agents. It is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework. The LangGraph Platform consists of several components that work together to support the development, deployment, debugging, and monitoring of LangGraph applications: [LangGraph Server](https://langchain-ai.github.io/langgraph/concepts/langgraph_server) (APIs), [LangGraph SDKs](https://langchain-ai.github.io/langgraph/concepts/sdk) (clients for the APIs), [LangGraph CLI](https://langchain-ai.github.io/langgraph/concepts/langgraph_cli) (command line tool for building the server), [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio) (UI/debugger),
### Why use LangGraph?
To learn more about LangGraph, check out our first LangChain Academy course, *Introduction to LangGraph*, available for free [here](https://academy.langchain.com/courses/intro-to-langgraph).
LangGraph provides fine-grained control over both the flow and state of your
agent applications. It implements a central
[persistence layer](https://langchain-ai.github.io/langgraph/concepts/persistence/),
enabling features that are common to most agent architectures:
### Key Features
- **Memory**: LangGraph persists arbitrary aspects of your application's state,
supporting memory of conversations and other updates within and across user
interactions;
- **Human-in-the-loop**: Because state is checkpointed, execution can be interrupted
and resumed, allowing for decisions, validation, and corrections at key stages via
human input.
- **Cycles and Branching**: Implement loops and conditionals in your apps.
- **Persistence**: Automatically save state after each step in the graph. Pause and resume the graph execution at any point to support error recovery, human-in-the-loop workflows, time travel and more.
- **Human-in-the-Loop**: Interrupt graph execution to approve or edit next action planned by the agent.
- **Streaming Support**: Stream outputs as they are produced by each node (including token streaming).
- **Integration with LangChain**: LangGraph integrates seamlessly with [LangChain](https://github.com/langchain-ai/langchain/) and [LangSmith](https://docs.smith.langchain.com/) (but does not require them).
Standardizing these components allows individuals and teams to focus on the behavior
of their agent, instead of its supporting infrastructure.
Through [LangGraph Platform](#langgraph-platform), LangGraph also provides tooling for
the development, deployment, debugging, and monitoring of your applications.
LangGraph integrates seamlessly with
[LangChain](https://python.langchain.com/docs/introduction/) and
[LangSmith](https://docs.smith.langchain.com/) (but does not require them).
To learn more about LangGraph, check out our first LangChain Academy
course, *Introduction to LangGraph*, available for free
[here](https://academy.langchain.com/courses/intro-to-langgraph).
### LangGraph Platform
LangGraph Platform is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework.
[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform) is infrastructure for deploying LangGraph agents. It is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework. The LangGraph Platform consists of several components that work together to support the development, deployment, debugging, and monitoring of LangGraph applications: [LangGraph Server](https://langchain-ai.github.io/langgraph/concepts/langgraph_server) (APIs), [LangGraph SDKs](https://langchain-ai.github.io/langgraph/concepts/sdk) (clients for the APIs), [LangGraph CLI](https://langchain-ai.github.io/langgraph/concepts/langgraph_cli) (command line tool for building the server), and [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio) (UI/debugger).
See deployment options [here](https://langchain-ai.github.io/langgraph/concepts/deployment_options/)
(includes a free tier).
Here are some common issues that arise in complex deployments, which LangGraph Platform addresses:
- **Streaming support**: LangGraph Server provides [multiple streaming modes](https://langchain-ai.github.io/langgraph/concepts/streaming) optimized for various application needs
@@ -103,6 +126,20 @@ final_state["messages"][-1].content
```
"Based on the search results, I can tell you that the current weather in San Francisco is:\n\nTemperature: 60 degrees Fahrenheit\nConditions: Foggy\n\nSan Francisco is known for its microclimates and frequent fog, especially during the summer months. The temperature of 60°F (about 15.5°C) is quite typical for the city, which tends to have mild temperatures year-round. The fog, often referred to as "Karl the Fog" by locals, is a characteristic feature of San Francisco\'s weather, particularly in the mornings and evenings.\n\nIs there anything else you\'d like to know about the weather in San Francisco or any other location?"
```
Now when we pass the same <code>"thread_id"</code>, the conversation context is retained via the saved state (i.e. stored list of messages)
```python
final_state = app.invoke(
{"messages": [{"role": "user", "content": "what about ny"}]},
config={"configurable": {"thread_id": 42}}
)
final_state["messages"][-1].content
```
```
"Based on the search results, I can tell you that the current weather in New York City is:\n\nTemperature: 90 degrees Fahrenheit (approximately 32.2 degrees Celsius)\nConditions: Sunny\n\nThis weather is quite different from what we just saw in San Francisco. New York is experiencing much warmer temperatures right now. Here are a few points to note:\n\n1. The temperature of 90°F is quite hot, typical of summer weather in New York City.\n2. The sunny conditions suggest clear skies, which is great for outdoor activities but also means it might feel even hotter due to direct sunlight.\n3. This kind of weather in New York often comes with high humidity, which can make it feel even warmer than the actual temperature suggests.\n\nIt's interesting to see the stark contrast between San Francisco's mild, foggy weather and New York's hot, sunny conditions. This difference illustrates how varied weather can be across different parts of the United States, even on the same day.\n\nIs there anything else you'd like to know about the weather in New York or any other location?"
```
</details>
> [!TIP]
@@ -198,82 +235,95 @@ final_state = app.invoke(
)
final_state["messages"][-1].content
```
<b>Step-by-step Breakdown</b>:
<details>
<summary>Initialize the model and tools.</summary>
<ul>
<li>
We use <code>ChatAnthropic</code> as our LLM. <strong>NOTE:</strong> we need to make sure the model knows that it has these tools available to call. We can do this by converting the LangChain tools into the format for OpenAI tool calling using the <code>.bind_tools()</code> method.
</li>
<li>
We define the tools we want to use - a search tool in our case. It is really easy to create your own tools - see documentation here on how to do that <a href="https://python.langchain.com/docs/how_to/custom_tools/">here</a>.
</li>
</ul>
</details>
Now when we pass the same `"thread_id"`, the conversation context is retained via the saved state (i.e. stored list of messages)
<details>
<summary>Initialize graph with state.</summary>
```python
final_state = app.invoke(
{"messages": [{"role": "user", "content": "what about ny"}]},
config={"configurable": {"thread_id": 42}}
)
final_state["messages"][-1].content
```
<ul>
<li>We initialize graph (<code>StateGraph</code>) by passing state schema (in our case <code>MessagesState</code>)</li>
<li><code>MessagesState</code> is a prebuilt state schema that has one attribute -- a list of LangChain <code>Message</code> objects, as well as logic for merging the updates from each node into the state.</li>
</ul>
</details>
```
"Based on the search results, I can tell you that the current weather in New York City is:\n\nTemperature: 90 degrees Fahrenheit (approximately 32.2 degrees Celsius)\nConditions: Sunny\n\nThis weather is quite different from what we just saw in San Francisco. New York is experiencing much warmer temperatures right now. Here are a few points to note:\n\n1. The temperature of 90°F is quite hot, typical of summer weather in New York City.\n2. The sunny conditions suggest clear skies, which is great for outdoor activities but also means it might feel even hotter due to direct sunlight.\n3. This kind of weather in New York often comes with high humidity, which can make it feel even warmer than the actual temperature suggests.\n\nIt's interesting to see the stark contrast between San Francisco's mild, foggy weather and New York's hot, sunny conditions. This difference illustrates how varied weather can be across different parts of the United States, even on the same day.\n\nIs there anything else you'd like to know about the weather in New York or any other location?"
```
<details>
<summary>Define graph nodes.</summary>
### Step-by-step Breakdown
There are two main nodes we need:
1. <details>
<summary>Initialize the model and tools.</summary>
<ul>
<li>The <code>agent</code> node: responsible for deciding what (if any) actions to take.</li>
<li>The <code>tools</code> node that invokes tools: if the agent decides to take an action, this node will then execute that action.</li>
</ul>
</details>
- we use `ChatAnthropic` as our LLM. **NOTE:** we need make sure the model knows that it has these tools available to call. We can do this by converting the LangChain tools into the format for OpenAI tool calling using the `.bind_tools()` method.
- we define the tools we want to use - a search tool in our case. It is really easy to create your own tools - see documentation here on how to do that [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools).
</details>
<details>
<summary>Define entry point and graph edges.</summary>
2. <details>
<summary>Initialize graph with state.</summary>
First, we need to set the entry point for graph execution - <code>agent</code> node.
- we initialize graph (`StateGraph`) by passing state schema (in our case `MessagesState`)
- `MessagesState` is a prebuilt state schema that has one attribute -- a list of LangChain `Message` objects, as well as logic for merging the updates from each node into the state
</details>
Then we define one normal and one conditional edge. Conditional edge means that the destination depends on the contents of the graph's state (<code>MessagesState</code>). In our case, the destination is not known until the agent (LLM) decides.
3. <details>
<summary>Define graph nodes.</summary>
<ul>
<li>Conditional edge: after the agent is called, we should either:
<ul>
<li>a. Run tools if the agent said to take an action, OR</li>
<li>b. Finish (respond to the user) if the agent did not ask to run tools</li>
</ul>
</li>
<li>Normal edge: after the tools are invoked, the graph should always return to the agent to decide what to do next</li>
</ul>
</details>
There are two main nodes we need:
<details>
<summary>Compile the graph.</summary>
- The `agent` node: responsible for deciding what (if any) actions to take.
- The `tools` node that invokes tools: if the agent decides to take an action, this node will then execute that action.
</details>
<ul>
<li>
When we compile the graph, we turn it into a LangChain
<a href="https://python.langchain.com/docs/concepts/runnables/">Runnable</a>,
which automatically enables calling <code>.invoke()</code>, <code>.stream()</code> and <code>.batch()</code>
with your inputs
</li>
<li>
We can also optionally pass checkpointer object for persisting state between graph runs, and enabling memory,
human-in-the-loop workflows, time travel and more. In our case we use <code>MemorySaver</code> -
a simple in-memory checkpointer
</li>
</ul>
</details>
4. <details>
<summary>Define entry point and graph edges.</summary>
<details>
<summary>Execute the graph.</summary>
First, we need to set the entry point for graph execution - `agent` node.
Then we define one normal and one conditional edge. Conditional edge means that the destination depends on the contents of the graph's state (`MessageState`). In our case, the destination is not known until the agent (LLM) decides.
- Conditional edge: after the agent is called, we should either:
- a. Run tools if the agent said to take an action, OR
- b. Finish (respond to the user) if the agent did not ask to run tools
- Normal edge: after the tools are invoked, the graph should always return to the agent to decide what to do next
</details>
5. <details>
<summary>Compile the graph.</summary>
- When we compile the graph, we turn it into a LangChain [Runnable](https://python.langchain.com/v0.2/docs/concepts/#runnable-interface), which automatically enables calling `.invoke()`, `.stream()` and `.batch()` with your inputs
- We can also optionally pass checkpointer object for persisting state between graph runs, and enabling memory, human-in-the-loop workflows, time travel and more. In our case we use `MemorySaver` - a simple in-memory checkpointer
</details>
6. <details>
<summary>Execute the graph.</summary>
1. LangGraph adds the input message to the internal state, then passes the state to the entrypoint node, `"agent"`.
2. The `"agent"` node executes, invoking the chat model.
3. The chat model returns an `AIMessage`. LangGraph adds this to the state.
4. Graph cycles the following steps until there are no more `tool_calls` on `AIMessage`:
- If `AIMessage` has `tool_calls`, `"tools"` node executes
- The `"agent"` node executes again and returns `AIMessage`
5. Execution progresses to the special `END` value and outputs the final state.
And as a result, we get a list of all our chat messages as output.
</details>
<ol>
<li>LangGraph adds the input message to the internal state, then passes the state to the entrypoint node, <code>"agent"</code>.</li>
<li>The <code>"agent"</code> node executes, invoking the chat model.</li>
<li>The chat model returns an <code>AIMessage</code>. LangGraph adds this to the state.</li>
<li>Graph cycles the following steps until there are no more <code>tool_calls</code> on <code>AIMessage</code>:
<ul>
<li>If <code>AIMessage</code> has <code>tool_calls</code>, <code>"tools"</code> node executes</li>
<li>The <code>"agent"</code> node executes again and returns <code>AIMessage</code></li>
</ul>
</li>
<li>Execution progresses to the special <code>END</code> value and outputs the final state. And as a result, we get a list of all our chat messages as output.</li>
</ol>
</details>
</details>
## Documentation
+2 -2
View File
@@ -31,7 +31,7 @@ def request(self, method, url, body=None, headers=None):
The result of calling the parent request method.
"""
# Update the inner socket's timeout value to send the request.
# This only triggers if the connection is re-used.
# This only triggers if the connection is reused.
if getattr(self, "sock", None) is not None:
self.sock.settimeout(self.timeout)
@@ -90,4 +90,4 @@ def patch_urllib3():
return request(self, *args, **kwargs)
connection.HTTPConnection.request = new_request
_PATCHED = True
_PATCHED = True
+6 -11
View File
@@ -1,17 +1,11 @@
import importlib
import inspect
import logging
import os
import re
from typing import List, Literal, Optional
from typing_extensions import TypedDict
from functools import lru_cache
from typing import List, Literal, Optional
import nbformat
from nbconvert.preprocessors import Preprocessor
from typing_extensions import TypedDict
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@@ -52,6 +46,8 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
(["langgraph.constants"], "langgraph.types", "Interrupt", "types"),
(["langgraph.constants"], "langgraph.types", "interrupt", "types"),
(["langgraph.constants"], "langgraph.types", "Command", "types"),
(["langgraph.func"], "langgraph.func", "entrypoint", "func"),
(["langgraph.func"], "langgraph.func", "task", "func"),
([], "langgraph.types", "RetryPolicy", "types"),
([], "langgraph.checkpoint.base", "Checkpoint", "checkpoints"),
([], "langgraph.checkpoint.base", "CheckpointMetadata", "checkpoints"),
@@ -88,8 +84,6 @@ _IMPORT_LANGCHAIN_RE = _make_regular_expression("langchain")
_IMPORT_LANGGRAPH_RE = _make_regular_expression("langgraph")
@lru_cache(maxsize=10_000)
def _get_full_module_name(module_path: str, class_name: str) -> Optional[str]:
"""Get full module name using inspect, with LRU cache to memoize results."""
@@ -109,6 +103,7 @@ def _get_full_module_name(module_path: str, class_name: str) -> Optional[str]:
logger.warning(f"API Reference: Failed to load for class {class_name}, {e}")
return None
def _get_doc_title(data: str, file_name: str) -> str:
try:
return re.findall(r"^#\s*(.*)", data, re.MULTILINE)[0]
@@ -287,4 +282,4 @@ def update_markdown_with_imports(markdown: str) -> str:
# Apply the replace_code_block function to all matches in the markdown
updated_markdown = code_block_pattern.sub(replace_code_block, markdown)
return updated_markdown
return updated_markdown
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
eNqNVVtMHFUYXnoleKnGSL01nG4qJHVnd2Z2C+z6oLBQ2yIXYZtKUcnZmcPOwMyc6cxZ2uVmhPqk0M5TE/WpLLtmxQKxalP1SUiqVmyaNkqjyIsxhgRjfCs2eGbZ5VJu3aeZ8//f/33//51/tjfZjgxTxlrOsKwRZECB0BfT6k0a6FQUmeRsQkVEwmK8rrYhNBg15KnnJUJ0M+DxQF12Q41IBtZlwS1g1dPOeVRkmjCCzHgYi7E7ObmdThWeaSa4DWmmMwA4lve5gDObRU+aOp0GVhB9ckZNZDhpVMBUikbsI0neD44WqSCMw85uF1jOhaYpm4TS3wc4IoNyHF4EBRUYFZEb1MgCAgQDFSECYji6HxzBp4EANXAUSEjR7TMaF2HspdUk6wg6LUFSZAI1BjSoIpr/pt0OFpFiR4U0I+NlDjEm1jREGAUSOkVnd1JCUKSjPheXsEmssTXDG4GCgHTCIE3AoqxFrE8jHbLuAiJqsWukBLte2h0r1YaQzkBFbkeJRZQ1CnVdkQVoxz2tlHs4o5khMR2tDafs1hhqgUasL8uyOjx1Meq1Bli31+fmR88wdMKyplCzaB9UUkJPx79aGdCh0EbrMJl7ZCUWwZdW5mDTGqqGQm3DqpLQECRrCBpqse+zledGVCOyiqxksG4tXSa4TOd1c5zbP7aqsBnTBGuoBSomGlsa8hIkxbO8l2GLGZa7lJ2SgrQIkaxBvtj3sYFMne4A6kvQkiRq9sapI+j6tWTm1l6srcq6Oe3YG6+g7ljfnECiC/A8OEZvFa1/CPBcgPUHOA68Uh0aDmZoQuuaMRYyoGa2UEMqs+YnBSmqtSExFVzX9innclsG5VdkVSZMZmWpWfarFfexLDtVuGmmgVQ6NZsx7vX7/VvUpZNBxLps90eHx/B8KNMle3J9HlnTo/QKprc/oyphq6K6Dm6Zv6wtiyl8AMwGCrmTU0XroXGUrJE4VJpme2Hr/GWJGUzRg2A2lgjWg983vkWiA5tkrhzcYjbYNHtDPamM84wsWl/T52aWa2wMHe84Vl7devzVGkmr9tbprWYJGmyXoZXi3ByIYBxR0EjwMBOEgoSYhvQKWcmKxpqy6qPB4deZehzG9C6FIL1zGtZQogEZdDWtlKDgqEg/dgZKUHh9WaN12c8W86XFLB+GXlQi8j6m8kT9aHaZlpYlbn8p038479CVNejRRE5OwXu5jvRvu3iurGr85YffXfj+/L+hOyc95huP7ZmdftLiJ8d/u6TWFz09ytz7++bEzomGmfnpzvM7r+Y+usv/Bfr87dLrt7u6uj6Y++mvy1Xeu9/27Hgo0H9lqG684HHXgbmSmcrAa7V5+bsGfunruzH/3NgVZi4POm9ddM8mDs4+49pRWFmve36Ihv5p+6R5oC8y+d35ghsk3P5fS/9e76niP12tgZvj3O73n63qvTf/yPBIfkVMeaIDbtt9ZOz2R7k9Z2cih/NyB4zfm65d8Py6bV9/Y9Mfew7k/Hwrv3dPkRIsH9nXMF9S91Z3ecHCi5N3Pwz9eLVnu8OxsLDdIZY/lWrOcTj+B1mCCwM=
@@ -0,0 +1 @@
eNqNVGtMHFUUXoQ0FavRGAt/bCcThCrMMjMsWxarlAIlpVIorKW8bG5nLuzAvJjH8rKmYKkxlbRDUxoTCElZdulKYRGMTaiExpKYtFFrDbAxaeIfpRok0USNreDdZReovDrJJDP3nO+c73znnNvqcUJF5SQxYoATNagARkM/qtHqUWCdDlXtjFuAmkNiXYUFxfZeXeH8rzg0TVbTk5OBzJmBqDkUSeYYMyMJyU4qWYCqCqqh6jopsY3++mZcAA0nNKkWiiqejlEkbUnC8LATOilvxhWJh+gL11Wo4MjKSIiJqAWO6h1AS1AxoRETgQAz8FOVAbTEQj5gZXigs5BIIVIJVRJFqBE80BBn/JTHAQGLCjvvckiqZgyvoToEGAbKGgFFRmI5sdq4Vt3EyUkYC6sCMbxMIF5QC8NbC6FMAJ5zQvcSyvABWeY5BgTsyTUo90CIM6E1ynCt2RsojUAVi5rxeWaYR3JhI1JWxEhzisVM+xoIVQOcyCNtUB2IklsO2sdWG2TA1KI4RKhrhnsJPLjaR1KNvnzAFBQ/FhIojMPoA4pgtYysPld0UeMEaHiyCtemCxlX0qWYKcpsG34ssNooMkZfFeBVOLws8jLES5N0CkFaCZIaDKvEQ7Facxi9FGXrV6Aqo4mD77tRSE1XW12oI/DOV57QkFwpOBzu5n1TjCsbdcf4ogSySRhNY3lAxFD8VIym0klbOkVjufn2gaxQGvu6zRi2K0BUq1BDcsLN9zAOXayFrDdr3bb78ZWyFJSf5wROI0ILgpoV+DVcFpIk/fGbeipQQKoFMrpSbDbbFnGRMlAzRgP1IfEImraHqqTK1s/DibKORjC4bCFW7gArxOu1Lf1XuIUx8U+A2YAhXeZPWA8t6doain1pwWyJW/uvUAxhEp4EszFFbD34/+RbShS3iedq4Za8sU29N+TjDXWe4FjjBvo+QVJHGw5AzuI8mi2WHM+rLSnJrSm0C1KvkwOGlzJTWLUkVfNwKOsgkQUYBySKgytkeLJLj2TmH8oaOE4USSclNEt2gGZOlEToLoYKWk3Dy/CSzqLLToFuBC/KLDVGbaSVTrOSaawtjdzL0hYip6TIF16m5WVxBW7K4PXeglZWQUeTEW/vPrfdFHwi0bu4yF7IPxyZuePsYt6dyduDo+VvXI95tb/w23Otu7I766z6B1/H6x1XO79bNI+V9TdE75vPPeCMLRsYnv6riu7O2Nc01nP3J+fDR1pGzPi/12+Mf/mUMOa+meP9rds3EVdnOpLab7Mm+S534lOzvbeJSyO/X7GD+MRPDulvdttf2jnb1j7je8//cl77+Nm/02oqTv/RWflRuaOt4NljST2+S12nKwsu2Dp+MU3sOTj7IErsmp94RtpzdQTb0XMzffbp7yvoNojPRRe2TEq1P1ujWi53/NNzpvkiz8zd3TZXgW93bvumg3n909hjUfHPz3SM+u81dBVVvPPjwmf7U0v3ftgVzbTfEhgbtRCdt/vjmRenupgk8vziQH1sQ/c1qvSFxKl73v3z7+a2F1h2zTxKfeu5Jnx6YTruB8o29OdDKahdpOnXnbfuP4gwmf4DBVoOEg==
@@ -0,0 +1 @@
eNqNVF1sFFUUbmlTEYPBYCTGGIahpQY6uzOz28q2prpua2Vr6d+G/gji7czt7tCZucPMncqKgJZiNI3iGAzExBe7P82mlFZRVGqEGgTUpn0huCiYJhp5MvEBmz7VO9vdttilZZ7u3HO+c77znXNuT7wb6oaE1NxBScVQBwImP4bVE9fhfhMauDemQBxCYqShvjnQb+pSsiiEsWaUO51AkxxAxSEdaZLgEJDi7OacCjQMEIRGpAOJ4aRxkFbAgb0YdUHVoMspjuXdJRSdcSI3Lx+kdSRDcqJNA+o0sQqIMFGxfRWSNlE7ihWqA3XQh/bYSCRC2bYIMjBFyLiYUsZAqgoxIwNM+NKH4iEIRFLU8UgIGdgaWULzDBAEqGEGqgISJTVonQ6+LmkllAg77RgJwY6X0sFKdEGoMUCWumFsDmUNA02TJQHYduc+knswzZfBYQ0uNSfsshhSrYqtc94MD2dDmKiqUqzD5XbwwwcYAwNJlYkupA5CKaal7OcXGzQgdJE4TLpjVmwOPLTYBxlWtA4I9c13hQS6ELKiQFfK3J8vvtdNFUsKtOK+hqXp0saFdC4Hxzk8I3cFNsKqYEU7gWzAkXmR5yEJnuVdDFvGsNxQRiUZqkEcsvo5rnRAh4ZGpg0ejZGQ2DR6IqQj8Ocr8fSAfFpfm+nmrZwNkSrSHevbFiiWUDxP+YFKkfilFM+Vs55yjqVq6gKDvnSaQNZmjAR0oBqdpCHVmebHhZCpdkEx4cva9iS9UJZO8suSImEmvRykWfavFXGzLJvcsqynDhWimp0x4vJ4PCvEJcpAbJ216yPiMTwfmKuS9bRnzyOpmklGMLVoaVYxmxXhtXVF/wVuGcyW+8BkZ8ix7cnibGhk4iUUo9tT2bat7L9AMY0pvh/MvSlS2eD/k28uUeEynouFm/OmlvW+J59EuvOMJFqj5LyX5ZqQx2sohrC/1CeJ1Ri5db/ZsLO/WwJWgnNwVBChoAzP+F5gfEAIQaY5tUJWvKptp7duh2+wlWlCHYjMUgCQmVORCmPNUCeraSUEGZkieex0GCPwJm+bddbDlvHbSzuhS+Q6nhZ5N1Pd0jScWab5ZYnYL2XqaX+LrKxOri7lrtvYtzon9eWJx7213z+37tjsj92OV8o+lO98tsHbeKNvYPUaGtx88tfo1Zc+/iT69uSs48HHHq3P+3361tHHNxUMT8bq9uw6F579qqL35PWnJl6rv6nPJHdN5bqvTE6dGDvxW4HXNzGev9vPvbs5dy3zgHyhB+1+uLWk7yeae8QP/EbjRz+8WTBR+OKqf5Vn3ltzuO7OTGtrtTm+8aEvKruujY/iob/7a4oG26sa8/461v6PN1yz7/SXv+D1zWMX17Zob1z/oPf8NzUnT11Y1TRQNJ3/LD1VuTmcX/u15P5jpqji+Yijlhn1+V/d1n+x4rtLN6ZvF/LXjuT9eXX69tiRy22H1wvvX0ak9NnZvJzkE+9Uyrk5Of8BO5PlsA==
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
eNqNVg1QFOcZBolN7GjHTH9itNHNpQ1pwx67t/eLQxUO1IPiAXcBCejN3u53d8vtH/vtAndIGiVNk+p0umla07+MCty1lIgWxxiFtLZjGrWTTKqTBBmjHWPaxNqa2KZpScZ+u3cnh0LKzcDd7vd87/u87/u87/dtT3cCBXKSWDjMiSpQaEZFD1DfnlZAhwag+lhKAGpMYgfq/YFgv6ZwE1+PqaoMy0pLaZmzSjIQac7KSEJpJ1nKxGi1FP2WeWCaGQhLbOLswmSPRQAQ0lEALWVYa4+FkZAvUUUPFi8tYglJwxRAMzFM0lQsIilYTBPQexpCDqq0yIAyrAuZxmBM0ngW82ERAFhMSGAMra7BAkCmFVoFfKIEvciYY2KAiWNqDGBdgEZfCsaJWAAtrlOQPQ4y0hpLCWZRJB4YLDQIFEtvCZZPTdR4Pg+SI6Ma+1RJ4kMMzfPZgNSEbIIimmgm0MBwrPHGAIWaKxrXRyjXJoW2c+5kNdlUJ8tckkkasBtbypB3kRZMO2b8oen4DSCtRDUBUTNcWnraLKg8SqINPbRZmudMTpul19JrBvZ/KLKxCp9f2CiF6/2CO+5d93B9kGsig+ScFKNADWVzOxs7XkLu0Z4MwRmZz3DafFO6cz/Li1tQ/bLBmKGgeipGMBiNRTgYsxZjBofy4puTVIzdKEuIY8uLPzX1xfnlN/bNKGtoPsWbK4A2i08thhjURDFxb5slyzYvX7MSnasA8yc6Zwl7N6MtgsQC3iycrOJ2CRc4kTNMQRW1noAWIjQPQdb2p8h6PmJgAWQUTs7CUI/zPAoZQ6gZLRlRJAEVFcqA4SIcg+U0YzVsGD2NRg+aTqYnWUGjRlE5kHnMQc2HHE0UCidGTb1bjPHFKcDITus02khEDi2F2wGjIvSc7THv1rwp3sbM6MybX7lQzc3zCM/s7fnFloHOHtjm3nQM0Cxy8lbB0oGYBFV938ypPUIzDECCACIjsciB/lw0ycklGAsiPBqpQ0jTIjCToA/FAZBxmuc6QSqzS99PyzLPZXJb2g4lcTjbA7jB5dblIWPO4ugcEFX9oB+RqPCV1ifQ8SJipNXhsbr2d+MoYZzIo+MC52nEJyWb60fzF2SaiSMjePbo0lOZzfvyMRLUB+toxh+YYZJWmJg+SCuC0z6a/17RRJUTgJ721t/qLrs47Y6ykqTVdWCGYZgQGX3Q7KHnZ2wGqpLAGQnZ0PcQKUaS4hzQJz4IhZhIKCyUuwhXtVZbXeeqo7wNVEttVXuTn22qd5FeOlGzrkHtslkbeC3pDdItOOmiXA63y+N24qSVsJJWEue4EE8+/BBR46vs7uCVECGEKpkOyt3c2R0VSU+wocMuyC0V4KFOxSV6Ic0yG0O+aD3cwEQYpVGVgh1OspojK6VgIFHF+iKUR5PqoqsxxE7rRLOJrKvaIG9wQ627vSUQiQfbG75JOaqiXZWsfYOo+atq1quO+i4uoia78ui57C6cyDJ0EnY3YXz25bTBAzGqxvQB0k7YfqEAKKObAuhLoZypGtw+gIQI/vhyOntj2OuvndbwlwaqkCj18WbAlmA2G1aDjhUbYXNgNqqM9JTZXdj6uuCwN+snOKsGDwTROQQjSIfVOc2nmZgmxgE75J1V7eOG2lEpDf5oEOOgW5YgwLOs9OFNeLbhcV/VaKa1cEmJ0iKXNN3q46bsu5LdXSyjsWyss0sgPEk7xYWBxkQOZregEWC4QYRwAer9Tsq+L7uSE94QipXASQInyCPduHnl4QQOJdT8n72wQX3AgbJ9+FaAKsUButql7WY5iBfzEQoQkGIN39Nm7B6PZ2x2UM4UhSAegjgyEwVBPhvSJsDDtwKyJvpJAQ535+A4x+oTX0EPIcrtIEGE9HhchM3udLOUjXKG3U7W5aZp2uFyvmAMRAaZMaopS4qKQ8Cg66ma0CdKBLrbmDLlFOmgnCjU1ej2x/AaCwJauEoygoCrMVkBvESzI951uBfdPQEeMAWop6taNlbU+byHNuH5SsL9cuZqnBYlKHKRSCoAFFQZfYjhJY1F41IBKWSrsaJFP+ghUPkcDocbsDY3S7rwSjSIctZu6G7AmLVpmkfcOxl9NEaVW8rsdsqyGhPocrcT1cm8QG9LZYb/8cJ/rNpxR4H5KUJ/16/vbDwlThJLxy4/eGjsn5aF31ulX261Vpyrfe3+XXsX3faTNp/6tcZL1sll266P7/74+32ptaW3HR6lnpk4t2vx2mV4ExZ8sEUbx8VFb5y8+vw7137/SXokPrX130t/fGg0OXXt1Hq9+PHzfMP7T2qDW99oZS+eK2wq/CrTOG6NXgq3PLLprcVr/+I4WxVPWt958a87qeOq7ed3/eDXh7esnHjO9Vnf8IWezxRctY2dSJ/+qKSJOOH/4eeeZPvCmrhg6bHJ4J1Pvby09g+WVRWX7ulfMTxy7+4g3n5V3/Nh4dk3fbcXP/u7Lw+c/i3+90fDpzEyNcK++Z87zvw35jjzRNknRQ+cqF/RPrLyXP/RwYqnWh87/8ALxwteOjauyncNBB5/+tVjV3sWfPeVK4+O/+ho166motd1dvmuHa+vvDwFOy//9BsfLbnnfTjufKW+deHTb28+6ftbD7l7avEF7pmOrQ7na7ef/1fbgevbrIs2Rnf0PbusefXe0XfvZF/905b94smPl4zX3FfLvBuurVzSt+qXF2vAmZdCBRPV7/3m3Nhg7wax7dqaD+7b/YUPdx55r6i9f0vxyskFqVPXmycTJ/voimWP3P3tL65YzH1+efzuK39eufDKEwd+1X5x+eJe4jv9w1NDP9trvRQhmy2T0W+ZVS0quH/Phbf3ohL/D1RMKFk=
+2 -2
View File
@@ -1,5 +1,5 @@
ERROR_FOUND=0
for file in $(find $1 -name "*.ipynb"); do
for file in $(find $1 -name "*.ipynb" | grep -v ".ipynb_checkpoints"); do
OUTPUT=$(cat "$file" | jupytext --from ipynb --to py:percent | codespell -)
if [ -n "$OUTPUT" ]; then
echo "Errors found in $file"
@@ -10,4 +10,4 @@ done
if [ "$ERROR_FOUND" -ne 0 ]; then
exit 1
fi
fi
+936
View File
@@ -0,0 +1,936 @@
# Functional API
!!! warning "Beta"
The Functional API is currently in **beta** and is subject to change. Please [report any issues](https://github.com/langchain-ai/langgraph/issues) or feedback to the LangGraph team.
## Overview
The Functional API is an alternative to [Graph API (StateGraph)](low_level.md#stategraph) for development in LangGraph.
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.
## Example
Below we demonstrate a simple application that writes an essay and [interrupts](human_in_the_loop.md) to request human review.
```python
from langgraph.func import entrypoint, task
from langgraph.types import interrupt
@task
def write_essay(topic: str) -> str:
"""Write an essay about the given topic."""
time.sleep(1) # A placeholder for a long-running task.
return f"An essay about topic: {topic}"
@entrypoint(checkpointer=MemorySaver())
def workflow(topic: str) -> dict:
"""A simple workflow that writes an essay and asks for a review."""
essay = write_essay("cat").result()
is_approved = interrupt({
# Any json-serializable payload provided to interrupt as argument.
# It will be surfaced on the client side as an Interrupt when streaming data
# from the workflow.
"essay": essay, # The essay we want reviewed.
# We can add any additional information that we need.
# For example, introduce a key called "action" with some instructions.
"action": "Please approve/reject the essay",
})
return {
"essay": essay, # The essay that was generated
"is_approved": is_approved, # Response from HIL
}
```
??? example "Detailed Explanation"
This workflow will write an essay about the topic "cat" and then pause to get a review from a human. The workflow can be interrupted for an indefinite amount of time until a review is provided.
When the workflow is resumed, it executes from the very start, but because the result of the `write_essay` task was already saved, the task result will be loaded from the checkpoint instead of being recomputed.
```python
import time
import uuid
from langgraph.func import entrypoint, task
from langgraph.types import interrupt
from langgraph.checkpoint.memory import MemorySaver
@task
def write_essay(topic: str) -> str:
"""Write an essay about the given topic."""
time.sleep(1) # This is a placeholder for a long-running task.
return f"An essay about topic: {topic}"
@entrypoint(checkpointer=MemorySaver())
def workflow(topic: str) -> dict:
"""A simple workflow that writes an essay and asks for a review."""
essay = write_essay("cat").result()
is_approved = interrupt({
# Any json-serializable payload provided to interrupt as argument.
# It will be surfaced on the client side as an Interrupt when streaming data
# from the workflow.
"essay": essay, # The essay we want reviewed.
# We can add any additional information that we need.
# For example, introduce a key called "action" with some instructions.
"action": "Please approve/reject the essay",
})
return {
"essay": essay, # The essay that was generated
"is_approved": is_approved, # Response from HIL
}
thread_id = str(uuid.uuid4())
config = {
"configurable": {
"thread_id": thread_id
}
}
for item in workflow.stream("cat", config):
print(item)
```
```pycon
{'write_essay': 'An essay about topic: cat'}
{'__interrupt__': (Interrupt(value={'essay': 'An essay about topic: cat', 'action': 'Please approve/reject the essay'}, resumable=True, ns=['workflow:f7b8508b-21c0-8b4c-5958-4e8de74d2684'], when='during'),)}
```
An essay has been written and is ready for review. Once the review is provided, we can resume the workflow:
```python
from langgraph.types import Command
# Get review from a user (e.g., via a UI)
# In this case, we're using a bool, but this can be any json-serializable value.
human_review = True
for item in workflow.stream(Command(resume=human_review), config):
print(item)
```
```pycon
{'workflow': {'essay': 'An essay about topic: cat', 'is_approved': False}}
```
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:
- **[Entrypoint](#entrypoint)**: An **entrypoint** is a decorator that designates a function as the starting point of a workflow. It encapsulates workflow logic and manages execution flow, including handling *long-running tasks* and [interrupts](human_in_the_loop.md).
- **[Task](#task)**: Represents a discrete unit of work, such as an API call or data processing step, that can be executed asynchronously from within an **entrypoint**. Invoking a **task** returns a future-like object, which can be awaited to obtain the result or resolved synchronously.
## Entrypoint
The [`@entrypoint`][langgraph.func.entrypoint] decorator can be used to create a workflow from a function. It encapsulates workflow logic and manages execution flow, including handling *long-running tasks* and [interrupts](./low_level.md#interrupt).
### Definition
An **entrypoint** is defined by decorating a function with the `@entrypoint` decorator.
The function **must accept a single positional argument**, which serves as the workflow input. If you need to pass multiple pieces of data, use a dictionary as the input type for the first argument.
Decorating a function with an `entrypoint` produces a Pregel instance which helps to manage the execution of the workflow (e.g., handles streaming, resumption, and checkpointing).
You will usually want to pass a **checkpointer** to the `@entrypoint` decorator to enable persistence and use features like **human-in-the-loop**.
=== "Sync"
```python
from langgraph.func import entrypoint
@entrypoint(checkpointer=checkpointer)
def my_workflow(some_input: dict) -> int:
# some logic that may involve long-running tasks like API calls,
# and may be interrupted for human-in-the-loop.
...
return result
```
=== "Async"
```python
from langgraph.func import entrypoint
@entrypoint(checkpointer=checkpointer)
async def my_workflow(some_input: dict) -> int:
# some logic that may involve long-running tasks like API calls,
# and may be interrupted for human-in-the-loop
...
return result
```
!!! important "Serialization"
The **inputs** and **outputs** of entrypoints must be JSON-serializable to support checkpointing. Please see the [serialization](#serialization) section for more details.
### Injectable Parameters
When declaring an `entrypoint`, you can request access to additional parameters that will be injected automatically at run time. These parameters include:
| Parameter | Description |
|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
| **previous** | Access the the state associated with the previous `checkpoint` for the given thread. See [state management](#state-management). |
| **store** | An instance of [BaseStore][langgraph.store.base.BaseStore]. Useful for [long-term memory](#long-term-memory). |
| **writer** | For streaming custom data, to write custom data to the `custom` stream. Useful for [streaming custom data](#streaming-custom-data). |
| **config** | For accessing run time configuration. See [RunnableConfig](https://python.langchain.com/docs/concepts/runnables/#runnableconfig) for information. |
!!! important
Declare the parameters with the appropriate name and type annotation.
??? example "Requesting Injectable Parameters"
```python
from langchain_core.runnables import RunnableConfig
from langgraph.func import entrypoint
from langgraph.store.base import BaseStore
from langgraph.store.memory import InMemoryStore
in_memory_store = InMemoryStore(...) # An instance of InMemoryStore for long-term memory
@entrypoint(
checkpointer=checkpointer, # Specify the checkpointer
store=in_memory_store # Specify the store
)
def my_workflow(
some_input: dict, # The input (e.g., passed via `invoke`)
*,
previous: Any = None, # For short-term memory
store: BaseStore, # For long-term memory
writer: StreamWriter, # For streaming custom data
config: RunnableConfig # For accessing the configuration passed to the entrypoint
) -> ...:
```
### Executing
Using the [`@entrypoint`](#entrypoint) yields a Pregel object that can be executed using the `invoke`, `ainvoke`, `stream`, and `astream` methods.
=== "Invoke"
```python
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
my_workflow.invoke(some_input, config) # Wait for the result synchronously
```
=== "Async Invoke"
```python
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
await my_workflow.ainvoke(some_input, config) # Await result asynchronously
```
=== "Stream"
```python
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
for chunk in my_workflow.stream(some_input, config):
print(chunk)
```
=== "Async Stream"
```python
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
async for chunk in my_workflow.astream(some_input, config):
print(chunk)
```
### Resuming
Resuming an execution after an [interrupt][langgraph.types.interrupt] can be done by passing a **resume** value to the [Command][langgraph.types.Command] primitive.
=== "Invoke"
```python
from langgraph.types import Command
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
my_workflow.invoke(Command(resume=some_resume_value), config)
```
=== "Async Invoke"
```python
from langgraph.types import Command
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
await my_workflow.ainvoke(Command(resume=some_resume_value), config)
```
=== "Stream"
```python
from langgraph.types import Command
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
for chunk in my_workflow.stream(Command(resume=some_resume_value), config):
print(chunk)
```
=== "Async Stream"
```python
from langgraph.types import Command
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
async for chunk in my_workflow.astream(Command(resume=some_resume_value), config):
print(chunk)
```
**Resuming after an error**
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
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
my_workflow.invoke(None, config)
```
=== "Async Invoke"
```python
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
await my_workflow.ainvoke(None, config)
```
=== "Stream"
```python
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
for chunk in my_workflow.stream(None, config):
print(chunk)
```
=== "Async Stream"
```python
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
async for chunk in my_workflow.astream(None, config):
print(chunk)
```
### State Management
When an `entrypoint` is defined with a `checkpointer`, it stores information between successive invocations on the same **thread id** in [checkpoints](persistence.md#checkpoints).
This allows accessing the state from the previous invocation using the `previous` parameter.
By default, the `previous` parameter is the return value of the previous invocation.
```python
@entrypoint(checkpointer=checkpointer)
def my_workflow(number: int, *, previous: Any = None) -> int:
previous = previous or 0
return number + previous
config = {
"configurable": {
"thread_id": "some_thread_id"
}
}
my_workflow.invoke(1, config) # 1 (previous was None)
my_workflow.invoke(2, config) # 3 (previous was 1 from the previous invocation)
```
#### `entrypoint.final`
[entrypoint.final][langgraph.func.entrypoint.final] is a special primitive that can be returned from an entrypoint and allows **decoupling** the value that is **saved in the checkpoint** from the **return value of the entrypoint**.
The first value is the return value of the entrypoint, and the second value is the value that will be saved in the checkpoint. The type annotation is `entrypoint.final[return_type, save_type]`.
```python
@entrypoint(checkpointer=checkpointer)
def my_workflow(number: int, *, previous: Any = None) -> entrypoint.final[int, int]:
previous = previous or 0
# This will return the previous value to the caller, saving
# 2 * number to the checkpoint, which will be used in the next invocation
# for the `previous` parameter.
return entrypoint.final(value=previous, save=2 * number)
config = {
"configurable": {
"thread_id": "1"
}
}
my_workflow.invoke(3, config) # 0 (previous was None)
my_workflow.invoke(1, config) # 6 (previous was 3 * 2 from the previous invocation)
```
## Task
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
Tasks are defined using the `@task` decorator, which wraps a regular Python function.
```python
from langgraph.func import task
@task()
def slow_computation(input_value):
# Simulate a long-running operation
...
return result
```
!!! important "Serialization"
The **outputs** of tasks must be JSON-serializable to support checkpointing.
### Execution
**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"
```python
@entrypoint(checkpointer=checkpointer)
def my_workflow(some_input: int) -> int:
future = slow_computation(some_input)
return future.result() # Wait for the result synchronously
```
=== "Asynchronous Invocation"
```python
@entrypoint(checkpointer=checkpointer)
async def my_workflow(some_input: int) -> int:
return await slow_computation(some_input) # Await result asynchronously
```
## When to use a task
**Tasks** are useful in the following scenarios:
- **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
There are two key aspects to serialization in LangGraph:
1. `@entrypoint` inputs and outputs must be JSON-serializable.
2. `@task` outputs must be JSON-serializable.
These requirements are necessary for enabling checkpointing and workflow resumption. Use python primitives
like dictionaries, lists, strings, numbers, and booleans to ensure that your inputs and outputs are serializable.
Serialization ensures that workflow state, such as task results and intermediate values, can be reliably saved and restored. This is critical for enabling human-in-the-loop interactions, fault tolerance, and parallel execution.
Providing non-serializable inputs or outputs will result in a runtime error when a workflow is configured with a checkpointer.
## Determinism
To utilize features like **human-in-the-loop**, any randomness should be encapsulated inside of **tasks**. This guarantees that when execution is halted (e.g., for human in the loop) and then resumed, it will follow the same *sequence of steps*, even if **task** results are non-deterministic.
LangGraph achieves this behavior by persisting **task** and [**subgraph**](./low_level.md#subgraphs) results as they execute. A well-designed workflow ensures that resuming execution follows the *same sequence of steps*, allowing previously computed results to be retrieved correctly without having to re-execute them. This is particularly useful for long-running **tasks** or **tasks** with non-deterministic results, as it avoids repeating previously done work and allows resuming from essentially the same
While different runs of a workflow can produce different results, resuming a **specific** run should always follow the same sequence of recorded steps. This allows LangGraph to efficiently look up **task** and **subgraph** results that were executed prior to the graph being interrupted and avoid recomputing them.
## Idempotency
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.
## Common Pitfalls
### Handling side effects
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, so it will be executed a second time when resuming the workflow.
```python
@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
# This code will be executed a second time when resuming the workflow.
# Which is likely not what you want.
# highlight-next-line
with open("output.txt", "w") as f:
# highlight-next-line
f.write("Side effect executed")
value = interrupt("question")
return value
```
=== "Correct"
In this example, the side effect is encapsulated in a task, ensuring consistent execution upon resumption.
```python
from langgraph.func import task
# highlight-next-line
@task
# highlight-next-line
def write_to_file():
with open("output.txt", "w") as f:
f.write("Side effect executed")
@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
# The side effect is now encapsulated in a task.
write_to_file().result()
value = interrupt("question")
return value
```
### Non-deterministic control flow
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"
In this example, the workflow uses the current time to determine which task to execute. This is non-deterministic because the result of the workflow depends on the time at which it is executed.
```python
from langgraph.func import entrypoint
@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
t0 = inputs["t0"]
# highlight-next-line
t1 = time.time()
delta_t = t1 - t0
if delta_t > 1:
result = slow_task(1).result()
value = interrupt("question")
else:
result = slow_task(2).result()
value = interrupt("question")
return {
"result": result,
"value": value
}
```
=== "Correct"
In this example, the workflow uses the input `t0` to determine which task to execute. This is deterministic because the result of the workflow depends only on the input.
```python
import time
from langgraph.func import task
# highlight-next-line
@task
# highlight-next-line
def get_time() -> float:
return time.time()
@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
t0 = inputs["t0"]
# highlight-next-line
t1 = get_time().result()
delta_t = t1 - t0
if delta_t > 1:
result = slow_task(1).result()
value = interrupt("question")
else:
result = slow_task(2).result()
value = interrupt("question")
return {
"result": result,
"value": value
}
```
## Patterns
Below are a few simple patterns that show examples of **how to** use the **Functional API**.
When defining an `entrypoint`, input is restricted to the first argument of the function. To pass multiple inputs, you can use a dictionary.
```python
@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
value = inputs["value"]
another_value = inputs["another_value"]
...
my_workflow.invoke({"value": 1, "another_value": 2})
```
### Parallel execution
Tasks can be executed in parallel by invoking them concurrently and waiting for the results. This is useful for improving performance in IO bound tasks (e.g., calling APIs for LLMs).
```python
@task
def add_one(number: int) -> int:
return number + 1
@entrypoint(checkpointer=checkpointer)
def graph(numbers: list[int]) -> list[str]:
futures = [add_one(i) for i in numbers]
return [f.result() for f in futures]
```
### Calling subgraphs
The **Functional API** and the [**Graph API**](./low_level.md) can be used together in the same application as they share the same underlying runtime.
```python
from langgraph.func import entrypoint
from langgraph.graph import StateGraph
builder = StateGraph()
...
some_graph = builder.compile()
@entrypoint()
def some_workflow(some_input: dict) -> int:
# Call a graph defined using the graph API
result_1 = some_graph.invoke(...)
# Call another graph defined using the graph API
result_2 = another_graph.invoke(...)
return {
"result_1": result_1,
"result_2": result_2
}
```
### Calling other entrypoints
You can call other **entrypoints** from within an **entrypoint** or a **task**.
```python
@entrypoint() # Will automatically use the checkpointer from the parent entrypoint
def some_other_workflow(inputs: dict) -> int:
return inputs["value"]
@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
value = some_other_workflow.invoke({"value": 1})
return value
```
### Streaming custom data
You can stream custom data from an **entrypoint** by using the `StreamWriter` type. This allows you to write custom data to the `custom` stream.
```python
from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import entrypoint, task
from langgraph.types import StreamWriter
@task
def add_one(x):
return x + 1
@task
def add_two(x):
return x + 2
checkpointer = MemorySaver()
@entrypoint(checkpointer=checkpointer)
def main(inputs, writer: StreamWriter) -> int:
"""A simple workflow that adds one and two to a number."""
writer("hello") # Write some data to the `custom` stream
add_one(inputs['number']).result() # Will write data to the `updates` stream
writer("world") # Write some more data to the `custom` stream
add_two(inputs['number']).result() # Will write data to the `updates` stream
return 5
config = {
"configurable": {
"thread_id": "1"
}
}
for chunk in main.stream({"number": 1}, stream_mode=["custom", "updates"], config=config):
print(chunk)
```
```pycon
('updates', {'add_one': 2})
('updates', {'add_two': 3})
('custom', 'hello')
('custom', 'world')
('updates', {'main': 5})
```
!!! important
The `writer` parameter is automatically injected at run time. It will only be injected if the
parameter name appears in the function signature with that *exact* name.
### Retry policy
```python
from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import entrypoint, task
from langgraph.types import RetryPolicy
attempts = 0
# Let's configure the RetryPolicy to retry on ValueError.
# The default RetryPolicy is optimized for retrying specific network errors.
retry_policy = RetryPolicy(retry_on=ValueError)
@task(retry=retry_policy)
def get_info():
global attempts
attempts += 1
if attempts < 2:
raise ValueError('Failure')
return "OK"
checkpointer = MemorySaver()
@entrypoint(checkpointer=checkpointer)
def main(inputs, writer):
return get_info().result()
config = {
"configurable": {
"thread_id": "1"
}
}
main.invoke({'any_input': 'foobar'}, config=config)
```
```pycon
'OK'
```
### Resuming after an error
```python
import time
from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import entrypoint, task
from langgraph.types import StreamWriter
# Global variable to track the number of attempts
attempts = 0
@task()
def get_info():
"""
Simulates a task that fails once before succeeding.
Raises an exception on the first attempt, then returns "OK" on subsequent tries.
"""
global attempts
attempts += 1
if attempts < 2:
raise ValueError("Failure") # Simulate a failure on the first attempt
return "OK"
# Initialize an in-memory checkpointer for persistence
checkpointer = MemorySaver()
@task
def slow_task():
"""
Simulates a slow-running task by introducing a 1-second delay.
"""
time.sleep(1)
return "Ran slow task."
@entrypoint(checkpointer=checkpointer)
def main(inputs, writer: StreamWriter):
"""
Main workflow function that runs the slow_task and get_info tasks sequentially.
Parameters:
- inputs: Dictionary containing workflow input values.
- writer: StreamWriter for streaming custom data.
The workflow first executes `slow_task` and then attempts to execute `get_info`,
which will fail on the first invocation.
"""
slow_task_result = slow_task().result() # Blocking call to slow_task
get_info().result() # Exception will be raised here on the first attempt
return slow_task_result
# Workflow execution configuration with a unique thread identifier
config = {
"configurable": {
"thread_id": "1" # Unique identifier to track workflow execution
}
}
# This invocation will take ~1 second due to the slow_task execution
try:
# First invocation will raise an exception due to the `get_info` task failing
main.invoke({'any_input': 'foobar'}, config=config)
except ValueError:
pass # Handle the failure gracefully
```
When we resume execution, we won't need to re-run the `slow_task` as its result is already saved in the checkpoint.
```python
main.invoke(None, config=config)
```
```pycon
'Ran slow task.'
```
### Human-in-the-loop
The functional API supports [human-in-the-loop](human_in_the_loop.md) workflows using the `interrupt` function and the `Command` primitive.
Please see the following examples for more details:
* [How to wait for user input (Functional API)](../how-tos/wait-user-input-functional.ipynb): Shows how to implement a simple human-in-the-loop workflow using the functional API.
* [How to review tool calls (Functional API)](../how-tos/review-tool-calls-functional.ipynb): Guide demonstrates how to implement human-in-the-loop workflows in a ReAct agent using the LangGraph Functional API.
### Short-term memory
[State management](#state-management) using the **previous** parameter and optionally using the `entrypoint.final` primitive can be used to implement [short term memory](memory.md).
Please see the following how-to guides for more details:
* [How to add thread-level persistence (functional API)](../how-tos/persistence-functional.ipynb): Shows how to add thread-level persistence to a functional API workflow and implements a simple chatbot.
### Long-term memory
[long-term memory](memory.md#long-term-memory) allows storing information across different **thread ids**. This could be useful for learning information
about a given user in one conversation and using it in another.
Please see the following how-to guides for more details:
* [How to add cross-thread persistence (functional API)](../how-tos/cross-thread-persistence-functional.ipynb): Shows how to add cross-thread persistence to a functional API workflow and implements a simple chatbot.
### Workflows
* [Workflows and agent](../tutorials/workflows/index.md) guide for more examples of how to build workflows using the Functional API.
### Agents
* [How to create a React agent from scratch (Functional API)](../how-tos/react-agent-from-scratch-functional.ipynb): Shows how to create a simple React agent from scratch using the functional API.
* [How to build a multi-agent network](../how-tos/multi-agent-network-functional.ipynb): Shows how to build a multi-agent network using the functional API.
* [How to add multi-turn conversation in a multi-agent application (functional API)](../how-tos/multi-agent-multi-turn-convo-functional.ipynb): allow an end-user to engage in a multi-turn conversation with one or more agents.
+13 -45
View File
@@ -1,58 +1,26 @@
# Why LangGraph?
LLMs are extremely powerful, particularly when connected to other systems such as a retriever or APIs. This is why many LLM applications use a control flow of steps before and / or after LLM calls. As an example [RAG](https://github.com/langchain-ai/rag-from-scratch) performs retrieval of relevant documents to a question, and passes those documents to an LLM in order to ground the response. Often a control flow of steps before and / or after an LLM is called a "chain." Chains are a popular paradigm for programming with LLMs and offer a high degree of reliability; the same set of steps runs with each chain invocation.
## LLM applications
However, we often want LLM systems that can pick their own control flow! This is one definition of an [agent](https://blog.langchain.dev/what-is-an-agent/): an agent is a system that uses an LLM to decide the control flow of an application. Unlike a chain, an agent gives an LLM some degree of control over the sequence of steps in the application. Examples of using an LLM to decide the control of an application:
LLMs make it possible to embed intelligence into a new class of applications. There are many patterns for building applications that use LLMs. [Workflows](https://www.anthropic.com/research/building-effective-agents) have scaffolding of predefined code paths around LLM calls. LLMs can direct the control flow through these predefined code paths, which some consider to be an "[agentic system](https://www.anthropic.com/research/building-effective-agents)". In other cases, it's possible to remove this scaffolding, creating autonomous agents that can [plan](https://huyenchip.com/2025/01/07/agents.html), take actions via [tool calls](https://python.langchain.com/docs/concepts/tool_calling/), and directly respond [to the feedback from their own actions](https://research.google/blog/react-synergizing-reasoning-and-acting-in-language-models/) with further actions.
- Using an LLM to route between two potential paths
- Using an LLM to decide which of many tools to call
- Using an LLM to decide whether the generated answer is sufficient or more work is need
![Agent Workflow](img/agent_workflow.png)
There are many different types of [agent architectures](https://blog.langchain.dev/what-is-a-cognitive-architecture/) to consider, which give an LLM varying levels of control. On one extreme, a router allows an LLM to select a single step from a specified set of options and, on the other extreme, a fully autonomous long-running agent may have complete freedom to select any sequence of steps that it wants for a given problem.
## What LangGraph provides
![Agent Types](img/agent_types.png)
LangGraph provides low-level supporting infrastructure that sits underneath *any* workflow or agent. It does not abstract prompts or architecture, and provides three central benefits:
Several concepts are utilized in many agent architectures:
### Persistence
- [Tool calling](agentic_concepts.md#tool-calling): this is often how LLMs make decisions
- Action taking: often times, the LLMs' outputs are used as the input to an action
- [Memory](agentic_concepts.md#memory): reliable systems need to have knowledge of things that occurred
- [Planning](agentic_concepts.md#planning): planning steps (either explicit or implicit) are useful for ensuring that the LLM, when making decisions, makes them in the highest fidelity way.
LangGraph has a [persistence layer](https://langchain-ai.github.io/langgraph/concepts/persistence/), which offers a number of benefits:
## Challenges
- [Memory](https://langchain-ai.github.io/langgraph/concepts/memory/): LangGraph persists arbitrary aspects of your application's state, supporting memory of conversations and other updates within and across user interactions;
- [Human-in-the-loop](https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/): Because state is checkpointed, execution can be interrupted and resumed, allowing for decisions, validation, and corrections via human input.
In practice, there is often a trade-off between control and reliability. As we give LLMs more control, the application often become less reliable. This can be due to factors such as LLM non-determinism and / or errors in selecting tools (or steps) that the agent uses (takes).
### Streaming
![Agent Challenge](img/challenge.png)
LangGraph also provides support for [streaming](../how-tos/index.md#streaming) workflow / agent state to the user (or developer) over the course of execution. LangGraph supports streaming of both events ([such as feedback from a tool call](../how-tos/stream-updates.ipynb)) and [tokens from LLM calls](../how-tos/streaming-tokens.ipynb) embedded in an application.
## Core Principles
### Debugging and Deployment
The motivation of LangGraph is to help bend the curve, preserving higher reliability as we give the agent more control over the application. We'll outline a few specific pillars of LangGraph that make it well suited for building reliable agents.
![Langgraph](img/langgraph.png)
**Controllability**
LangGraph gives the developer a high degree of [control](../how-tos/index.md#controllability) by expressing the flow of the application as a set of nodes and edges. All nodes can access and modify a common state (memory). The control flow of the application can set using edges that connect nodes, either deterministically or via conditional logic.
**Persistence**
LangGraph gives the developer many options for [persisting](../how-tos/index.md#persistence) graph state using short-term or long-term (e.g., via a database) memory.
**Human-in-the-Loop**
The persistence layer enables several different [human-in-the-loop](../how-tos/index.md#human-in-the-loop) interaction patterns with agents; for example, it's possible to pause an agent, review its state, edit it state, and approve a follow-up step.
**Streaming**
LangGraph comes with first class support for [streaming](../how-tos/index.md#streaming), which can expose state to the user (or developer) over the course of agent execution. LangGraph supports streaming of both events ([like a tool call being taken](../how-tos/stream-updates.ipynb)) as well as of [tokens that an LLM may emit](../how-tos/streaming-tokens.ipynb).
## Debugging
Once you've built a graph, you often want to test and debug it. [LangGraph Studio](https://github.com/langchain-ai/langgraph-studio?tab=readme-ov-file) is a specialized IDE for visualization and debugging of LangGraph applications.
![Langgraph Studio](img/lg_studio.png)
## Deployment
Once you have confidence in your LangGraph application, many developers want an easy path to deployment. [LangGraph Platform](../concepts/index.md#langgraph-platform) offers a range of options for deploying LangGraph graphs.
LangGraph provides an easy onramp for testing, debugging, and deploying applications via [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/). This includes [Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/), an IDE that enables visualization, interaction, and debugging of workflows or agents. This also includes numerous [options](https://langchain-ai.github.io/langgraph/tutorials/deployment/) for deployment.
Binary file not shown.

After

Width:  |  Height:  |  Size: 646 KiB

+1
View File
@@ -28,6 +28,7 @@ The conceptual guide does not cover step-by-step instructions or specific implem
- [Persistence](persistence.md): LangGraph has a built-in persistence layer, implemented through checkpointers. This persistence layer helps to support powerful capabilities like human-in-the-loop, memory, time travel, and fault-tolerance.
- [Memory](memory.md): Memory in AI applications refers to the ability to process, store, and effectively recall information from past interactions. With memory, your agents can learn from feedback and adapt to users' preferences.
- [Streaming](streaming.md): Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
- [Functional API (beta)](functional_api.md): An alternative to [Graph API (StateGraph)](low_level.md#stategraph) for development in LangGraph.
- [FAQ](faq.md): Frequently asked questions about LangGraph.
## LangGraph Platform
+10 -2
View File
@@ -55,12 +55,20 @@ Infrastructure for [deployments](#deployment) and [revisions](#revision) are pro
- When a subsequent revision is created for a deployment, there is no database creation step. The deployment time for a subsequent revision is significantly faster compared to the deployment time of the initial revision.
- The deployment process for each revision contains a build step, which can take up to a few minutes.
!!! info "Database creation for `Development` type deployments takes longer than database creation for `Production` type deployments."
## Automatic Deletion
Deployments are automatically deleted after 28 consecutive days of non-use (it is in an unused state). A deployment is in an unused state if there are no traces emitted to LangSmith from the deployment after 28 consecutive days. On any given day, if a deployment emits a trace to LangSmith, the counter for consecutive days of non-use is reset.
- An email notification is sent after 7 consecutive days of non-use.
- A deployment is deleted after 28 consecutive days of non-use.
!!! danger "Data Cannot Be Recovered"
After a deployment is deleted, the data (i.e. [persistence](#persistence)) from the deployment cannot be recovered.
## Architecture
!!! warning "Subject to Change"
The Cloud SaaS deployment architecture may change in the future.
The Cloud SaaS deployment architecture may change in the future.
A high-level diagram of a Cloud SaaS deployment.
+19
View File
@@ -359,6 +359,25 @@ Use `Command` when you need to **both** update the graph state **and** route to
Use [conditional edges](#conditional-edges) to route between nodes conditionally without updating the state.
### Navigating to a node in a parent graph
If you are using [subgraphs](#subgraphs), you might want to navigate from a node a subgraph to a different subgraph (i.e. a different node in the parent graph). To do so, you can specify `graph=Command.PARENT` in `Command`:
```python
def my_node(state: State) -> Command[Literal["my_other_node"]]:
return Command(
update={"foo": "bar"},
goto="other_subgraph", # where `other_subgraph` is a node in the parent graph
graph=Command.PARENT
)
```
!!! note
Setting `graph` to `Command.PARENT` will navigate to the closest parent graph.
This is particularly useful when implementing [multi-agent handoffs](./multi_agent.md#handoffs).
### Using inside tools
A common use case is updating graph state from inside a tool. For example, in a customer support application you might want to look up customer information based on their account number or ID in the beginning of the conversation. To update the graph state from the tool, you can return `Command(update={"my_custom_key": "foo", "messages": [...]})` from the tool:
+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",
")"
]
},
File diff suppressed because one or more lines are too long
@@ -23,12 +23,12 @@
" </a> \n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#chat-models/\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/chat_models/\">\n",
" Chat Models\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#tools\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/tools/\">\n",
" Tools\n",
" </a>\n",
" </li> \n",
@@ -368,7 +368,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.12.3"
}
},
"nbformat": 4,
@@ -5,7 +5,7 @@
"id": "992c4695-ec4f-428d-bd05-fb3b5fbd70f4",
"metadata": {},
"source": [
"# How to add memory to the prebuilt ReAct agent\n",
"# How to add thread-level memory to a ReAct Agent\n",
"\n",
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Prerequisites</p>\n",
@@ -28,12 +28,12 @@
" </a> \n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#chat-models/\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/chat_models/\">\n",
" Chat Models\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#tools\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/tools/\">\n",
" Tools\n",
" </a>\n",
" </li>\n",
@@ -285,7 +285,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.12.3"
}
},
"nbformat": 4,
@@ -0,0 +1,287 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "992c4695-ec4f-428d-bd05-fb3b5fbd70f4",
"metadata": {},
"source": [
"# How to return structured output from the prebuilt ReAct agent\n",
"\n",
"!!! info \"Prerequisites\"\n",
" This guide assumes familiarity with the following:\n",
" \n",
" - [Agent Architectures](../../concepts/agentic_concepts/)\n",
" - [Chat Models](https://python.langchain.com/docs/concepts/chat_models/)\n",
" - [Tools](https://python.langchain.com/docs/concepts/tools/)\n",
" - [Structured Output](https://python.langchain.com/docs/concepts/structured_outputs/)\n",
"\n",
"To return structured output from the prebuilt ReAct agent you can provide a `response_format` parameter with the desired output schema to [create_react_agent][langgraph.prebuilt.chat_agent_executor.create_react_agent]:\n",
"\n",
"```python\n",
"class ResponseFormat(BaseModel):\n",
" \"\"\"Respond to the user in this format.\"\"\"\n",
" my_special_output: str\n",
"\n",
"\n",
"graph = create_react_agent(\n",
" model,\n",
" tools=tools,\n",
" # specify the schema for the structured output using `response_format` parameter\n",
" response_format=ResponseFormat\n",
")\n",
"```\n",
"\n",
"Prebuilt ReAct makes an additional LLM call at the end of the ReAct loop to produce a structured output response. Please see [this guide](../react-agent-structured-output) to learn about other strategies for returning structured outputs from a tool-calling agent."
]
},
{
"cell_type": "markdown",
"id": "7be3889f-3c17-4fa1-bd2b-84114a2c7247",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"First, let's install the required packages and set our API keys"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "a213e11a-5c62-4ddb-a707-490d91add383",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "23a1885c-04ab-4750-aefa-105891fddf3e",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "87a00ce9",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
" </p>\n",
"</div>"
]
},
{
"cell_type": "markdown",
"id": "03c0f089-070c-4cd4-87e0-6c51f2477b82",
"metadata": {},
"source": [
"## Code"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "7a154152-973e-4b5d-aa13-48c617744a4c",
"metadata": {},
"outputs": [],
"source": [
"# First we initialize the model we want to use.\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
"\n",
"# For this tutorial we will use custom tool that returns pre-defined values for weather in two cities (NYC & SF)\n",
"\n",
"from typing import Literal\n",
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def get_weather(city: Literal[\"nyc\", \"sf\"]):\n",
" \"\"\"Use this to get weather information.\"\"\"\n",
" if city == \"nyc\":\n",
" return \"It might be cloudy in nyc\"\n",
" elif city == \"sf\":\n",
" return \"It's always sunny in sf\"\n",
" else:\n",
" raise AssertionError(\"Unknown city\")\n",
"\n",
"\n",
"tools = [get_weather]\n",
"\n",
"# Define the structured output schema\n",
"\n",
"from pydantic import BaseModel, Field\n",
"\n",
"\n",
"class WeatherResponse(BaseModel):\n",
" \"\"\"Respond to the user in this format.\"\"\"\n",
"\n",
" conditions: str = Field(description=\"Weather conditions\")\n",
"\n",
"\n",
"# Define the graph\n",
"\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"graph = create_react_agent(\n",
" model,\n",
" tools=tools,\n",
" # specify the schema for the structured output using `response_format` parameter\n",
" response_format=WeatherResponse,\n",
")"
]
},
{
"cell_type": "markdown",
"id": "00407425-506d-4ffd-9c86-987921d8c844",
"metadata": {},
"source": [
"## Usage\n",
"\n",
"Let's now test our agent:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "9ffff6c3-a4f5-47c9-b51d-97caaee85cd6",
"metadata": {},
"outputs": [],
"source": [
"inputs = {\"messages\": [(\"user\", \"What's the weather in NYC?\")]}\n",
"response = graph.invoke(inputs)"
]
},
{
"cell_type": "markdown",
"id": "50e273a0-fbdb-4eee-89ca-580fbfb52daf",
"metadata": {},
"source": [
"You can see that the agent output contains a `structured_response` key with the structured output conforming to the specified `WeatherResponse` schema, in addition to the message history under `messages` key."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "300748d4-0ed2-470d-8dbc-7c14231e73b8",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"WeatherResponse(conditions='cloudy')"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"response[\"structured_response\"]"
]
},
{
"cell_type": "markdown",
"id": "bd9e3487-2cec-44cf-9472-0a51eebeddff",
"metadata": {},
"source": [
"### Customizing prompt"
]
},
{
"cell_type": "markdown",
"id": "a608548d-77fc-4d7a-845c-32ae9ec0489a",
"metadata": {},
"source": [
"You might need to further customize the second LLM call for the structured output generation and provide a system prompt. To do so, you can pass a tuple (prompt, schema):"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "d1386f99-ffd1-4b36-86ec-cabb3357d929",
"metadata": {},
"outputs": [],
"source": [
"graph = create_react_agent(\n",
" model,\n",
" tools=tools,\n",
" # specify both the system prompt and the schema for the structured output\n",
" response_format=(\"Always return capitalized weather conditions\", WeatherResponse),\n",
")\n",
"\n",
"inputs = {\"messages\": [(\"user\", \"What's the weather in NYC?\")]}\n",
"response = graph.invoke(inputs)"
]
},
{
"cell_type": "markdown",
"id": "91f34991-b406-4fd2-a776-4dd03e3dc3dd",
"metadata": {},
"source": [
"You can verify that the structured response now contains a capitalized value:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "ba43a67f-127c-45e7-982c-a8210d97a3ed",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"WeatherResponse(conditions='Cloudy')"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"response[\"structured_response\"]"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -14,7 +14,7 @@
" This guide assumes familiarity with the following:\n",
" <ul>\n",
" <li> \n",
" <a href=\"https://python.langchain.com/v0.1/docs/modules/model_io/concepts/#systemmessage\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/messages/#systemmessage\">\n",
" SystemMessage\n",
" </a>\n",
" </li>\n",
@@ -24,12 +24,12 @@
" </a> \n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#chat-models/\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/chat_models/\">\n",
" Chat Models\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#tools\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/tools/\">\n",
" Tools\n",
" </a>\n",
" </li>\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)"
]
},
{
@@ -223,7 +223,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.12.3"
}
},
"nbformat": 4,
+4 -4
View File
@@ -5,7 +5,7 @@
"id": "992c4695-ec4f-428d-bd05-fb3b5fbd70f4",
"metadata": {},
"source": [
"# How to use the prebuilt ReAct agent"
"# How to use the pre-built ReAct agent"
]
},
{
@@ -24,12 +24,12 @@
" </a> \n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#chat-models/\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/chat_models/\">\n",
" Chat Models\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#tools\">\n",
" <a href=\"https://python.langchain.com/docs/concepts/tools/\">\n",
" Tools\n",
" </a>\n",
" </li>\n",
@@ -292,7 +292,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.12.3"
}
},
"nbformat": 4,
@@ -0,0 +1,362 @@
{
"cells": [
{
"attachments": {},
"cell_type": "markdown",
"id": "d2eecb96-cf0e-47ed-8116-88a7eaa4236d",
"metadata": {},
"source": [
"# How to add cross-thread persistence (functional API)\n",
"\n",
"!!! info \"Prerequisites\"\n",
"\n",
" This guide assumes familiarity with the following:\n",
" \n",
" - [Functional API](../../concepts/functional_api/)\n",
" - [Persistence](../../concepts/persistence/)\n",
" - [Memory](../../concepts/memory/)\n",
" - [Chat Models](https://python.langchain.com/docs/concepts/chat_models/)\n",
"\n",
"LangGraph allows you to persist data across **different [threads](../../concepts/persistence/#threads)**. For instance, you can store information about users (their names or preferences) in a shared (cross-thread) memory and reuse them in the new threads (e.g., new conversations).\n",
"\n",
"When using the [functional API](../../concepts/functional_api/), you can set it up to store and retrieve memories by using the [Store](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore) interface:\n",
"\n",
"1. Create an instance of a `Store`\n",
"\n",
" ```python\n",
" from langgraph.store.memory import InMemoryStore, BaseStore\n",
" \n",
" store = InMemoryStore()\n",
" ```\n",
"\n",
"2. Pass the `store` instance to the `entrypoint()` decorator and expose `store` parameter in the function signature:\n",
"\n",
" ```python\n",
" from langgraph.func import entrypoint\n",
"\n",
" @entrypoint(store=store)\n",
" def workflow(inputs: dict, store: BaseStore):\n",
" my_task(inputs).result()\n",
" ...\n",
" ```\n",
" \n",
"In this guide, we will show how to construct and use a workflow that has a shared memory implemented using the [Store](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore) interface.\n",
"\n",
"!!! note Note\n",
"\n",
" Support for the [`Store`](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore) API that is used in this guide was added in LangGraph `v0.2.32`.\n",
"\n",
" Support for __index__ and __query__ arguments of the [`Store`](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore) API that is used in this guide was added in LangGraph `v0.2.54`.\n",
"\n",
"!!! tip \"Note\"\n",
"\n",
" If you need to add cross-thread persistence to a `StateGraph`, check out this [how-to guide](../cross-thread-persistence).\n",
"\n",
"## Setup\n",
"\n",
"First, let's install the required packages and set our API keys"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "3457aadf",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langchain_anthropic langchain_openai langgraph"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "aa2c64a7",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"ANTHROPIC_API_KEY\")\n",
"_set_env(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "51b6817d",
"metadata": {},
"source": [
"!!! tip \"Set up [LangSmith](https://smith.langchain.com) for LangGraph development\"\n",
"\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started [here](https://docs.smith.langchain.com)"
]
},
{
"cell_type": "markdown",
"id": "6b5b3d42-3d2c-455e-ac10-e2ae74dc1cf1",
"metadata": {},
"source": [
"## Example: simple chatbot with long-term memory"
]
},
{
"cell_type": "markdown",
"id": "c4c550b5-1954-496b-8b9d-800361af17dc",
"metadata": {},
"source": [
"### Define store\n",
"\n",
"In this example we will create a workflow that will be able to retrieve information about a user's preferences. We will do so by defining an `InMemoryStore` - an object that can store data in memory and query that data.\n",
"\n",
"When storing objects using the `Store` interface you define two things:\n",
"\n",
"* the namespace for the object, a tuple (similar to directories)\n",
"* the object key (similar to filenames)\n",
"\n",
"In our example, we'll be using `(\"memories\", <user_id>)` as namespace and random UUID as key for each new memory.\n",
"\n",
"Importantly, to determine the user, we will be passing `user_id` via the config keyword argument of the node function.\n",
"\n",
"Let's first define our store!"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "a7f303d6-612e-4e34-bf36-29d4ed25d802",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.store.memory import InMemoryStore\n",
"from langchain_openai import OpenAIEmbeddings\n",
"\n",
"in_memory_store = InMemoryStore(\n",
" index={\n",
" \"embed\": OpenAIEmbeddings(model=\"text-embedding-3-small\"),\n",
" \"dims\": 1536,\n",
" }\n",
")"
]
},
{
"cell_type": "markdown",
"id": "3389c9f4-226d-40c7-8bfc-ee8aac24f79d",
"metadata": {},
"source": [
"### Create workflow"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "2a30a362-528c-45ee-9df6-630d2d843588",
"metadata": {},
"outputs": [],
"source": [
"import uuid\n",
"\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_core.runnables import RunnableConfig\n",
"from langchain_core.messages import BaseMessage\n",
"from langgraph.func import entrypoint, task\n",
"from langgraph.graph import add_messages\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.store.base import BaseStore\n",
"\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")\n",
"\n",
"\n",
"@task\n",
"def call_model(messages: list[BaseMessage], memory_store: BaseStore, user_id: str):\n",
" namespace = (\"memories\", user_id)\n",
" last_message = messages[-1]\n",
" memories = memory_store.search(namespace, query=str(last_message.content))\n",
" info = \"\\n\".join([d.value[\"data\"] for d in memories])\n",
" system_msg = f\"You are a helpful assistant talking to the user. User info: {info}\"\n",
"\n",
" # Store new memories if the user asks the model to remember\n",
" if \"remember\" in last_message.content.lower():\n",
" memory = \"User name is Bob\"\n",
" memory_store.put(namespace, str(uuid.uuid4()), {\"data\": memory})\n",
"\n",
" response = model.invoke([{\"role\": \"system\", \"content\": system_msg}] + messages)\n",
" return response\n",
"\n",
"\n",
"# NOTE: we're passing the store object here when creating a workflow via entrypoint()\n",
"@entrypoint(checkpointer=MemorySaver(), store=in_memory_store)\n",
"def workflow(\n",
" inputs: list[BaseMessage],\n",
" *,\n",
" previous: list[BaseMessage],\n",
" config: RunnableConfig,\n",
" store: BaseStore,\n",
"):\n",
" user_id = config[\"configurable\"][\"user_id\"]\n",
" previous = previous or []\n",
" inputs = add_messages(previous, inputs)\n",
" response = call_model(inputs, store, user_id).result()\n",
" return entrypoint.final(value=response, save=add_messages(inputs, response))"
]
},
{
"cell_type": "markdown",
"id": "f22a4a18-67e4-4f0b-b655-a29bbe202e1c",
"metadata": {},
"source": [
"!!! note Note\n",
"\n",
" If you're using LangGraph Cloud or LangGraph Studio, you __don't need__ to pass store to the entrypoint decorator, since it's done automatically."
]
},
{
"cell_type": "markdown",
"id": "552d4e33-556d-4fa5-8094-2a076bc21529",
"metadata": {},
"source": [
"### Run the workflow!"
]
},
{
"cell_type": "markdown",
"id": "1842c626-6cd9-4f58-b549-58978e478098",
"metadata": {},
"source": [
"Now let's specify a user ID in the config and tell the model our name:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "c871a073-a466-46ad-aafe-2b870831057e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Hello Bob! Nice to meet you. I'll remember that your name is Bob. How can I help you today?\n"
]
}
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"1\", \"user_id\": \"1\"}}\n",
"input_message = {\"role\": \"user\", \"content\": \"Hi! Remember: my name is Bob\"}\n",
"for chunk in workflow.stream([input_message], config, stream_mode=\"values\"):\n",
" chunk.pretty_print()"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "d862be40-1f8a-4057-81c4-b7bf073dc4c1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Your name is Bob.\n"
]
}
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"2\", \"user_id\": \"1\"}}\n",
"input_message = {\"role\": \"user\", \"content\": \"what is my name?\"}\n",
"for chunk in workflow.stream([input_message], config, stream_mode=\"values\"):\n",
" chunk.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "80fd01ec-f135-4811-8743-daff8daea422",
"metadata": {},
"source": [
"We can now inspect our in-memory store and verify that we have in fact saved the memories for the user:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "76cde493-89cf-4709-a339-207d2b7e9ea7",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'data': 'User name is Bob'}\n"
]
}
],
"source": [
"for memory in in_memory_store.search((\"memories\", \"1\")):\n",
" print(memory.value)"
]
},
{
"cell_type": "markdown",
"id": "23f5d7eb-af23-4131-b8fd-2a69e74e6e55",
"metadata": {},
"source": [
"Let's now run the workflow for another user to verify that the memories about the first user are self contained:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "d362350b-d730-48bd-9652-983812fd7811",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"I don't have any information about your name. I can only see our current conversation without any prior context or personal details about you. If you'd like me to know your name, feel free to tell me!\n"
]
}
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"3\", \"user_id\": \"2\"}}\n",
"input_message = {\"role\": \"user\", \"content\": \"what is my name?\"}\n",
"for chunk in workflow.stream([input_message], config, stream_mode=\"values\"):\n",
" chunk.pretty_print()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -64,18 +64,10 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": null,
"id": "aa2c64a7",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"ANTHROPIC_API_KEY: ········\n"
]
}
],
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
@@ -86,7 +78,8 @@
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"ANTHROPIC_API_KEY\")"
"_set_env(\"ANTHROPIC_API_KEY\")\n",
"_set_env(\"OPENAI_API_KEY\")"
]
},
{
@@ -183,7 +176,7 @@
" store.put(namespace, str(uuid.uuid4()), {\"data\": memory})\n",
"\n",
" response = model.invoke(\n",
" [{\"type\": \"system\", \"content\": system_msg}] + state[\"messages\"]\n",
" [{\"role\": \"system\", \"content\": system_msg}] + state[\"messages\"]\n",
" )\n",
" return {\"messages\": response}\n",
"\n",
@@ -247,7 +240,7 @@
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"1\", \"user_id\": \"1\"}}\n",
"input_message = {\"type\": \"user\", \"content\": \"Hi! Remember: my name is Bob\"}\n",
"input_message = {\"role\": \"user\", \"content\": \"Hi! Remember: my name is Bob\"}\n",
"for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
@@ -273,7 +266,7 @@
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"2\", \"user_id\": \"1\"}}\n",
"input_message = {\"type\": \"user\", \"content\": \"what is my name?\"}\n",
"input_message = {\"role\": \"user\", \"content\": \"what is my name?\"}\n",
"for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
@@ -334,7 +327,7 @@
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"3\", \"user_id\": \"2\"}}\n",
"input_message = {\"type\": \"user\", \"content\": \"what is my name?\"}\n",
"input_message = {\"role\": \"user\", \"content\": \"what is my name?\"}\n",
"for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
@@ -356,7 +349,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
"version": "3.12.3"
}
},
"nbformat": 4,
+14 -2
View File
@@ -25,8 +25,20 @@ If you would like to deploy LangGraph Cloud on Kubernetes, you can use this [Hel
You will eventually need to pass in the following environment variables to the LangGraph Deploy server:
- `REDIS_URI`: Connection details to a Redis instance. Redis will be used as a pub-sub broker to enable streaming real time output from background runs.
- `DATABASE_URI`: Postgres connection details. Postgres will be used to store assistants, threads, runs, persist thread state and long term memory, and to manage the state of the background task queue with 'exactly once' semantics.
- `REDIS_URI`: Connection details to a Redis instance. Redis will be used as a pub-sub broker to enable streaming real time output from background runs. The value of `REDIS_URI` must be a valid [Redis connection URI](https://redis-py.readthedocs.io/en/stable/connections.html#redis.Redis.from_url).
!!! Note "Shared Redis Instance"
Multiple self-hosted deployments can share the same Redis instance. For example, for `Deployment A`, `REDIS_URI` can be set to `redis://<hostname_1>:<port>/1` and for `Deployment B`, `REDIS_URI` can be set to `redis://<hostname_1>:<port>/2`.
`1` and `2` are different database numbers within the same instance, but `<hostname_1>` is shared. **The same database number cannot be used for separate deployments**.
- `DATABASE_URI`: Postgres connection details. Postgres will be used to store assistants, threads, runs, persist thread state and long term memory, and to manage the state of the background task queue with 'exactly once' semantics. The value of `DATABASE_URI` must be a valid [Postgres connection URI](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS).
!!! Note "Shared Postgres Instance"
Multiple self-hosted deployments can share the same Postgres instance. For example, for `Deployment A`, `DATABASE_URI` can be set to `postgres://<user>:<password>@/<database_name_1>?host=<hostname_1>` and for `Deployment B`, `DATABASE_URI` can be set to `postgres://<user>:<password>@/<database_name_2>?host=<hostname_1>`.
`<database_name_1>` and `database_name_2` are different databases within the same instance, but `<hostname_1>` is shared. **The same database cannot be used for separate deployments**.
- `LANGSMITH_API_KEY`: (If using [Self-Hosted Lite](../concepts/deployment_options.md#self-hosted-lite)) LangSmith API key. This will be used to authenticate ONCE at server start up.
- `LANGGRAPH_CLOUD_LICENSE_KEY`: (If using [Self-Hosted Enterprise](../concepts/deployment_options.md#self-hosted-enterprise)) LangGraph Platform license key. This will be used to authenticate ONCE at server start up.
- `LANGCHAIN_ENDPOINT`: To send traces to a [self-hosted LangSmith](https://docs.smith.langchain.com/self_hosting) instance, set `LANGCHAIN_ENDPOINT` to the hostname of the self-hosted LangSmith instance.
@@ -591,7 +591,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.10.4"
}
},
"nbformat": 4,
+46 -9
View File
@@ -22,15 +22,21 @@ These how-to guides show how to achieve that controllability.
### Persistence
[LangGraph Persistence](../concepts/persistence.md) makes it easy to persist state across graph runs (thread-level persistence) and across threads (cross-thread persistence). These how-to guides show how to add persistence to your graph.
[LangGraph Persistence](../concepts/persistence.md) makes it easy to persist state across graph runs (per-thread persistence) and across threads (cross-thread persistence). These how-to guides show how to add persistence to your graph.
- [How to add thread-level persistence to your graph](persistence.ipynb)
- [How to add thread-level persistence to subgraphs](subgraph-persistence.ipynb)
- [How to add thread-level persistence to a subgraph](subgraph-persistence.ipynb)
- [How to add cross-thread persistence to your graph](cross-thread-persistence.ipynb)
- [How to use Postgres checkpointer for persistence](persistence_postgres.ipynb)
- [How to use MongoDB checkpointer for persistence](persistence_mongodb.ipynb)
- [How to create a custom checkpointer using Redis](persistence_redis.ipynb)
See the below guides for how-to add persistence to your workflow using the (beta)
[Functional API](../concepts/functional_api.md):
- [How to add thread-level persistence (functional API)](persistence-functional.ipynb)
- [How to add cross-thread persistence (functional API)](cross-thread-persistence-functional.ipynb)
### Memory
LangGraph makes it easy to manage conversation [memory](../concepts/memory.md) in your graph. These how-to guides show how to implement different strategies for that.
@@ -59,6 +65,12 @@ Other methods:
- [How to edit graph state](human_in_the_loop/edit-graph-state.ipynb): Edit graph state using `graph.update_state` method. Use this if implementing a **human-in-the-loop** workflow via **static breakpoints**.
- [How to add dynamic breakpoints with `NodeInterrupt`](human_in_the_loop/dynamic_breakpoints.ipynb): **Not recommended**: Use the [`interrupt` function](../concepts/human_in_the_loop.md) instead.
See the below guides for how-to implement human-in-the-loop workflows with the (beta)
[Functional API](../concepts/functional_api.md):
- [How to wait for user input (Functional API)](wait-user-input-functional.ipynb)
- [How to review tool calls (Functional API)](review-tool-calls-functional.ipynb)
### Time Travel
[Time travel](../concepts/time-travel.md) allows you to replay past actions in your LangGraph application to explore alternative paths and debug issues. These how-to guides show how to use time travel in your graph.
@@ -83,7 +95,10 @@ Other methods:
### Tool calling
[Tool calling](https://python.langchain.com/docs/concepts/tool_calling/) is a type of chat model API that accepts tool schemas, along with messages, as input and returns invocations of those tools as part of the output message.
[Tool calling](https://python.langchain.com/docs/concepts/tool_calling/) is a type of
[chat model](https://python.langchain.com/docs/concepts/chat_models/) API that accepts
tool schemas, along with messages, as input and returns invocations of those tools as
part of the output message.
These how-to guides show common patterns for tool calling with LangGraph:
@@ -98,7 +113,7 @@ These how-to guides show common patterns for tool calling with LangGraph:
[Subgraphs](../concepts/low_level.md#subgraphs) allow you to reuse an existing graph from another graph. These how-to guides show how to use subgraphs:
- [How to add and use subgraphs](subgraph.ipynb)
- [How to use subgraphs](subgraph.ipynb)
- [How to view and update state in subgraphs](subgraphs-manage-state.ipynb)
- [How to transform inputs and outputs of a subgraph](subgraph-transform-state.ipynb)
@@ -112,9 +127,15 @@ These how-to guides show common patterns for tool calling with LangGraph:
See the [multi-agent tutorials](../tutorials/index.md#multi-agent-systems) for implementations of other multi-agent architectures.
See the below guides for how-to implement multi-agent workflows with the (beta)
[Functional API](../concepts/functional_api.md):
- [How to build a multi-agent network (functional API)](multi-agent-network-functional.ipynb)
- [How to add multi-turn conversation in a multi-agent application (functional API)](multi-agent-multi-turn-convo-functional.ipynb)
### State Management
- [How to use Pydantic model as state](state-model.ipynb)
- [How to use Pydantic model as graph state](state-model.ipynb)
- [How to define input/output schema for your graph](input_output_schema.ipynb)
- [How to pass private state between nodes inside the graph](pass_private_state.ipynb)
@@ -124,7 +145,7 @@ See the [multi-agent tutorials](../tutorials/index.md#multi-agent-systems) for i
- [How to visualize your graph](visualization.ipynb)
- [How to add runtime configuration to your graph](configuration.ipynb)
- [How to add node retries](node-retries.ipynb)
- [How to force function calling agent to structure output](react-agent-structured-output.ipynb)
- [How to force tool-calling agent to structure output](react-agent-structured-output.ipynb)
- [How to pass custom LangSmith run ID for graph runs](run-id-langsmith.ipynb)
- [How to return state before hitting recursion limit](return-when-recursion-limit-hits.ipynb)
- [How to integrate LangGraph with AutoGen, CrewAI, and other frameworks](autogen-integration.ipynb)
@@ -137,13 +158,23 @@ One of the big benefits of LangGraph is that you can easily create your own agen
These guides show how to use the prebuilt ReAct agent:
- [How to create a ReAct agent](create-react-agent.ipynb)
- [How to add memory to a ReAct agent](create-react-agent-memory.ipynb)
- [How to use the pre-built ReAct agent](create-react-agent.ipynb)
- [How to add thread-level memory to a ReAct Agent](create-react-agent-memory.ipynb)
- [How to add a custom system prompt to a ReAct agent](create-react-agent-system-prompt.ipynb)
- [How to add human-in-the-loop processes to a ReAct agent](create-react-agent-hitl.ipynb)
- [How to create prebuilt ReAct agent from scratch](react-agent-from-scratch.ipynb)
- [How to return structured output from a ReAct agent](create-react-agent-structured-output.ipynb)
- [How to add semantic search for long-term memory to a ReAct agent](memory/semantic-search.ipynb#using-in-create-react-agent)
Interested in further customizing the ReAct agent? This guide provides an
overview of its underlying implementation to help you customize for your own needs:
- [How to create prebuilt ReAct agent from scratch](react-agent-from-scratch.ipynb)
See the below guide for how-to build ReAct agents with the (beta)
[Functional API](../concepts/functional_api.md):
- [How to create a ReAct agent from scratch (Functional API)](react-agent-from-scratch-functional.ipynb)
## LangGraph Platform
This section includes how-to guides for LangGraph Platform.
@@ -187,11 +218,17 @@ LangGraph applications can be deployed using LangGraph Cloud, which provides a r
[Assistants](../concepts/assistants.md) is a configured instance of a template.
See [SDK Reference](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.client.AssistantsClient)
for supported endpoints and other details.
- [How to configure agents](../cloud/how-tos/configuration_cloud.md)
- [How to version assistants](../cloud/how-tos/assistant_versioning.md)
### Threads
See [SDK Reference](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.client.ThreadsClient)
for supported endpoints and other details.
- [How to copy threads](../cloud/how-tos/copy_threads.md)
- [How to check status of your threads](../cloud/how-tos/check_thread_status.md)
@@ -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,
@@ -0,0 +1,449 @@
{
"cells": [
{
"attachments": {},
"cell_type": "markdown",
"id": "a2b182eb-1e31-43c8-85b1-706508dfa370",
"metadata": {},
"source": [
"# How to add multi-turn conversation in a multi-agent application (functional API)\n",
"\n",
"!!! info \"Prerequisites\"\n",
" This guide assumes familiarity with the following:\n",
"\n",
" - [Multi-agent systems](../../concepts/multi_agent)\n",
" - [Human-in-the-loop](../../concepts/human_in_the_loop)\n",
" - [Functional API](../../concepts/functional_api)\n",
" - [Command](../../concepts/low_level/#command)\n",
" - [LangGraph Glossary](../../concepts/low_level/)\n",
"\n",
"\n",
"In this how-to guide, we’ll build an application that allows an end-user to engage in a *multi-turn conversation* with one or more agents. We'll create a node that uses an [`interrupt`](../../reference/types/#langgraph.types.interrupt) to collect user input and routes back to the **active** agent.\n",
"\n",
"The agents will be implemented as tasks in a workflow that executes agent steps and determines the next action:\n",
"\n",
"1. **Wait for user input** to continue the conversation, or\n",
"2. **Route to another agent** (or back to itself, such as in a loop) via a [**handoff**](../../concepts/multi_agent/#handoffs).\n",
"\n",
"```python\n",
"from langgraph.func import entrypoint, task\n",
"from langgraph.prebuilt import create_react_agent\n",
"from langchain_core.tools import tool\n",
"from langgraph.types import interrupt\n",
"\n",
"\n",
"# Define a tool to signal intent to hand off to a different agent\n",
"# Note: this is not using Command(goto) syntax for navigating to different agents:\n",
"# `workflow()` below handles the handoffs explicitly\n",
"@tool(return_direct=True)\n",
"def transfer_to_hotel_advisor():\n",
" \"\"\"Ask hotel advisor agent for help.\"\"\"\n",
" return \"Successfully transferred to hotel advisor\"\n",
"\n",
"\n",
"# define an agent\n",
"travel_advisor_tools = [transfer_to_hotel_advisor, ...]\n",
"travel_advisor = create_react_agent(model, travel_advisor_tools)\n",
"\n",
"\n",
"# define a task that calls an agent\n",
"@task\n",
"def call_travel_advisor(messages):\n",
" response = travel_advisor.invoke({\"messages\": messages})\n",
" return response[\"messages\"]\n",
"\n",
"\n",
"# define the multi-agent network workflow\n",
"@entrypoint(checkpointer)\n",
"def workflow(messages):\n",
" call_active_agent = call_travel_advisor\n",
" while True:\n",
" agent_messages = call_active_agent(messages).result()\n",
" ai_msg = get_last_ai_msg(agent_messages)\n",
" if not ai_msg.tool_calls:\n",
" user_input = interrupt(value=\"Ready for user input.\")\n",
" messages = messages + [{\"role\": \"user\", \"content\": user_input}]\n",
" continue\n",
"\n",
" messages = messages + agent_messages\n",
" call_active_agent = get_next_agent(messages)\n",
" return entrypoint.final(value=agent_messages[-1], save=messages)\n",
"```"
]
},
{
"cell_type": "markdown",
"id": "faaa4444-cd06-4813-b9ca-c9700fe12cb7",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"First, let's install the required packages"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "05038da0-31df-4066-a1a4-c4ccb5db4d3a",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-anthropic"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "0bcff5d4-130e-426d-9285-40d0f72c7cd3",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"ANTHROPIC_API_KEY: ········\n"
]
}
],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"ANTHROPIC_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "c3ec6e48-85dc-4905-ba50-985e5d4788e6",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
" </p>\n",
"</div>"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "c217c3fe-ca50-45a1-be91-912bc83ed8b3",
"metadata": {},
"source": [
"In this example we will build a team of travel assistant agents that can communicate with each other.\n",
"\n",
"We will create 2 agents:\n",
"\n",
"* `travel_advisor`: can help with travel destination recommendations. Can ask `hotel_advisor` for help.\n",
"* `hotel_advisor`: can help with hotel recommendations. Can ask `travel_advisor` for help.\n",
"\n",
"This is a fully-connected network - every agent can talk to any other agent. "
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "eb51463a-4425-44ad-91d5-f21fd5b4e3b3",
"metadata": {},
"outputs": [],
"source": [
"import random\n",
"from typing_extensions import Literal\n",
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def get_travel_recommendations():\n",
" \"\"\"Get recommendation for travel destinations\"\"\"\n",
" return random.choice([\"aruba\", \"turks and caicos\"])\n",
"\n",
"\n",
"@tool\n",
"def get_hotel_recommendations(location: Literal[\"aruba\", \"turks and caicos\"]):\n",
" \"\"\"Get hotel recommendations for a given destination.\"\"\"\n",
" return {\n",
" \"aruba\": [\n",
" \"The Ritz-Carlton, Aruba (Palm Beach)\"\n",
" \"Bucuti & Tara Beach Resort (Eagle Beach)\"\n",
" ],\n",
" \"turks and caicos\": [\"Grace Bay Club\", \"COMO Parrot Cay\"],\n",
" }[location]\n",
"\n",
"\n",
"@tool(return_direct=True)\n",
"def transfer_to_hotel_advisor():\n",
" \"\"\"Ask hotel advisor agent for help.\"\"\"\n",
" return \"Successfully transferred to hotel advisor\"\n",
"\n",
"\n",
"@tool(return_direct=True)\n",
"def transfer_to_travel_advisor():\n",
" \"\"\"Ask travel advisor agent for help.\"\"\"\n",
" return \"Successfully transferred to travel advisor\""
]
},
{
"cell_type": "markdown",
"id": "7f5b2a7f",
"metadata": {},
"source": [
"!!! note \"Transfer tools\"\n",
"\n",
" You might have noticed that we're using `@tool(return_direct=True)` in the transfer tools. This is done so that individual agents (e.g., `travel_advisor`) can exit the ReAct loop early once these tools are called. This is the desired behavior, as we want to detect when the agent calls this tool and hand control off _immediately_ to a different agent. \n",
" \n",
" **NOTE**: This is meant to work with the prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] -- if you are building a custom agent, make sure to manually add logic for handling early exit for tools that are marked with `return_direct`."
]
},
{
"cell_type": "markdown",
"id": "213d661e-6ba4-42b9-bc7f-6c8c423e3419",
"metadata": {},
"source": [
"Let's now create our agents using the the prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] and our multi-agent workflow. Note that will be calling [`interrupt`][langgraph.types.interrupt] every time after we get the final response from each of the agents."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "aa4bdbff-9461-46cc-aee9-8a22d3c3d9ec",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import AIMessage\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langgraph.prebuilt import create_react_agent\n",
"from langgraph.graph import add_messages\n",
"from langgraph.func import entrypoint, task\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.types import interrupt, Command\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")\n",
"\n",
"# Define travel advisor ReAct agent\n",
"travel_advisor_tools = [\n",
" get_travel_recommendations,\n",
" transfer_to_hotel_advisor,\n",
"]\n",
"travel_advisor = create_react_agent(\n",
" model,\n",
" travel_advisor_tools,\n",
" state_modifier=(\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",
" ),\n",
")\n",
"\n",
"\n",
"@task\n",
"def call_travel_advisor(messages):\n",
" # You can also add additional logic like changing the input to the agent / output from the agent, etc.\n",
" # NOTE: we're invoking the ReAct agent with the full history of messages in the state\n",
" response = travel_advisor.invoke({\"messages\": messages})\n",
" return response[\"messages\"]\n",
"\n",
"\n",
"# Define hotel advisor ReAct agent\n",
"hotel_advisor_tools = [get_hotel_recommendations, transfer_to_travel_advisor]\n",
"hotel_advisor = create_react_agent(\n",
" model,\n",
" hotel_advisor_tools,\n",
" state_modifier=(\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",
" ),\n",
")\n",
"\n",
"\n",
"@task\n",
"def call_hotel_advisor(messages):\n",
" response = hotel_advisor.invoke({\"messages\": messages})\n",
" return response[\"messages\"]\n",
"\n",
"\n",
"checkpointer = MemorySaver()\n",
"\n",
"\n",
"@entrypoint(checkpointer=checkpointer)\n",
"def multi_turn_graph(messages, previous):\n",
" previous = previous or []\n",
" messages = add_messages(previous, messages)\n",
"\n",
" call_active_agent = call_travel_advisor\n",
" while True:\n",
" agent_messages = call_active_agent(messages).result()\n",
" messages = add_messages(messages, agent_messages)\n",
" # Find the last AI message\n",
" # If one of the handoff tools is called, the last message returned\n",
" # by the agent will be a ToolMessage because we set them to have\n",
" # \"return_direct=True\". This means that the last AIMessage will\n",
" # have tool calls.\n",
" # Otherwise, the last returned message will be an AIMessage with\n",
" # no tool calls, which means we are ready for new input.\n",
" ai_msg = next(m for m in reversed(agent_messages) if isinstance(m, AIMessage))\n",
" if not ai_msg.tool_calls:\n",
" user_input = interrupt(value=\"Ready for user input.\")\n",
" messages = add_messages(messages, [{\"role\": \"user\", \"content\": user_input}])\n",
" continue\n",
"\n",
" tool_call = ai_msg.tool_calls[-1]\n",
" if tool_call[\"name\"] == \"transfer_to_hotel_advisor\":\n",
" call_active_agent = call_hotel_advisor\n",
" elif tool_call[\"name\"] == \"transfer_to_travel_advisor\":\n",
" call_active_agent = call_travel_advisor\n",
" else:\n",
" raise ValueError(f\"Expected transfer tool, got '{tool_call['name']}'\")\n",
"\n",
" return entrypoint.final(value=agent_messages[-1], save=messages)"
]
},
{
"cell_type": "markdown",
"id": "af856e1b-41fc-4041-8cbf-3818a60088e0",
"metadata": {},
"source": [
"## Test multi-turn conversation\n",
"\n",
"Let's test a multi turn conversation with this application."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "161e0cf1-d13a-4026-8f89-bdab67d1ad4d",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"--- Conversation Turn 1 ---\n",
"\n",
"User: {'role': 'user', 'content': 'i wanna go somewhere warm in the caribbean'}\n",
"\n",
"call_travel_advisor: Based on the recommendations, Aruba would be an excellent choice for your Caribbean getaway! Aruba is known for its perfect warm weather year-round, with consistent temperatures around 82°F (28°C) and very little rainfall. The island offers:\n",
"\n",
"1. Beautiful white-sand beaches like Eagle Beach and Palm Beach\n",
"2. Crystal clear waters perfect for swimming and snorkeling\n",
"3. Constant cooling trade winds that make the warm weather comfortable\n",
"4. A mix of luxury resorts and boutique hotels\n",
"5. Diverse activities from water sports to desert-like terrain exploration\n",
"6. Great dining and nightlife options\n",
"7. Safe and tourist-friendly environment\n",
"\n",
"Would you like me to connect you with our hotel advisor to help you find the perfect place to stay in Aruba?\n",
"\n",
"--- Conversation Turn 2 ---\n",
"\n",
"User: Command(resume='could you recommend a nice hotel in one of the areas and tell me which area it is.')\n",
"\n",
"call_hotel_advisor: Based on the recommendations, I can highlight two excellent options in different areas:\n",
"\n",
"1. The Ritz-Carlton, Aruba - Located in Palm Beach\n",
"- Part of the high-rise hotel district\n",
"- Luxury beachfront resort with full-service spa\n",
"- Multiple restaurants and a casino\n",
"- Perfect for those who want to be in the heart of the action\n",
"- Close to shopping, dining, and nightlife\n",
"\n",
"2. Bucuti & Tara Beach Resort - Located in Eagle Beach\n",
"- Adults-only boutique resort\n",
"- Located on Eagle Beach, voted one of the best beaches in the world\n",
"- More serene and romantic atmosphere\n",
"- Perfect for couples and those seeking a quieter vacation\n",
"- Known for its excellent service and sustainability practices\n",
"\n",
"Would you like more specific information about either of these properties or would you like to explore other options in either area?\n",
"\n",
"--- Conversation Turn 3 ---\n",
"\n",
"User: Command(resume='i like the first one. could you recommend something to do near the hotel?')\n",
"\n",
"call_travel_advisor: Near the Ritz-Carlton in Palm Beach, you can enjoy several fantastic activities:\n",
"\n",
"1. Paseo Herencia Mall - A beautiful outdoor shopping and entertainment center just a short walk away\n",
"2. High-Rise Beach Strip - Perfect for beach walks and water sports\n",
"3. Bubali Bird Sanctuary - A nature preserve where you can spot local wildlife\n",
"4. The Butterfly Farm - A unique attraction featuring hundreds of exotic butterflies\n",
"5. Palm Beach Plaza Mall - Great for shopping and dining\n",
"6. Various water sports operators offering:\n",
" - Jet skiing\n",
" - Parasailing\n",
" - Snorkeling trips\n",
" - Sunset sailing cruises\n",
"\n",
"Additionally, the hotel concierge can arrange most activities directly for you. Would you like more specific information about any of these activities?\n"
]
}
],
"source": [
"import uuid\n",
"\n",
"thread_config = {\"configurable\": {\"thread_id\": uuid.uuid4()}}\n",
"\n",
"inputs = [\n",
" # 1st round of conversation,\n",
" {\"role\": \"user\", \"content\": \"i wanna go somewhere warm in the caribbean\"},\n",
" # Since we're using `interrupt`, we'll need to resume using the Command primitive.\n",
" # 2nd round of conversation,\n",
" Command(\n",
" resume=\"could you recommend a nice hotel in one of the areas and tell me which area it is.\"\n",
" ),\n",
" # 3rd round of conversation,\n",
" Command(\n",
" resume=\"i like the first one. could you recommend something to do near the hotel?\"\n",
" ),\n",
"]\n",
"\n",
"for idx, user_input in enumerate(inputs):\n",
" print()\n",
" print(f\"--- Conversation Turn {idx + 1} ---\")\n",
" print()\n",
" print(f\"User: {user_input}\")\n",
" print()\n",
" for update in multi_turn_graph.stream(\n",
" user_input,\n",
" config=thread_config,\n",
" stream_mode=\"updates\",\n",
" ):\n",
" for node_id, value in update.items():\n",
" if isinstance(value, list) and value:\n",
" last_message = value[-1]\n",
" if isinstance(last_message, dict) or last_message.type != \"ai\":\n",
" continue\n",
" print(f\"{node_id}: {last_message.content}\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -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",
@@ -0,0 +1,500 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "87684b48-150e-4e15-b0a5-a9dd7851f8fb",
"metadata": {},
"source": [
"# How to build a multi-agent network (functional API)"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "2c65639c-9705-49f1-840a-370718852e98",
"metadata": {},
"source": [
"!!! info \"Prerequisites\" \n",
" This guide assumes familiarity with the following:\n",
"\n",
" - [Multi-agent systems](../../concepts/multi_agent)\n",
" - [Functional API](../../concepts/functional_api)\n",
" - [Command](../../concepts/low_level/#command)\n",
" - [LangGraph Glossary](../../concepts/low_level/)\n",
"\n",
"In this how-to guide we will demonstrate how to implement a [multi-agent network](../../concepts/multi_agent#network) architecture where each agent can communicate with every other agent (many-to-many connections) and can decide which agent to call next. We will be using [functional API](../../concepts/functional_api) — individual agents will be defined as tasks and the agent handoffs will be defined in the main [entrypoint()][langgraph.func.entrypoint]:\n",
"\n",
"```python\n",
"from langgraph.func import entrypoint\n",
"from langgraph.prebuilt import create_react_agent\n",
"from langchain_core.tools import tool\n",
"\n",
"\n",
"# Define a tool to signal intent to hand off to a different agent\n",
"@tool(return_direct=True)\n",
"def transfer_to_hotel_advisor():\n",
" \"\"\"Ask hotel advisor agent for help.\"\"\"\n",
" return \"Successfully transferred to hotel advisor\"\n",
"\n",
"\n",
"# define an agent\n",
"travel_advisor_tools = [transfer_to_hotel_advisor, ...]\n",
"travel_advisor = create_react_agent(model, travel_advisor_tools)\n",
"\n",
"\n",
"# define a task that calls an agent\n",
"@task\n",
"def call_travel_advisor(messages):\n",
" response = travel_advisor.invoke({\"messages\": messages})\n",
" return response[\"messages\"]\n",
"\n",
"\n",
"# define the multi-agent network workflow\n",
"@entrypoint()\n",
"def workflow(messages):\n",
" call_active_agent = call_travel_advisor\n",
" while True:\n",
" agent_messages = call_active_agent(messages).result()\n",
" messages = messages + agent_messages\n",
" call_active_agent = get_next_agent(messages)\n",
" return messages\n",
"```"
]
},
{
"cell_type": "markdown",
"id": "faaa4444-cd06-4813-b9ca-c9700fe12cb7",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"First, let's install the required packages"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "05038da0-31df-4066-a1a4-c4ccb5db4d3a",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-anthropic"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "0bcff5d4-130e-426d-9285-40d0f72c7cd3",
"metadata": {},
"outputs": [
{
"name": "stdin",
"output_type": "stream",
"text": [
"ANTHROPIC_API_KEY: ········\n"
]
}
],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"ANTHROPIC_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "c3ec6e48-85dc-4905-ba50-985e5d4788e6",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
" </p>\n",
"</div>"
]
},
{
"cell_type": "markdown",
"id": "4a53f304-3709-4df7-8714-1ca61e615743",
"metadata": {},
"source": [
"## Travel agent example"
]
},
{
"cell_type": "markdown",
"id": "34cd131b-f0c2-4b69-887f-2cbd5afb14a7",
"metadata": {},
"source": [
"In this example we will build a team of travel assistant agents that can communicate with each other.\n",
"\n",
"We will create 2 agents:\n",
"\n",
"* `travel_advisor`: can help with travel destination recommendations. Can ask `hotel_advisor` for help.\n",
"* `hotel_advisor`: can help with hotel recommendations. Can ask `travel_advisor` for help.\n",
"\n",
"This is a fully-connected network - every agent can talk to any other agent. "
]
},
{
"cell_type": "markdown",
"id": "fedc9ed0-e90c-4ee1-a7c6-f5af3c634a7b",
"metadata": {},
"source": [
"First, let's create some of the tools that the agents will be using:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "7e31f258-ec28-4020-b86d-c91dfa9a3bfc",
"metadata": {},
"outputs": [],
"source": [
"import random\n",
"from typing_extensions import Literal\n",
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def get_travel_recommendations():\n",
" \"\"\"Get recommendation for travel destinations\"\"\"\n",
" return random.choice([\"aruba\", \"turks and caicos\"])\n",
"\n",
"\n",
"@tool\n",
"def get_hotel_recommendations(location: Literal[\"aruba\", \"turks and caicos\"]):\n",
" \"\"\"Get hotel recommendations for a given destination.\"\"\"\n",
" return {\n",
" \"aruba\": [\n",
" \"The Ritz-Carlton, Aruba (Palm Beach)\"\n",
" \"Bucuti & Tara Beach Resort (Eagle Beach)\"\n",
" ],\n",
" \"turks and caicos\": [\"Grace Bay Club\", \"COMO Parrot Cay\"],\n",
" }[location]\n",
"\n",
"\n",
"@tool(return_direct=True)\n",
"def transfer_to_hotel_advisor():\n",
" \"\"\"Ask hotel advisor agent for help.\"\"\"\n",
" return \"Successfully transferred to hotel advisor\"\n",
"\n",
"\n",
"@tool(return_direct=True)\n",
"def transfer_to_travel_advisor():\n",
" \"\"\"Ask travel advisor agent for help.\"\"\"\n",
" return \"Successfully transferred to travel advisor\""
]
},
{
"cell_type": "markdown",
"id": "d8519a32-d23b-48b0-bd18-74f8c0dacf58",
"metadata": {},
"source": [
"!!! note \"Transfer tools\"\n",
"\n",
" You might have noticed that we're using `@tool(return_direct=True)` in the transfer tools. This is done so that individual agents (e.g., `travel_advisor`) can exit the ReAct loop early once these tools are called. This is the desired behavior, as we want to detect when the agent calls this tool and hand control off _immediately_ to a different agent. \n",
" \n",
" **NOTE**: This is meant to work with the prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] -- if you are building a custom agent, make sure to manually add logic for handling early exit for tools that are marked with `return_direct`."
]
},
{
"cell_type": "markdown",
"id": "93dbc3bd-27b9-4d79-b5dd-be592bc50f74",
"metadata": {},
"source": [
"Now let's define our agent tasks and combine them into a single multi-agent network workflow:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "b638d6c4-3de6-4921-980c-2df1bd1cc9c7",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import AIMessage\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langgraph.prebuilt import create_react_agent\n",
"from langgraph.graph import add_messages\n",
"from langgraph.func import entrypoint, task\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")\n",
"\n",
"# Define travel advisor ReAct agent\n",
"travel_advisor_tools = [\n",
" get_travel_recommendations,\n",
" transfer_to_hotel_advisor,\n",
"]\n",
"travel_advisor = create_react_agent(\n",
" model,\n",
" travel_advisor_tools,\n",
" state_modifier=(\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",
" ),\n",
")\n",
"\n",
"\n",
"@task\n",
"def call_travel_advisor(messages):\n",
" # You can also add additional logic like changing the input to the agent / output from the agent, etc.\n",
" # NOTE: we're invoking the ReAct agent with the full history of messages in the state\n",
" response = travel_advisor.invoke({\"messages\": messages})\n",
" return response[\"messages\"]\n",
"\n",
"\n",
"# Define hotel advisor ReAct agent\n",
"hotel_advisor_tools = [get_hotel_recommendations, transfer_to_travel_advisor]\n",
"hotel_advisor = create_react_agent(\n",
" model,\n",
" hotel_advisor_tools,\n",
" state_modifier=(\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",
" ),\n",
")\n",
"\n",
"\n",
"@task\n",
"def call_hotel_advisor(messages):\n",
" response = hotel_advisor.invoke({\"messages\": messages})\n",
" return response[\"messages\"]\n",
"\n",
"\n",
"@entrypoint()\n",
"def workflow(messages):\n",
" messages = add_messages([], messages)\n",
"\n",
" call_active_agent = call_travel_advisor\n",
" while True:\n",
" agent_messages = call_active_agent(messages).result()\n",
" messages = add_messages(messages, agent_messages)\n",
" ai_msg = next(m for m in reversed(agent_messages) if isinstance(m, AIMessage))\n",
" if not ai_msg.tool_calls:\n",
" break\n",
"\n",
" tool_call = ai_msg.tool_calls[-1]\n",
" if tool_call[\"name\"] == \"transfer_to_travel_advisor\":\n",
" call_active_agent = call_travel_advisor\n",
" elif tool_call[\"name\"] == \"transfer_to_hotel_advisor\":\n",
" call_active_agent = call_hotel_advisor\n",
" else:\n",
" raise ValueError(f\"Expected transfer tool, got '{tool_call['name']}'\")\n",
"\n",
" return messages"
]
},
{
"cell_type": "markdown",
"id": "9223db83-1938-434a-9d24-8666842a8eea",
"metadata": {},
"source": [
"Lastly, let's define a helper to render the agent outputs:"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "058f3d96-534f-4b97-afb3-799ba81224ea",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import convert_to_messages\n",
"\n",
"\n",
"def pretty_print_messages(update):\n",
" if isinstance(update, tuple):\n",
" ns, update = update\n",
" # skip parent graph updates in the printouts\n",
" if len(ns) == 0:\n",
" return\n",
"\n",
" graph_id = ns[-1].split(\":\")[0]\n",
" print(f\"Update from subgraph {graph_id}:\")\n",
" print(\"\\n\")\n",
"\n",
" for node_name, node_update in update.items():\n",
" print(f\"Update from node {node_name}:\")\n",
" print(\"\\n\")\n",
"\n",
" for m in convert_to_messages(node_update[\"messages\"]):\n",
" m.pretty_print()\n",
" print(\"\\n\")"
]
},
{
"cell_type": "markdown",
"id": "7132e2c0-d937-4325-a30e-e715c5304fe0",
"metadata": {},
"source": [
"Let's test it out using the same input as our original multi-agent system:"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "29b47c57-ad05-4f10-83bf-c3ff6ff8eb93",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Update from subgraph call_travel_advisor:\n",
"\n",
"\n",
"Update from node agent:\n",
"\n",
"\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"[{'text': \"I'll help you find a warm Caribbean destination and then get some hotel recommendations for you.\\n\\nLet me first get some destination recommendations for the Caribbean region.\", 'type': 'text'}, {'id': 'toolu_015vT8PkPq1VXvjrDvSpWUwJ', 'input': {}, 'name': 'get_travel_recommendations', 'type': 'tool_use'}]\n",
"Tool Calls:\n",
" get_travel_recommendations (toolu_015vT8PkPq1VXvjrDvSpWUwJ)\n",
" Call ID: toolu_015vT8PkPq1VXvjrDvSpWUwJ\n",
" Args:\n",
"\n",
"\n",
"Update from subgraph call_travel_advisor:\n",
"\n",
"\n",
"Update from node tools:\n",
"\n",
"\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: get_travel_recommendations\n",
"\n",
"turks and caicos\n",
"\n",
"\n",
"Update from subgraph call_travel_advisor:\n",
"\n",
"\n",
"Update from node agent:\n",
"\n",
"\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"[{'text': \"Based on the recommendation, I suggest Turks and Caicos! This beautiful British Overseas Territory is known for its stunning white-sand beaches, crystal-clear turquoise waters, and year-round warm weather. Grace Bay Beach in Providenciales is consistently ranked among the world's best beaches. The islands offer excellent snorkeling, diving, and water sports opportunities, plus a relaxed Caribbean atmosphere.\\n\\nNow, let me connect you with our hotel advisor to get some specific hotel recommendations for Turks and Caicos.\", 'type': 'text'}, {'id': 'toolu_01JY7pNNWFuaWoe9ymxFYiPV', 'input': {}, 'name': 'transfer_to_hotel_advisor', 'type': 'tool_use'}]\n",
"Tool Calls:\n",
" transfer_to_hotel_advisor (toolu_01JY7pNNWFuaWoe9ymxFYiPV)\n",
" Call ID: toolu_01JY7pNNWFuaWoe9ymxFYiPV\n",
" Args:\n",
"\n",
"\n",
"Update from subgraph call_travel_advisor:\n",
"\n",
"\n",
"Update from node tools:\n",
"\n",
"\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: transfer_to_hotel_advisor\n",
"\n",
"Successfully transferred to hotel advisor\n",
"\n",
"\n",
"Update from subgraph call_hotel_advisor:\n",
"\n",
"\n",
"Update from node agent:\n",
"\n",
"\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"[{'text': 'Let me get some hotel recommendations for Turks and Caicos:', 'type': 'text'}, {'id': 'toolu_0129ELa7jFocn16bowaGNapg', 'input': {'location': 'turks and caicos'}, 'name': 'get_hotel_recommendations', 'type': 'tool_use'}]\n",
"Tool Calls:\n",
" get_hotel_recommendations (toolu_0129ELa7jFocn16bowaGNapg)\n",
" Call ID: toolu_0129ELa7jFocn16bowaGNapg\n",
" Args:\n",
" location: turks and caicos\n",
"\n",
"\n",
"Update from subgraph call_hotel_advisor:\n",
"\n",
"\n",
"Update from node tools:\n",
"\n",
"\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: get_hotel_recommendations\n",
"\n",
"[\"Grace Bay Club\", \"COMO Parrot Cay\"]\n",
"\n",
"\n",
"Update from subgraph call_hotel_advisor:\n",
"\n",
"\n",
"Update from node agent:\n",
"\n",
"\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Here are two excellent hotel options in Turks and Caicos:\n",
"\n",
"1. Grace Bay Club: This luxury resort is located on the world-famous Grace Bay Beach. It offers all-oceanfront suites, exceptional dining options, and personalized service. The resort features adult-only and family-friendly sections, making it perfect for any type of traveler.\n",
"\n",
"2. COMO Parrot Cay: This exclusive private island resort offers the ultimate luxury escape. It's known for its pristine beach, world-class spa, and holistic wellness programs. The resort provides an intimate, secluded experience with top-notch amenities and service.\n",
"\n",
"Would you like more specific information about either of these properties or would you like to explore hotels in another destination?\n",
"\n",
"\n"
]
}
],
"source": [
"for chunk in workflow.stream(\n",
" [\n",
" {\n",
" \"role\": \"user\",\n",
" \"content\": \"i wanna go somewhere warm in the caribbean. pick one destination and give me hotel recommendations\",\n",
" }\n",
" ],\n",
" subgraphs=True,\n",
"):\n",
" pretty_print_messages(chunk)"
]
},
{
"cell_type": "markdown",
"id": "d7d89ee0-0229-4718-9b98-bdd3f59c1014",
"metadata": {},
"source": [
"Voila - `travel_advisor` picks a destination and then makes a decision to call `hotel_advisor` for more info!"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+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",
@@ -0,0 +1,349 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53",
"metadata": {},
"source": [
"# How to add thread-level persistence (functional API)\n",
"\n",
"!!! info \"Prerequisites\"\n",
"\n",
" This guide assumes familiarity with the following:\n",
" \n",
" - [Functional API](../../concepts/functional_api/)\n",
" - [Persistence](../../concepts/persistence/)\n",
" - [Memory](../../concepts/memory/)\n",
" - [Chat Models](https://python.langchain.com/docs/concepts/chat_models/)\n",
"\n",
"Many AI applications need memory to share context across multiple interactions on the same [thread](../../concepts/persistence#threads) (e.g., multiple turns of a conversation). In LangGraph functional API, this kind of memory can be added to any [entrypoint()][langgraph.func.entrypoint] workflow using [thread-level persistence](https://langchain-ai.github.io/langgraph/concepts/persistence).\n",
"\n",
"When creating a LangGraph workflow, you can set it up to persist its results by using a [checkpointer](https://langchain-ai.github.io/langgraph/reference/checkpoints/#basecheckpointsaver):\n",
"\n",
"\n",
"1. Create an instance of a checkpointer:\n",
"\n",
" ```python\n",
" from langgraph.checkpoint.memory import MemorySaver\n",
" \n",
" checkpointer = MemorySaver() \n",
" ```\n",
"\n",
"2. Pass `checkpointer` instance to the `entrypoint()` decorator:\n",
"\n",
" ```python\n",
" from langgraph.func import entrypoint\n",
" \n",
" @entrypoint(checkpointer=checkpointer)\n",
" def workflow(inputs)\n",
" ...\n",
" ```\n",
"\n",
"3. Optionally expose `previous` parameter in the workflow function signature:\n",
"\n",
" ```python\n",
" @entrypoint(checkpointer=checkpointer)\n",
" def workflow(\n",
" inputs,\n",
" *,\n",
" # you can optionally specify `previous` in the workflow function signature\n",
" # to access the return value from the workflow as of the last execution\n",
" previous\n",
" ):\n",
" previous = previous or []\n",
" combined_inputs = previous + inputs\n",
" result = do_something(combined_inputs)\n",
" ...\n",
" ```\n",
"\n",
"4. Optionally choose which values will be returned from the workflow and which will be saved by the checkpointer as `previous`:\n",
"\n",
" ```python\n",
" @entrypoint(checkpointer=checkpointer)\n",
" def workflow(inputs, *, previous):\n",
" ...\n",
" result = do_something(...)\n",
" return entrypoint.final(value=result, save=combine(inputs, result))\n",
" ```\n",
"\n",
"This guide shows how you can add thread-level persistence to your workflow.\n",
"\n",
"!!! tip \"Note\"\n",
"\n",
" If you need memory that is __shared__ across multiple conversations or users (cross-thread persistence), check out this [how-to guide](../cross-thread-persistence-functional).\n",
"\n",
"!!! tip \"Note\"\n",
"\n",
" If you need to add thread-level persistence to a `StateGraph`, check out this [how-to guide](../persistence)."
]
},
{
"cell_type": "markdown",
"id": "7cbd446a-808f-4394-be92-d45ab818953c",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"First we need to install the packages required"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_anthropic"
]
},
{
"cell_type": "markdown",
"id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d",
"metadata": {},
"source": [
"Next, we need to set API key for Anthropic (the LLM we will use)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"ANTHROPIC_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
" </p>\n",
"</div>"
]
},
{
"cell_type": "markdown",
"id": "4cf509bc",
"metadata": {},
"source": [
"## Example: simple chatbot with short-term memory\n",
"\n",
"We will be using a workflow with a single task that calls a [chat model](https://python.langchain.com/docs/concepts/chat_models/).\n",
"\n",
"Let's first define the model we'll be using:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
"metadata": {},
"outputs": [],
"source": [
"from langchain_anthropic import ChatAnthropic\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")"
]
},
{
"cell_type": "markdown",
"id": "7b7a2792-982b-4e47-83eb-0c594725d1c1",
"metadata": {},
"source": [
"Now we can define our task and workflow. To add in persistence, we need to pass in a [Checkpointer](https://langchain-ai.github.io/langgraph/reference/checkpoints/#langgraph.checkpoint.base.BaseCheckpointSaver) to the [entrypoint()][langgraph.func.entrypoint] decorator."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "87326ea6-34c5-46da-a41f-dda26ef9bd74",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import BaseMessage\n",
"from langgraph.graph import add_messages\n",
"from langgraph.func import entrypoint, task\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"\n",
"@task\n",
"def call_model(messages: list[BaseMessage]):\n",
" response = model.invoke(messages)\n",
" return response\n",
"\n",
"\n",
"checkpointer = MemorySaver()\n",
"\n",
"\n",
"@entrypoint(checkpointer=checkpointer)\n",
"def workflow(inputs: list[BaseMessage], *, previous: list[BaseMessage]):\n",
" if previous:\n",
" inputs = add_messages(previous, inputs)\n",
"\n",
" response = call_model(inputs).result()\n",
" return entrypoint.final(value=response, save=add_messages(inputs, response))"
]
},
{
"cell_type": "markdown",
"id": "250d8fd9-2e7a-4892-9adc-19762a1e3cce",
"metadata": {},
"source": [
"If we try to use this workflow, the context of the conversation will be persisted across interactions:"
]
},
{
"cell_type": "markdown",
"id": "7654ebcc-2179-41b4-92d1-6666f6f8634f",
"metadata": {},
"source": [
"!!! note Note\n",
"\n",
" If you're using LangGraph Cloud or LangGraph Studio, you __don't need__ to pass checkpointer to the entrypoint decorator, since it's done automatically."
]
},
{
"cell_type": "markdown",
"id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159",
"metadata": {},
"source": [
"We can now interact with the agent and see that it remembers previous messages!"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "cfd140f0-a5a6-4697-8115-322242f197b5",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Hi Bob! I'm Claude. Nice to meet you! How are you today?\n"
]
}
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
"input_message = {\"role\": \"user\", \"content\": \"hi! I'm bob\"}\n",
"for chunk in workflow.stream([input_message], config, stream_mode=\"values\"):\n",
" chunk.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "1bb07bf8-68b7-4049-a0f1-eb67a4879a3a",
"metadata": {},
"source": [
"You can always resume previous threads:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "08ae8246-11d5-40e1-8567-361e5bef8917",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Your name is Bob.\n"
]
}
],
"source": [
"input_message = {\"role\": \"user\", \"content\": \"what's my name?\"}\n",
"for chunk in workflow.stream([input_message], config, stream_mode=\"values\"):\n",
" chunk.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "3f47bbfc-d9ef-4288-ba4a-ebbc0136fa9d",
"metadata": {},
"source": [
"If we want to start a new conversation, we can pass in a different `thread_id`. Poof! All the memories are gone!"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "273d56a8-f40f-4a51-a27f-7c6bb2bda0ba",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"I don't know your name unless you tell me. Each conversation I have starts fresh, so I don't have access to any previous interactions or personal information unless you share it with me.\n"
]
}
],
"source": [
"input_message = {\"role\": \"user\", \"content\": \"what's my name?\"}\n",
"for chunk in workflow.stream(\n",
" [input_message],\n",
" {\"configurable\": {\"thread_id\": \"2\"}},\n",
" stream_mode=\"values\",\n",
"):\n",
" chunk.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "ac7926a8-4c88-4b16-973c-53d6da3f4a08",
"metadata": {},
"source": [
"!!! tip \"Streaming tokens\"\n",
"\n",
" If you would like to stream LLM tokens from your chatbot, you can use `stream_mode=\"messages\"`. Check out this [how-to guide](../streaming-tokens) to learn more."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+7 -7
View File
@@ -78,7 +78,7 @@
"id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d",
"metadata": {},
"source": [
"Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)"
"Next, we need to set API key for Anthropic (the LLM we will use)."
]
},
{
@@ -211,11 +211,11 @@
}
],
"source": [
"input_message = {\"type\": \"user\", \"content\": \"hi! I'm bob\"}\n",
"input_message = {\"role\": \"user\", \"content\": \"hi! I'm bob\"}\n",
"for chunk in graph.stream({\"messages\": [input_message]}, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()\n",
"\n",
"input_message = {\"type\": \"user\", \"content\": \"what's my name?\"}\n",
"input_message = {\"role\": \"user\", \"content\": \"what's my name?\"}\n",
"for chunk in graph.stream({\"messages\": [input_message]}, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
@@ -286,7 +286,7 @@
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
"input_message = {\"type\": \"user\", \"content\": \"hi! I'm bob\"}\n",
"input_message = {\"role\": \"user\", \"content\": \"hi! I'm bob\"}\n",
"for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
@@ -319,7 +319,7 @@
}
],
"source": [
"input_message = {\"type\": \"user\", \"content\": \"what's my name?\"}\n",
"input_message = {\"role\": \"user\", \"content\": \"what's my name?\"}\n",
"for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
@@ -352,7 +352,7 @@
}
],
"source": [
"input_message = {\"type\": \"user\", \"content\": \"what's my name?\"}\n",
"input_message = {\"role\": \"user\", \"content\": \"what's my name?\"}\n",
"for chunk in graph.stream(\n",
" {\"messages\": [input_message]},\n",
" {\"configurable\": {\"thread_id\": \"2\"}},\n",
@@ -378,7 +378,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
"version": "3.12.3"
}
},
"nbformat": 4,
+2 -1
View File
@@ -44,7 +44,8 @@
"...\n",
"```\n",
"\n",
"!!! info \"Setup\n",
"!!! info \"Setup\"",
"\n",
" You need to run `.setup()` once on your checkpointer to initialize the database before you can use it."
]
},
@@ -0,0 +1,463 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to create a ReAct agent from scratch (Functional API)\n",
"\n",
"!!! info \"Prerequisites\"\n",
" This guide assumes familiarity with the following:\n",
" \n",
" - [Chat Models](https://python.langchain.com/docs/concepts/chat_models)\n",
" - [Messages](https://python.langchain.com/docs/concepts/messages)\n",
" - [Tool Calling](https://python.langchain.com/docs/concepts/tool_calling/)\n",
" - [Entrypoints](../../concepts/functional_api/#entrypoint) and [Tasks](../../concepts/functional_api/#task)\n",
"\n",
"This guide demonstrates how to implement a ReAct agent using the LangGraph [Functional API](../../concepts/functional_api).\n",
"\n",
"The ReAct agent is a [tool-calling agent](../../concepts/agentic_concepts/#tool-calling-agent) that operates as follows:\n",
"\n",
"1. Queries are issued to a chat model;\n",
"2. If the model generates no [tool calls](../../concepts/agentic_concepts/#tool-calling), we return the model response.\n",
"3. If the model generates tool calls, we execute the tool calls with available tools, append them as [tool messages](https://python.langchain.com/docs/concepts/messages/) to our message list, and repeat the process.\n",
"\n",
"This is a simple and versatile set-up that can be extended with memory, human-in-the-loop capabilities, and other features. See the dedicated [how-to guides](../../how-tos/#prebuilt-react-agent) for examples.\n",
"\n",
"## Setup\n",
"\n",
"First, let's install the required packages and set our API keys:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for better debugging</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM aps built with LangGraph — read more about how to get started in the <a href=\"https://docs.smith.langchain.com\">docs</a>. \n",
" </p>\n",
" </div>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Create ReAct agent\n",
"\n",
"Now that you have installed the required packages and set your environment variables, we can create our agent.\n",
"\n",
"### Define model and tools\n",
"\n",
"Let's first define the tools and model we will use for our example. Here we will use a single place-holder tool that gets a description of the weather for a location.\n",
"\n",
"We will use an [OpenAI](https://python.langchain.com/docs/integrations/providers/openai/) chat model for this example, but any model [supporting tool-calling](https://python.langchain.com/docs/integrations/chat/) will suffice."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"from langchain_core.tools import tool\n",
"\n",
"model = ChatOpenAI(model=\"gpt-4o-mini\")\n",
"\n",
"\n",
"@tool\n",
"def get_weather(location: str):\n",
" \"\"\"Call to get the weather from a specific location.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" if any([city in location.lower() for city in [\"sf\", \"san francisco\"]]):\n",
" return \"It's sunny!\"\n",
" elif \"boston\" in location.lower():\n",
" return \"It's rainy!\"\n",
" else:\n",
" return f\"I am not sure what the weather is in {location}\"\n",
"\n",
"\n",
"tools = [get_weather]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Define tasks\n",
"\n",
"We next define the [tasks](../../concepts/functional_api/#task) we will execute. Here there are two different tasks:\n",
"\n",
"1. **Call model**: We want to query our chat model with a list of messages.\n",
"2. **Call tool**: If our model generates tool calls, we want to execute them."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import ToolMessage\n",
"from langgraph.func import entrypoint, task\n",
"\n",
"tools_by_name = {tool.name: tool for tool in tools}\n",
"\n",
"\n",
"@task\n",
"def call_model(messages):\n",
" \"\"\"Call model with a sequence of messages.\"\"\"\n",
" response = model.bind_tools(tools).invoke(messages)\n",
" return response\n",
"\n",
"\n",
"@task\n",
"def call_tool(tool_call):\n",
" tool = tools_by_name[tool_call[\"name\"]]\n",
" observation = tool.invoke(tool_call[\"args\"])\n",
" return ToolMessage(content=observation, tool_call_id=tool_call[\"id\"])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Define entrypoint\n",
"\n",
"Our [entrypoint](../../concepts/functional_api/#entrypoint) will handle the orchestration of these two tasks. As described above, when our `call_model` task generates tool calls, the `call_tool` task will generate responses for each. We append all messages to a single messages list.\n",
"\n",
"!!! tip\n",
" Note that because tasks return future-like objects, the below implementation executes tools in parallel."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph.message import add_messages\n",
"\n",
"\n",
"@entrypoint()\n",
"def agent(messages):\n",
" llm_response = call_model(messages).result()\n",
" while True:\n",
" if not llm_response.tool_calls:\n",
" break\n",
"\n",
" # Execute tools\n",
" tool_result_futures = [\n",
" call_tool(tool_call) for tool_call in llm_response.tool_calls\n",
" ]\n",
" tool_results = [fut.result() for fut in tool_result_futures]\n",
"\n",
" # Append to message list\n",
" messages = add_messages(messages, [llm_response, *tool_results])\n",
"\n",
" # Call model again\n",
" llm_response = call_model(messages).result()\n",
"\n",
" return llm_response"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Usage\n",
"\n",
"To use our agent, we invoke it with a messages list. Based on our implementation, these can be LangChain [message](https://python.langchain.com/docs/concepts/messages/) objects or OpenAI-style dicts:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'role': 'user', 'content': \"What's the weather in san francisco?\"}\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_tNnkrjnoz6MNfCHJpwfuEQ0v)\n",
" Call ID: call_tNnkrjnoz6MNfCHJpwfuEQ0v\n",
" Args:\n",
" location: san francisco\n",
"\n",
"call_tool:\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"It's sunny!\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in San Francisco is sunny!\n"
]
}
],
"source": [
"user_message = {\"role\": \"user\", \"content\": \"What's the weather in san francisco?\"}\n",
"print(user_message)\n",
"\n",
"for step in agent.stream([user_message]):\n",
" for task_name, message in step.items():\n",
" if task_name == \"agent\":\n",
" continue # Just print task updates\n",
" print(f\"\\n{task_name}:\")\n",
" message.pretty_print()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Perfect! The graph correctly calls the `get_weather` tool and responds to the user after receiving the information from the tool. Check out the LangSmith trace [here](https://smith.langchain.com/public/d5a0d5ea-bdaa-4032-911e-7db177c8141b/r)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Add thread-level persistence\n",
"\n",
"Adding [thread-level persistence](../../concepts/persistence#threads) lets us support conversational experiences with our agent: subsequent invocations will append to the prior messages list, retaining the full conversational context.\n",
"\n",
"To add thread-level persistence to our agent:\n",
"\n",
"1. Select a [checkpointer](../../concepts/persistence#checkpointer-libraries): here we will use [MemorySaver](../../reference/checkpoints/#langgraph.checkpoint.memory.MemorySaver), a simple in-memory checkpointer.\n",
"2. Update our entrypoint to accept the previous messages state as a second argument. Here, we simply append the message updates to the previous sequence of messages.\n",
"3. Choose which values will be returned from the workflow and which will be saved by the checkpointer as `previous` using `entrypoint.final` (optional)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"# highlight-next-line\n",
"checkpointer = MemorySaver()\n",
"\n",
"\n",
"# highlight-next-line\n",
"@entrypoint(checkpointer=checkpointer)\n",
"# highlight-next-line\n",
"def agent(messages, previous):\n",
" # highlight-next-line\n",
" if previous is not None:\n",
" # highlight-next-line\n",
" messages = add_messages(previous, messages)\n",
"\n",
" llm_response = call_model(messages).result()\n",
" while True:\n",
" if not llm_response.tool_calls:\n",
" break\n",
"\n",
" # Execute tools\n",
" tool_result_futures = [\n",
" call_tool(tool_call) for tool_call in llm_response.tool_calls\n",
" ]\n",
" tool_results = [fut.result() for fut in tool_result_futures]\n",
"\n",
" # Append to message list\n",
" messages = add_messages(messages, [llm_response, *tool_results])\n",
"\n",
" # Call model again\n",
" llm_response = call_model(messages).result()\n",
"\n",
" # Generate final response\n",
" messages = add_messages(messages, llm_response)\n",
" # highlight-next-line\n",
" return entrypoint.final(value=llm_response, save=messages)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We will now need to pass in a config when running our application. The config will specify an identifier for the conversational thread.\n",
"\n",
"!!! tip\n",
"\n",
" Read more about thread-level persistence in our [concepts page](../../concepts/persistence/) and [how-to guides](../../how-tos/#persistence)."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"config = {\"configurable\": {\"thread_id\": \"1\"}}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We start a thread the same way as before, this time passing in the config:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'role': 'user', 'content': \"What's the weather in san francisco?\"}\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_lubbUSdDofmOhFunPEZLBz3g)\n",
" Call ID: call_lubbUSdDofmOhFunPEZLBz3g\n",
" Args:\n",
" location: San Francisco\n",
"\n",
"call_tool:\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"It's sunny!\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in San Francisco is sunny!\n"
]
}
],
"source": [
"user_message = {\"role\": \"user\", \"content\": \"What's the weather in san francisco?\"}\n",
"print(user_message)\n",
"\n",
"# highlight-next-line\n",
"for step in agent.stream([user_message], config):\n",
" for task_name, message in step.items():\n",
" if task_name == \"agent\":\n",
" continue # Just print task updates\n",
" print(f\"\\n{task_name}:\")\n",
" message.pretty_print()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"When we ask a follow-up conversation, the model uses the prior context to infer that we are asking about the weather:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'role': 'user', 'content': 'How does it compare to Boston, MA?'}\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_8sTKYAhSIHOdjLD5d6gaswuV)\n",
" Call ID: call_8sTKYAhSIHOdjLD5d6gaswuV\n",
" Args:\n",
" location: Boston, MA\n",
"\n",
"call_tool:\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"It's rainy!\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Compared to San Francisco, which is sunny, Boston, MA is experiencing rainy weather.\n"
]
}
],
"source": [
"user_message = {\"role\": \"user\", \"content\": \"How does it compare to Boston, MA?\"}\n",
"print(user_message)\n",
"\n",
"for step in agent.stream([user_message], config):\n",
" for task_name, message in step.items():\n",
" if task_name == \"agent\":\n",
" continue # Just print task updates\n",
" print(f\"\\n{task_name}:\")\n",
" message.pretty_print()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In the [LangSmith trace](https://smith.langchain.com/public/20a1116b-bb3b-44c1-8765-7a28663439d9/r), we can see that the full conversational context is retained in each model call."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
@@ -6,37 +6,15 @@
"source": [
"# How to create a ReAct agent from scratch\n",
"\n",
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Prerequisites</p>\n",
" <p>\n",
" This guide assumes familiarity with the following:\n",
" <ul>\n",
" <li>\n",
" <a href=\"https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#tool-calling-agent\">\n",
" Tool calling agent\n",
" </a>\n",
" </li> \n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#chat-models\">\n",
" Chat Models\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://python.langchain.com/docs/concepts/#messages\">\n",
" Messages\n",
" </a>\n",
" </li>\n",
" <li>\n",
" <a href=\"https://langchain-ai.github.io/langgraph/concepts/low_level/\">\n",
" LangGraph Glossary\n",
" </a>\n",
" </li>\n",
" </ul>\n",
" </p>\n",
"</div> \n",
"!!! info \"Prerequisites\"\n",
" This guide assumes familiarity with the following:\n",
" \n",
" - [Tool calling agent](../../concepts/agentic_concepts/#tool-calling-agent)\n",
" - [Chat Models](https://python.langchain.com/docs/concepts/chat_models/)\n",
" - [Messages](https://python.langchain.com/docs/concepts/messages/)\n",
" - [LangGraph Glossary](../../concepts/low_level/)\n",
"\n",
"\n",
"Using the prebuilt ReAct agent ([create_react_agent](https://langchain-ai.github.io/langgraph/reference/prebuilt/#langgraph.prebuilt.chat_agent_executor.create_react_agent)) is a great way to get started, but sometimes you might want more control and customization. In those cases, you can create a custom ReAct agent. This guide shows how to implement ReAct agent from scratch using LangGraph.\n",
"Using the prebuilt ReAct agent [create_react_agent][langgraph.prebuilt.chat_agent_executor.create_react_agent] is a great way to get started, but sometimes you might want more control and customization. In those cases, you can create a custom ReAct agent. This guide shows how to implement ReAct agent from scratch using LangGraph.\n",
"\n",
"## Setup\n",
"\n",
@@ -202,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",
@@ -375,7 +353,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.12.3"
}
},
"nbformat": 4,
@@ -15,7 +15,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to return structured output with a ReAct style agent\n",
"# How to force tool-calling agent to structure output\n",
"\n",
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Prerequisites</p>\n",
@@ -0,0 +1,627 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to review tool calls (Functional API)\n",
"\n",
"!!! info \"Prerequisites\"\n",
" This guide assumes familiarity with the following:\n",
"\n",
" - Implementing [human-in-the-loop](../../concepts/human_in_the_loop) workflows with [interrupt](../../concepts/human_in_the_loop/#interrupt)\n",
" - [How to create a ReAct agent using the Functional API](../../how-tos/react-agent-from-scratch-functional)\n",
"\n",
"This guide demonstrates how to implement human-in-the-loop workflows in a ReAct agent using the LangGraph [Functional API](../../concepts/functional_api).\n",
"\n",
"We will build off of the agent created in the [How to create a ReAct agent using the Functional API](../../how-tos/react-agent-from-scratch-functional) guide.\n",
"\n",
"Specifically, we will demonstrate how to review [tool calls](https://python.langchain.com/docs/concepts/tool_calling/) generated by a [chat model](https://python.langchain.com/docs/concepts/chat_models/) prior to their execution. This can be accomplished through use of the [interrupt](../../concepts/human_in_the_loop/#interrupt) function at key points in our application.\n",
"\n",
"**Preview**:\n",
"\n",
"We will implement a simple function that reviews tool calls generated from our chat model and call it from inside our application's [entrypoint](../../concepts/functional_api/#entrypoint):\n",
"\n",
"```python\n",
"def review_tool_call(tool_call: ToolCall) -> Union[ToolCall, ToolMessage]:\n",
" \"\"\"Review a tool call, returning a validated version.\"\"\"\n",
" human_review = interrupt(\n",
" {\n",
" \"question\": \"Is this correct?\",\n",
" \"tool_call\": tool_call,\n",
" }\n",
" )\n",
" review_action = human_review[\"action\"]\n",
" review_data = human_review.get(\"data\")\n",
" if review_action == \"continue\":\n",
" return tool_call\n",
" elif review_action == \"update\":\n",
" updated_tool_call = {**tool_call, **{\"args\": review_data}}\n",
" return updated_tool_call\n",
" elif review_action == \"feedback\":\n",
" return ToolMessage(\n",
" content=review_data, name=tool_call[\"name\"], tool_call_id=tool_call[\"id\"]\n",
" )\n",
"```\n",
"\n",
"## Setup\n",
"\n",
"First, let's install the required packages and set our API keys:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for better debugging</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM aps built with LangGraph — read more about how to get started in the <a href=\"https://docs.smith.langchain.com\">docs</a>. \n",
" </p>\n",
" </div>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Define model and tools\n",
"\n",
"Let's first define the tools and model we will use for our example. As in the [ReAct agent guide](../../how-tos/react-agent-from-scratch-functional), we will use a single place-holder tool that gets a description of the weather for a location.\n",
"\n",
"We will use an [OpenAI](https://python.langchain.com/docs/integrations/providers/openai/) chat model for this example, but any model [supporting tool-calling](https://python.langchain.com/docs/integrations/chat/) will suffice."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"from langchain_core.tools import tool\n",
"\n",
"model = ChatOpenAI(model=\"gpt-4o-mini\")\n",
"\n",
"\n",
"@tool\n",
"def get_weather(location: str):\n",
" \"\"\"Call to get the weather from a specific location.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" if any([city in location.lower() for city in [\"sf\", \"san francisco\"]]):\n",
" return \"It's sunny!\"\n",
" elif \"boston\" in location.lower():\n",
" return \"It's rainy!\"\n",
" else:\n",
" return f\"I am not sure what the weather is in {location}\"\n",
"\n",
"\n",
"tools = [get_weather]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Define tasks\n",
"\n",
"Our [tasks](../../concepts/functional_api/#task) are unchanged from the [ReAct agent guide](../../how-tos/react-agent-from-scratch-functional):\n",
"\n",
"1. **Call model**: We want to query our chat model with a list of messages.\n",
"2. **Call tool**: If our model generates tool calls, we want to execute them."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import ToolCall, ToolMessage\n",
"from langgraph.func import entrypoint, task\n",
"\n",
"\n",
"tools_by_name = {tool.name: tool for tool in tools}\n",
"\n",
"\n",
"@task\n",
"def call_model(messages):\n",
" \"\"\"Call model with a sequence of messages.\"\"\"\n",
" response = model.bind_tools(tools).invoke(messages)\n",
" return response\n",
"\n",
"\n",
"@task\n",
"def call_tool(tool_call):\n",
" tool = tools_by_name[tool_call[\"name\"]]\n",
" observation = tool.invoke(tool_call[\"args\"])\n",
" return ToolMessage(content=observation, tool_call_id=tool_call[\"id\"])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Define entrypoint\n",
"\n",
"To review tool calls before execution, we add a `review_tool_call` function that calls [interrupt](../../concepts/human_in_the_loop/#interrupt). When this function is called, execution will be paused until we issue a command to resume it.\n",
"\n",
"Given a tool call, our function will `interrupt` for human review. At that point we can either:\n",
"\n",
"- Accept the tool call;\n",
"- Revise the tool call and continue;\n",
"- Generate a custom tool message (e.g., instructing the model to re-format its tool call).\n",
"\n",
"We will demonstrate these three cases in the [usage examples](#usage) below."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"from typing import Union\n",
"\n",
"\n",
"def review_tool_call(tool_call: ToolCall) -> Union[ToolCall, ToolMessage]:\n",
" \"\"\"Review a tool call, returning a validated version.\"\"\"\n",
" human_review = interrupt(\n",
" {\n",
" \"question\": \"Is this correct?\",\n",
" \"tool_call\": tool_call,\n",
" }\n",
" )\n",
" review_action = human_review[\"action\"]\n",
" review_data = human_review.get(\"data\")\n",
" if review_action == \"continue\":\n",
" return tool_call\n",
" elif review_action == \"update\":\n",
" updated_tool_call = {**tool_call, **{\"args\": review_data}}\n",
" return updated_tool_call\n",
" elif review_action == \"feedback\":\n",
" return ToolMessage(\n",
" content=review_data, name=tool_call[\"name\"], tool_call_id=tool_call[\"id\"]\n",
" )"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can now update our [entrypoint](../../concepts/functional_api/#entrypoint) to review the generated tool calls. If a tool call is accepted or revised, we execute in the same way as before. Otherwise, we just append the `ToolMessage` supplied by the human.\n",
"\n",
"!!! tip\n",
"\n",
" The results of prior tasks — in this case the initial model call — are persisted, so that they are not run again following the `interrupt`."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.types import Command, interrupt\n",
"\n",
"\n",
"checkpointer = MemorySaver()\n",
"\n",
"\n",
"@entrypoint(checkpointer=checkpointer)\n",
"def agent(messages, previous):\n",
" if previous is not None:\n",
" messages = add_messages(previous, messages)\n",
"\n",
" llm_response = call_model(messages).result()\n",
" while True:\n",
" if not llm_response.tool_calls:\n",
" break\n",
"\n",
" # Review tool calls\n",
" tool_results = []\n",
" tool_calls = []\n",
" for i, tool_call in enumerate(llm_response.tool_calls):\n",
" review = review_tool_call(tool_call)\n",
" if isinstance(review, ToolMessage):\n",
" tool_results.append(review)\n",
" else: # is a validated tool call\n",
" tool_calls.append(review)\n",
" if review != tool_call:\n",
" llm_response.tool_calls[i] = review # update message\n",
"\n",
" # Execute remaining tool calls\n",
" tool_result_futures = [call_tool(tool_call) for tool_call in tool_calls]\n",
" remaining_tool_results = [fut.result() for fut in tool_result_futures]\n",
"\n",
" # Append to message list\n",
" messages = add_messages(\n",
" messages,\n",
" [llm_response, *tool_results, *remaining_tool_results],\n",
" )\n",
"\n",
" # Call model again\n",
" llm_response = call_model(messages).result()\n",
"\n",
" # Generate final response\n",
" messages = add_messages(messages, llm_response)\n",
" return entrypoint.final(value=llm_response, save=messages)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Usage\n",
"\n",
"Let's demonstrate some scenarios."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"def _print_step(step: dict) -> None:\n",
" for task_name, result in step.items():\n",
" if task_name == \"agent\":\n",
" continue # just stream from tasks\n",
" print(f\"\\n{task_name}:\")\n",
" if task_name in (\"__interrupt__\", \"review_tool_call\"):\n",
" print(result)\n",
" else:\n",
" result.pretty_print()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Accept a tool call\n",
"\n",
"To accept a tool call, we just indicate in the data we provide in the `Command` that the tool call should pass through."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"config = {\"configurable\": {\"thread_id\": \"1\"}}"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'role': 'user', 'content': \"What's the weather in san francisco?\"}\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_Bh5cSwMqCpCxTjx7AjdrQTPd)\n",
" Call ID: call_Bh5cSwMqCpCxTjx7AjdrQTPd\n",
" Args:\n",
" location: San Francisco\n",
"\n",
"__interrupt__:\n",
"(Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'get_weather', 'args': {'location': 'San Francisco'}, 'id': 'call_Bh5cSwMqCpCxTjx7AjdrQTPd', 'type': 'tool_call'}}, resumable=True, ns=['agent:22fcc9cd-3573-b39b-eea7-272a025903e2'], when='during'),)\n"
]
}
],
"source": [
"user_message = {\"role\": \"user\", \"content\": \"What's the weather in san francisco?\"}\n",
"print(user_message)\n",
"\n",
"for step in agent.stream([user_message], config):\n",
" _print_step(step)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"call_tool:\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"It's sunny!\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in San Francisco is sunny!\n"
]
}
],
"source": [
"# highlight-next-line\n",
"human_input = Command(resume={\"action\": \"continue\"})\n",
"\n",
"for step in agent.stream(human_input, config):\n",
" _print_step(step)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Revise a tool call\n",
"\n",
"To revise a tool call, we can supply updated arguments."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [],
"source": [
"config = {\"configurable\": {\"thread_id\": \"2\"}}"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'role': 'user', 'content': \"What's the weather in san francisco?\"}\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_b9h8e18FqH0IQm3NMoeYKz6N)\n",
" Call ID: call_b9h8e18FqH0IQm3NMoeYKz6N\n",
" Args:\n",
" location: san francisco\n",
"\n",
"__interrupt__:\n",
"(Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'get_weather', 'args': {'location': 'san francisco'}, 'id': 'call_b9h8e18FqH0IQm3NMoeYKz6N', 'type': 'tool_call'}}, resumable=True, ns=['agent:9559a81d-5720-dc19-a457-457bac7bdd83'], when='during'),)\n"
]
}
],
"source": [
"user_message = {\"role\": \"user\", \"content\": \"What's the weather in san francisco?\"}\n",
"print(user_message)\n",
"\n",
"for step in agent.stream([user_message], config):\n",
" _print_step(step)"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"call_tool:\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"It's sunny!\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in San Francisco is sunny!\n"
]
}
],
"source": [
"# highlight-next-line\n",
"human_input = Command(resume={\"action\": \"update\", \"data\": {\"location\": \"SF, CA\"}})\n",
"\n",
"for step in agent.stream(human_input, config):\n",
" _print_step(step)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The LangSmith traces for this run are particularly informative:\n",
"\n",
"- In the trace [before the interrupt](https://smith.langchain.com/public/c8b07579-5cf4-4adb-a849-282163bc9d99/r/b5b128d6-e715-480b-b58d-59e64f724275), we generate a tool call for location `\"San Francisco\"`.\n",
"- In the trace [after resuming](https://smith.langchain.com/public/b28b92e5-a555-482d-aa4d-c675a19f0eb5/r), we see that the tool call in the message has been updated to `\"SF, CA\"`."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Generate a custom ToolMessage\n",
"\n",
"To Generate a custom `ToolMessage`, we supply the content of the message. In this case we will ask the model to reformat its tool call."
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [],
"source": [
"config = {\"configurable\": {\"thread_id\": \"3\"}}"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'role': 'user', 'content': \"What's the weather in san francisco?\"}\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_VqGjKE7uu8HdWs9XuY1kMV18)\n",
" Call ID: call_VqGjKE7uu8HdWs9XuY1kMV18\n",
" Args:\n",
" location: San Francisco\n",
"\n",
"__interrupt__:\n",
"(Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'get_weather', 'args': {'location': 'San Francisco'}, 'id': 'call_VqGjKE7uu8HdWs9XuY1kMV18', 'type': 'tool_call'}}, resumable=True, ns=['agent:4b3b372b-9da3-70be-5c68-3d9317346070'], when='during'),)\n"
]
}
],
"source": [
"user_message = {\"role\": \"user\", \"content\": \"What's the weather in san francisco?\"}\n",
"print(user_message)\n",
"\n",
"for step in agent.stream([user_message], config):\n",
" _print_step(step)"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_xoXkK8Cz0zIpvWs78qnXpvYp)\n",
" Call ID: call_xoXkK8Cz0zIpvWs78qnXpvYp\n",
" Args:\n",
" location: San Francisco, CA\n",
"\n",
"__interrupt__:\n",
"(Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'get_weather', 'args': {'location': 'San Francisco, CA'}, 'id': 'call_xoXkK8Cz0zIpvWs78qnXpvYp', 'type': 'tool_call'}}, resumable=True, ns=['agent:4b3b372b-9da3-70be-5c68-3d9317346070'], when='during'),)\n"
]
}
],
"source": [
"# highlight-next-line\n",
"human_input = Command(\n",
" # highlight-next-line\n",
" resume={\n",
" # highlight-next-line\n",
" \"action\": \"feedback\",\n",
" # highlight-next-line\n",
" \"data\": \"Please format as <City>, <State>.\",\n",
" # highlight-next-line\n",
" },\n",
" # highlight-next-line\n",
")\n",
"\n",
"for step in agent.stream(human_input, config):\n",
" _print_step(step)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Once it is re-formatted, we can accept it:"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"call_tool:\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"It's sunny!\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in San Francisco, CA is sunny!\n"
]
}
],
"source": [
"# highlight-next-line\n",
"human_input = Command(resume={\"action\": \"continue\"})\n",
"\n",
"for step in agent.stream(human_input, config):\n",
" _print_step(step)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
+1 -1
View File
@@ -5,7 +5,7 @@
"id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53",
"metadata": {},
"source": [
"# How to use Pydantic model as state\n",
"# How to use Pydantic model as graph state\n",
"\n",
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Prerequisites</p>\n",
+1 -1
View File
@@ -5,7 +5,7 @@
"id": "176e8dbb-1a0a-49ce-a10e-2417e8ea17a0",
"metadata": {},
"source": [
"# How to add thread-level persistence to subgraphs"
"# How to add thread-level persistence to a subgraph"
]
},
{
+1 -1
View File
@@ -9,7 +9,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to add and use subgraphs\n",
"# How to use subgraphs\n",
"\n",
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Prerequisites</p>\n",
+1 -1
View File
@@ -217,7 +217,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"We'll be using a small chat model from Anthropic in our example. To use chat models with tool calling, we need to first ensure that the model is aware of the available tools. We do this by calling `.bind_tools` method on `ChatAnthropic` moodel"
"We'll be using a small chat model from Anthropic in our example. To use chat models with tool calling, we need to first ensure that the model is aware of the available tools. We do this by calling `.bind_tools` method on `ChatAnthropic` model"
]
},
{
@@ -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",
")"
]
},
@@ -0,0 +1,561 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to wait for user input (Functional API)\n",
"\n",
"!!! info \"Prerequisites\"\n",
" This guide assumes familiarity with the following:\n",
"\n",
" - Implementing [human-in-the-loop](../../concepts/human_in_the_loop) workflows with [interrupt](../../concepts/human_in_the_loop/#interrupt)\n",
" - [How to create a ReAct agent using the Functional API](../../how-tos/react-agent-from-scratch-functional)\n",
"\n",
"**Human-in-the-loop (HIL)** interactions are crucial for [agentic systems](../../concepts/agentic_concepts/#human-in-the-loop). Waiting for human input is a common HIL interaction pattern, allowing the agent to ask the user clarifying questions and await input before proceeding. \n",
"\n",
"We can implement this in LangGraph using the [interrupt()][langgraph.types.interrupt] function. `interrupt` allows us to stop graph execution to collect input from a user and continue execution with collected input.\n",
"\n",
"This guide demonstrates how to implement human-in-the-loop workflows using LangGraph's [Functional API](../../concepts/functional_api). Specifically, we will demonstrate:\n",
"\n",
"1. [A simple usage example](#simple-usage)\n",
"2. [How to use with a ReAct agent](#agent)\n",
"\n",
"## Setup\n",
"\n",
"First, let's install the required packages and set our API keys:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for better debugging</p>\n",
" <p style=\"padding-top: 5px;\">\n",
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM aps built with LangGraph — read more about how to get started in the <a href=\"https://docs.smith.langchain.com\">docs</a>. \n",
" </p>\n",
" </div>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Simple usage\n",
"\n",
"Let's demonstrate a simple usage example. We will create three [tasks](../../concepts/functional_api/#task):\n",
"\n",
"1. Append `\"bar\"`.\n",
"2. Pause for human input. When resuming, append human input.\n",
"3. Append `\"qux\"`."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"from langgraph.func import entrypoint, task\n",
"from langgraph.types import Command, interrupt\n",
"\n",
"\n",
"@task\n",
"def step_1(input_query):\n",
" \"\"\"Append bar.\"\"\"\n",
" return f\"{input_query} bar\"\n",
"\n",
"\n",
"@task\n",
"def human_feedback(input_query):\n",
" \"\"\"Append user input.\"\"\"\n",
" feedback = interrupt(f\"Please provide feedback: {input_query}\")\n",
" return f\"{input_query} {feedback}\"\n",
"\n",
"\n",
"@task\n",
"def step_3(input_query):\n",
" \"\"\"Append qux.\"\"\"\n",
" return f\"{input_query} qux\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can now compose these tasks in a simple [entrypoint](../../concepts/functional_api/#entrypoint):"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"checkpointer = MemorySaver()\n",
"\n",
"\n",
"@entrypoint(checkpointer=checkpointer)\n",
"def graph(input_query):\n",
" result_1 = step_1(input_query).result()\n",
" result_2 = human_feedback(result_1).result()\n",
" result_3 = step_3(result_2).result()\n",
"\n",
" return result_3"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"All we have done to enable human-in-the-loop workflows is called [interrupt()](../../concepts/human_in_the_loop/#interrupt) inside a task.\n",
"\n",
"!!! tip\n",
"\n",
" The results of prior tasks-- in this case `step_1`-- are persisted, so that they are not run again following the `interrupt`.\n",
"\n",
"\n",
"Let's send in a query string:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"config = {\"configurable\": {\"thread_id\": \"1\"}}"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'step_1': 'foo bar'}\n",
"\n",
"\n",
"{'__interrupt__': (Interrupt(value='Please provide feedback: foo bar', resumable=True, ns=['graph:d66b2e35-0ee3-d8d6-1a22-aec9d58f13b9', 'human_feedback:e0cd4ee2-b874-e1d2-8bc4-3f7ddc06bcc2'], when='during'),)}\n",
"\n",
"\n"
]
}
],
"source": [
"for event in graph.stream(\"foo\", config):\n",
" print(event)\n",
" print(\"\\n\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Note that we've paused with an `interrupt` after `step_1`. The interrupt provides instructions to resume the run. To resume, we issue a [Command](../../concepts/human_in_the_loop/#the-command-primitive) containing the data expected by the `human_feedback` task."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'human_feedback': 'foo bar baz'}\n",
"\n",
"\n",
"{'step_3': 'foo bar baz qux'}\n",
"\n",
"\n",
"{'graph': 'foo bar baz qux'}\n",
"\n",
"\n"
]
}
],
"source": [
"# Continue execution\n",
"for event in graph.stream(Command(resume=\"baz\"), config):\n",
" print(event)\n",
" print(\"\\n\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"After resuming, the run proceeds through the remaining step and terminates as expected."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Agent\n",
"\n",
"We will build off of the agent created in the [How to create a ReAct agent using the Functional API](../../how-tos/react-agent-from-scratch-functional) guide.\n",
"\n",
"Here we will extend the agent by allowing it to reach out to a human for assistance when needed.\n",
"\n",
"### Define model and tools\n",
"\n",
"Let's first define the tools and model we will use for our example. As in the [ReAct agent guide](../../how-tos/react-agent-from-scratch-functional), we will use a single place-holder tool that gets a description of the weather for a location.\n",
"\n",
"We will use an [OpenAI](https://python.langchain.com/docs/integrations/providers/openai/) chat model for this example, but any model [supporting tool-calling](https://python.langchain.com/docs/integrations/chat/) will suffice."
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"from langchain_core.tools import tool\n",
"\n",
"model = ChatOpenAI(model=\"gpt-4o-mini\")\n",
"\n",
"\n",
"@tool\n",
"def get_weather(location: str):\n",
" \"\"\"Call to get the weather from a specific location.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" if any([city in location.lower() for city in [\"sf\", \"san francisco\"]]):\n",
" return \"It's sunny!\"\n",
" elif \"boston\" in location.lower():\n",
" return \"It's rainy!\"\n",
" else:\n",
" return f\"I am not sure what the weather is in {location}\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To reach out to a human for assistance, we can simply add a tool that calls [interrupt](../../concepts/human_in_the_loop/#interrupt):"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"from langgraph.types import Command, interrupt\n",
"\n",
"\n",
"@tool\n",
"def human_assistance(query: str) -> str:\n",
" \"\"\"Request assistance from a human.\"\"\"\n",
" human_response = interrupt({\"query\": query})\n",
" return human_response[\"data\"]\n",
"\n",
"\n",
"tools = [get_weather, human_assistance]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Define tasks\n",
"\n",
"Our tasks are otherwise unchanged from the [ReAct agent guide](../../how-tos/react-agent-from-scratch-functional):\n",
"\n",
"1. **Call model**: We want to query our chat model with a list of messages.\n",
"2. **Call tool**: If our model generates tool calls, we want to execute them.\n",
"\n",
"We just have one more tool accessible to the model."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import ToolMessage\n",
"from langgraph.func import entrypoint, task\n",
"\n",
"tools_by_name = {tool.name: tool for tool in tools}\n",
"\n",
"\n",
"@task\n",
"def call_model(messages):\n",
" \"\"\"Call model with a sequence of messages.\"\"\"\n",
" response = model.bind_tools(tools).invoke(messages)\n",
" return response\n",
"\n",
"\n",
"@task\n",
"def call_tool(tool_call):\n",
" tool = tools_by_name[tool_call[\"name\"]]\n",
" observation = tool.invoke(tool_call)\n",
" return ToolMessage(content=observation, tool_call_id=tool_call[\"id\"])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Define entrypoint\n",
"\n",
"Our [entrypoint](../../concepts/functional_api/#entrypoint) is also unchanged from the [ReAct agent guide](../../how-tos/react-agent-from-scratch-functional):"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph.message import add_messages\n",
"\n",
"checkpointer = MemorySaver()\n",
"\n",
"\n",
"@entrypoint(checkpointer=checkpointer)\n",
"def agent(messages, previous):\n",
" if previous is not None:\n",
" messages = add_messages(previous, messages)\n",
"\n",
" llm_response = call_model(messages).result()\n",
" while True:\n",
" if not llm_response.tool_calls:\n",
" break\n",
"\n",
" # Execute tools\n",
" tool_result_futures = [\n",
" call_tool(tool_call) for tool_call in llm_response.tool_calls\n",
" ]\n",
" tool_results = [fut.result() for fut in tool_result_futures]\n",
"\n",
" # Append to message list\n",
" messages = add_messages(messages, [llm_response, *tool_results])\n",
"\n",
" # Call model again\n",
" llm_response = call_model(messages).result()\n",
"\n",
" # Generate final response\n",
" messages = add_messages(messages, llm_response)\n",
" return entrypoint.final(value=llm_response, save=messages)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Usage\n",
"\n",
"Let's invoke our model with a question that requires human assistance. Our question will also require an invocation of the `get_weather` tool:"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
"def _print_step(step: dict) -> None:\n",
" for task_name, result in step.items():\n",
" if task_name == \"agent\":\n",
" continue # just stream from tasks\n",
" print(f\"\\n{task_name}:\")\n",
" if task_name == \"__interrupt__\":\n",
" print(result)\n",
" else:\n",
" result.pretty_print()"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [],
"source": [
"config = {\"configurable\": {\"thread_id\": \"1\"}}"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'role': 'user', 'content': 'Can you reach out for human assistance: what should I feed my cat? Separately, can you check the weather in San Francisco?'}\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" human_assistance (call_joAEBVX7Abfm7TsZ0k95ZkVx)\n",
" Call ID: call_joAEBVX7Abfm7TsZ0k95ZkVx\n",
" Args:\n",
" query: What should I feed my cat?\n",
" get_weather (call_ut7zfHFCcms63BOZLrRHszGH)\n",
" Call ID: call_ut7zfHFCcms63BOZLrRHszGH\n",
" Args:\n",
" location: San Francisco\n",
"\n",
"call_tool:\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"content=\"It's sunny!\" name='get_weather' tool_call_id='call_ut7zfHFCcms63BOZLrRHszGH'\n",
"\n",
"__interrupt__:\n",
"(Interrupt(value={'query': 'What should I feed my cat?'}, resumable=True, ns=['agent:aa676ccc-b038-25e3-9c8a-18e81d4e1372', 'call_tool:059d53d2-3344-13bc-e170-48b632c2dd97'], when='during'),)\n"
]
}
],
"source": [
"user_message = {\n",
" \"role\": \"user\",\n",
" \"content\": (\n",
" \"Can you reach out for human assistance: what should I feed my cat? \"\n",
" \"Separately, can you check the weather in San Francisco?\"\n",
" ),\n",
"}\n",
"print(user_message)\n",
"\n",
"for step in agent.stream([user_message], config):\n",
" _print_step(step)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Note that we generate two tool calls, and although our run is interrupted, we did not block the execution of the `get_weather` tool.\n",
"\n",
"Let's inspect where we're interrupted:"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'__interrupt__': (Interrupt(value={'query': 'What should I feed my cat?'}, resumable=True, ns=['agent:aa676ccc-b038-25e3-9c8a-18e81d4e1372', 'call_tool:059d53d2-3344-13bc-e170-48b632c2dd97'], when='during'),)}\n"
]
}
],
"source": [
"print(step)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can resume execution by issuing a [Command](../../concepts/human_in_the_loop/#the-command-primitive). Note that the data we supply in the `Command` can be customized to your needs based on the implementation of `human_assistance`."
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"call_tool:\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"content='You should feed your cat a fish.' name='human_assistance' tool_call_id='call_joAEBVX7Abfm7TsZ0k95ZkVx'\n",
"\n",
"call_model:\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"For human assistance, you should feed your cat fish. \n",
"\n",
"Regarding the weather in San Francisco, it's sunny!\n"
]
}
],
"source": [
"human_response = \"You should feed your cat a fish.\"\n",
"human_command = Command(resume={\"data\": human_response})\n",
"\n",
"for step in agent.stream(human_command, config):\n",
" _print_step(step)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Above, when we resume we provide the final tool message, allowing the model to generate its response. Check out the LangSmith traces to see a full breakdown of the runs:\n",
"\n",
"1. [Trace from initial query](https://smith.langchain.com/public/c3d8879d-4d01-41be-807e-6d9eed15df99/r)\n",
"2. [Trace after resuming](https://smith.langchain.com/public/97c05ef9-8b4c-428e-8826-3fd417c8c75f/r)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
File diff suppressed because it is too large Load Diff
+1
View File
@@ -9,6 +9,7 @@ New to LangGraph or LLM app development? Read this material to get up and runnin
## Get Started 🚀 {#quick-start}
- [LangGraph Quickstart](introduction.ipynb): Build a chatbot that can use tools and keep track of conversation history. Add human-in-the-loop capabilities and explore how time-travel works.
- [Common Workflows](workflows/index.md): Overview of the most common workflows using LLMs implemented with LangGraph.
- [LangGraph Server Quickstart](langgraph-platform/local-server.md): Launch a LangGraph server locally and interact with it using REST API and LangGraph Studio Web UI.
- [LangGraph Template Quickstart](../concepts/template_applications.md): Start building with LangGraph Platform using a template application.
- [Deploy with LangGraph Cloud Quickstart](../cloud/quick_start.md): Deploy a LangGraph app using LangGraph Cloud.
@@ -250,4 +250,4 @@ Access detailed documentation for development and API usage:
- **[LangGraph Server API Reference](../../cloud/reference/api/api_ref.html)**: Explore the LangGraph Server API documentation.
- **[Python SDK Reference](../../cloud/reference/sdk/python_sdk_ref.md)**: Explore the Python SDK API Reference.
- **[JS/TS SDK Reference](../../cloud/reference/sdk/js_ts_sdk_ref.md)**: Explore the Python SDK API Reference.
- **[JS/TS SDK Reference](../../cloud/reference/sdk/js_ts_sdk_ref.md)**: Explore the JS/TS SDK API Reference.
@@ -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",
@@ -130,37 +130,20 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": null,
"id": "72d233ca-1dbf-4b43-b680-b3bf39e3691f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m System Message \u001b[0m================================\n",
"\n",
"You are a helpful assistant.\n",
"\n",
"=============================\u001b[1m Messages Placeholder \u001b[0m=============================\n",
"\n",
"\u001b[33;1m\u001b[1;3m{messages}\u001b[0m\n"
]
}
],
"outputs": [],
"source": [
"from langchain import hub\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"# Get the prompt to use - you can modify this!\n",
"prompt = hub.pull(\"ih/ih-react-agent-executor\")\n",
"prompt.pretty_print()\n",
"\n",
"# Choose the LLM that will drive the agent\n",
"llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n",
"agent_executor = create_react_agent(llm, tools, state_modifier=prompt)"
"prompt = \"You are a helpful assistant.\"\n",
"agent_executor = create_react_agent(llm, tools, prompt=prompt)"
]
},
{
@@ -546,7 +529,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
"version": "3.12.3"
}
},
"nbformat": 4,
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

File diff suppressed because it is too large Load Diff
+10
View File
@@ -118,6 +118,8 @@ nav:
- how-tos/persistence_postgres.ipynb
- how-tos/persistence_mongodb.ipynb
- how-tos/persistence_redis.ipynb
- how-tos/persistence-functional.ipynb
- how-tos/cross-thread-persistence-functional.ipynb
- Memory:
- Memory: how-tos#memory
- how-tos/memory/manage-conversation-history.ipynb
@@ -132,6 +134,8 @@ nav:
- how-tos/human_in_the_loop/wait-user-input.ipynb
- how-tos/human_in_the_loop/time-travel.ipynb
- how-tos/human_in_the_loop/review-tool-calls.ipynb
- how-tos/wait-user-input-functional.ipynb
- how-tos/review-tool-calls-functional.ipynb
- Streaming:
- Streaming: how-tos#streaming
- how-tos/stream-values.ipynb
@@ -163,6 +167,8 @@ nav:
- how-tos/agent-handoffs.ipynb
- how-tos/multi-agent-network.ipynb
- how-tos/multi-agent-multi-turn-convo.ipynb
- how-tos/multi-agent-network-functional.ipynb
- how-tos/multi-agent-multi-turn-convo-functional.ipynb
- State Management:
- State Management: how-tos#state-management
- how-tos/state-model.ipynb
@@ -183,7 +189,9 @@ nav:
- how-tos/create-react-agent-memory.ipynb
- how-tos/create-react-agent-system-prompt.ipynb
- how-tos/create-react-agent-hitl.ipynb
- how-tos/create-react-agent-structured-output.ipynb
- how-tos/react-agent-from-scratch.ipynb
- how-tos/react-agent-from-scratch-functional.ipynb
- LangGraph Platform:
- LangGraph Platform: how-tos#langgraph-platform
- Application Structure:
@@ -264,6 +272,7 @@ nav:
- concepts/persistence.md
- concepts/memory.md
- concepts/streaming.md
- concepts/functional_api.md
- LangGraph Platform:
- LangGraph Platform: concepts#langgraph-platform
- High Level:
@@ -295,6 +304,7 @@ nav:
- Quick Start:
- Quick Start: tutorials#quick-start
- tutorials/introduction.ipynb
- tutorials/workflows/index.md
- tutorials/langgraph-platform/local-server.md
- cloud/quick_start.md
- Chatbots:
+1 -1
View File
@@ -330,7 +330,7 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
rfile = resolved / "requirements.txt"
pip_reqs.append(
(
rfile.relative_to(config_path.parent),
rfile.relative_to(config_path.parent).as_posix(),
f"{container_path}/requirements.txt",
)
)
+2 -1
View File
@@ -170,13 +170,14 @@ def compose_as_dict(
# Add Postgres service before langgraph-api if it is needed
if include_db:
services["langgraph-postgres"] = {
"image": "postgres:16",
"image": "pgvector/pgvector:pg16",
"ports": ['"5433:5432"'],
"environment": {
"POSTGRES_DB": "postgres",
"POSTGRES_USER": "postgres",
"POSTGRES_PASSWORD": "postgres",
},
"command": ["postgres", "-c", "shared_preload_libraries=vector"],
"volumes": ["langgraph-data:/var/lib/postgresql/data"],
"healthcheck": {
"test": "pg_isready -U postgres",
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
version = "0.1.68"
version = "0.1.69"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
+5 -1
View File
@@ -79,13 +79,17 @@ services:
timeout: 1s
retries: 5
langgraph-postgres:
image: postgres:16
image: pgvector/pgvector:pg16
ports:
- "5433:5432"
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
command:
- postgres
- -c
- shared_preload_libraries=vector
volumes:
- langgraph-data:/var/lib/postgresql/data
healthcheck:
+5 -1
View File
@@ -115,13 +115,17 @@ services:
timeout: 1s
retries: 5
langgraph-postgres:
image: postgres:16
image: pgvector/pgvector:pg16
ports:
- "5433:5432"
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
command:
- postgres
- -c
- shared_preload_libraries=vector
volumes:
- langgraph-data:/var/lib/postgresql/data
healthcheck:
+121 -71
View File
@@ -12,25 +12,48 @@
## Overview
[LangGraph](https://langchain-ai.github.io/langgraph/) is a library for building stateful, multi-actor applications with LLMs, used to create agent and multi-agent workflows. Compared to other LLM frameworks, it offers these core benefits: cycles, controllability, and persistence. LangGraph allows you to define flows that involve cycles, essential for most agentic architectures, differentiating it from DAG-based solutions. As a very low-level framework, it provides fine-grained control over both the flow and state of your application, crucial for creating reliable agents. Additionally, LangGraph includes built-in persistence, enabling advanced human-in-the-loop and memory features.
[LangGraph](https://langchain-ai.github.io/langgraph/) is a library for building
stateful, multi-actor applications with LLMs, used to create agent and multi-agent
workflows. Check out an introductory tutorial [here](https://langchain-ai.github.io/langgraph/tutorials/introduction/).
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform) is infrastructure for deploying LangGraph agents. It is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework. The LangGraph Platform consists of several components that work together to support the development, deployment, debugging, and monitoring of LangGraph applications: [LangGraph Server](https://langchain-ai.github.io/langgraph/concepts/langgraph_server) (APIs), [LangGraph SDKs](https://langchain-ai.github.io/langgraph/concepts/sdk) (clients for the APIs), [LangGraph CLI](https://langchain-ai.github.io/langgraph/concepts/langgraph_cli) (command line tool for building the server), [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio) (UI/debugger),
### Why use LangGraph?
To learn more about LangGraph, check out our first LangChain Academy course, *Introduction to LangGraph*, available for free [here](https://academy.langchain.com/courses/intro-to-langgraph).
LangGraph provides fine-grained control over both the flow and state of your
agent applications. It implements a central
[persistence layer](https://langchain-ai.github.io/langgraph/concepts/persistence/),
enabling features that are common to most agent architectures:
### Key Features
- **Memory**: LangGraph persists arbitrary aspects of your application's state,
supporting memory of conversations and other updates within and across user
interactions;
- **Human-in-the-loop**: Because state is checkpointed, execution can be interrupted
and resumed, allowing for decisions, validation, and corrections at key stages via
human input.
- **Cycles and Branching**: Implement loops and conditionals in your apps.
- **Persistence**: Automatically save state after each step in the graph. Pause and resume the graph execution at any point to support error recovery, human-in-the-loop workflows, time travel and more.
- **Human-in-the-Loop**: Interrupt graph execution to approve or edit next action planned by the agent.
- **Streaming Support**: Stream outputs as they are produced by each node (including token streaming).
- **Integration with LangChain**: LangGraph integrates seamlessly with [LangChain](https://github.com/langchain-ai/langchain/) and [LangSmith](https://docs.smith.langchain.com/) (but does not require them).
Standardizing these components allows individuals and teams to focus on the behavior
of their agent, instead of its supporting infrastructure.
Through [LangGraph Platform](#langgraph-platform), LangGraph also provides tooling for
the development, deployment, debugging, and monitoring of your applications.
LangGraph integrates seamlessly with
[LangChain](https://python.langchain.com/docs/introduction/) and
[LangSmith](https://docs.smith.langchain.com/) (but does not require them).
To learn more about LangGraph, check out our first LangChain Academy
course, *Introduction to LangGraph*, available for free
[here](https://academy.langchain.com/courses/intro-to-langgraph).
### LangGraph Platform
LangGraph Platform is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework.
[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform) is infrastructure for deploying LangGraph agents. It is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework. The LangGraph Platform consists of several components that work together to support the development, deployment, debugging, and monitoring of LangGraph applications: [LangGraph Server](https://langchain-ai.github.io/langgraph/concepts/langgraph_server) (APIs), [LangGraph SDKs](https://langchain-ai.github.io/langgraph/concepts/sdk) (clients for the APIs), [LangGraph CLI](https://langchain-ai.github.io/langgraph/concepts/langgraph_cli) (command line tool for building the server), and [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio) (UI/debugger).
See deployment options [here](https://langchain-ai.github.io/langgraph/concepts/deployment_options/)
(includes a free tier).
Here are some common issues that arise in complex deployments, which LangGraph Platform addresses:
- **Streaming support**: LangGraph Server provides [multiple streaming modes](https://langchain-ai.github.io/langgraph/concepts/streaming) optimized for various application needs
@@ -103,6 +126,20 @@ final_state["messages"][-1].content
```
"Based on the search results, I can tell you that the current weather in San Francisco is:\n\nTemperature: 60 degrees Fahrenheit\nConditions: Foggy\n\nSan Francisco is known for its microclimates and frequent fog, especially during the summer months. The temperature of 60°F (about 15.5°C) is quite typical for the city, which tends to have mild temperatures year-round. The fog, often referred to as "Karl the Fog" by locals, is a characteristic feature of San Francisco\'s weather, particularly in the mornings and evenings.\n\nIs there anything else you\'d like to know about the weather in San Francisco or any other location?"
```
Now when we pass the same <code>"thread_id"</code>, the conversation context is retained via the saved state (i.e. stored list of messages)
```python
final_state = app.invoke(
{"messages": [{"role": "user", "content": "what about ny"}]},
config={"configurable": {"thread_id": 42}}
)
final_state["messages"][-1].content
```
```
"Based on the search results, I can tell you that the current weather in New York City is:\n\nTemperature: 90 degrees Fahrenheit (approximately 32.2 degrees Celsius)\nConditions: Sunny\n\nThis weather is quite different from what we just saw in San Francisco. New York is experiencing much warmer temperatures right now. Here are a few points to note:\n\n1. The temperature of 90°F is quite hot, typical of summer weather in New York City.\n2. The sunny conditions suggest clear skies, which is great for outdoor activities but also means it might feel even hotter due to direct sunlight.\n3. This kind of weather in New York often comes with high humidity, which can make it feel even warmer than the actual temperature suggests.\n\nIt's interesting to see the stark contrast between San Francisco's mild, foggy weather and New York's hot, sunny conditions. This difference illustrates how varied weather can be across different parts of the United States, even on the same day.\n\nIs there anything else you'd like to know about the weather in New York or any other location?"
```
</details>
> [!TIP]
@@ -198,82 +235,95 @@ final_state = app.invoke(
)
final_state["messages"][-1].content
```
<b>Step-by-step Breakdown</b>:
<details>
<summary>Initialize the model and tools.</summary>
<ul>
<li>
We use <code>ChatAnthropic</code> as our LLM. <strong>NOTE:</strong> we need to make sure the model knows that it has these tools available to call. We can do this by converting the LangChain tools into the format for OpenAI tool calling using the <code>.bind_tools()</code> method.
</li>
<li>
We define the tools we want to use - a search tool in our case. It is really easy to create your own tools - see documentation here on how to do that <a href="https://python.langchain.com/docs/how_to/custom_tools/">here</a>.
</li>
</ul>
</details>
Now when we pass the same `"thread_id"`, the conversation context is retained via the saved state (i.e. stored list of messages)
<details>
<summary>Initialize graph with state.</summary>
```python
final_state = app.invoke(
{"messages": [{"role": "user", "content": "what about ny"}]},
config={"configurable": {"thread_id": 42}}
)
final_state["messages"][-1].content
```
<ul>
<li>We initialize graph (<code>StateGraph</code>) by passing state schema (in our case <code>MessagesState</code>)</li>
<li><code>MessagesState</code> is a prebuilt state schema that has one attribute -- a list of LangChain <code>Message</code> objects, as well as logic for merging the updates from each node into the state.</li>
</ul>
</details>
```
"Based on the search results, I can tell you that the current weather in New York City is:\n\nTemperature: 90 degrees Fahrenheit (approximately 32.2 degrees Celsius)\nConditions: Sunny\n\nThis weather is quite different from what we just saw in San Francisco. New York is experiencing much warmer temperatures right now. Here are a few points to note:\n\n1. The temperature of 90°F is quite hot, typical of summer weather in New York City.\n2. The sunny conditions suggest clear skies, which is great for outdoor activities but also means it might feel even hotter due to direct sunlight.\n3. This kind of weather in New York often comes with high humidity, which can make it feel even warmer than the actual temperature suggests.\n\nIt's interesting to see the stark contrast between San Francisco's mild, foggy weather and New York's hot, sunny conditions. This difference illustrates how varied weather can be across different parts of the United States, even on the same day.\n\nIs there anything else you'd like to know about the weather in New York or any other location?"
```
<details>
<summary>Define graph nodes.</summary>
### Step-by-step Breakdown
There are two main nodes we need:
1. <details>
<summary>Initialize the model and tools.</summary>
<ul>
<li>The <code>agent</code> node: responsible for deciding what (if any) actions to take.</li>
<li>The <code>tools</code> node that invokes tools: if the agent decides to take an action, this node will then execute that action.</li>
</ul>
</details>
- we use `ChatAnthropic` as our LLM. **NOTE:** we need make sure the model knows that it has these tools available to call. We can do this by converting the LangChain tools into the format for OpenAI tool calling using the `.bind_tools()` method.
- we define the tools we want to use - a search tool in our case. It is really easy to create your own tools - see documentation here on how to do that [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools).
</details>
<details>
<summary>Define entry point and graph edges.</summary>
2. <details>
<summary>Initialize graph with state.</summary>
First, we need to set the entry point for graph execution - <code>agent</code> node.
- we initialize graph (`StateGraph`) by passing state schema (in our case `MessagesState`)
- `MessagesState` is a prebuilt state schema that has one attribute -- a list of LangChain `Message` objects, as well as logic for merging the updates from each node into the state
</details>
Then we define one normal and one conditional edge. Conditional edge means that the destination depends on the contents of the graph's state (<code>MessagesState</code>). In our case, the destination is not known until the agent (LLM) decides.
3. <details>
<summary>Define graph nodes.</summary>
<ul>
<li>Conditional edge: after the agent is called, we should either:
<ul>
<li>a. Run tools if the agent said to take an action, OR</li>
<li>b. Finish (respond to the user) if the agent did not ask to run tools</li>
</ul>
</li>
<li>Normal edge: after the tools are invoked, the graph should always return to the agent to decide what to do next</li>
</ul>
</details>
There are two main nodes we need:
<details>
<summary>Compile the graph.</summary>
- The `agent` node: responsible for deciding what (if any) actions to take.
- The `tools` node that invokes tools: if the agent decides to take an action, this node will then execute that action.
</details>
<ul>
<li>
When we compile the graph, we turn it into a LangChain
<a href="https://python.langchain.com/docs/concepts/runnables/">Runnable</a>,
which automatically enables calling <code>.invoke()</code>, <code>.stream()</code> and <code>.batch()</code>
with your inputs
</li>
<li>
We can also optionally pass checkpointer object for persisting state between graph runs, and enabling memory,
human-in-the-loop workflows, time travel and more. In our case we use <code>MemorySaver</code> -
a simple in-memory checkpointer
</li>
</ul>
</details>
4. <details>
<summary>Define entry point and graph edges.</summary>
<details>
<summary>Execute the graph.</summary>
First, we need to set the entry point for graph execution - `agent` node.
Then we define one normal and one conditional edge. Conditional edge means that the destination depends on the contents of the graph's state (`MessageState`). In our case, the destination is not known until the agent (LLM) decides.
- Conditional edge: after the agent is called, we should either:
- a. Run tools if the agent said to take an action, OR
- b. Finish (respond to the user) if the agent did not ask to run tools
- Normal edge: after the tools are invoked, the graph should always return to the agent to decide what to do next
</details>
5. <details>
<summary>Compile the graph.</summary>
- When we compile the graph, we turn it into a LangChain [Runnable](https://python.langchain.com/v0.2/docs/concepts/#runnable-interface), which automatically enables calling `.invoke()`, `.stream()` and `.batch()` with your inputs
- We can also optionally pass checkpointer object for persisting state between graph runs, and enabling memory, human-in-the-loop workflows, time travel and more. In our case we use `MemorySaver` - a simple in-memory checkpointer
</details>
6. <details>
<summary>Execute the graph.</summary>
1. LangGraph adds the input message to the internal state, then passes the state to the entrypoint node, `"agent"`.
2. The `"agent"` node executes, invoking the chat model.
3. The chat model returns an `AIMessage`. LangGraph adds this to the state.
4. Graph cycles the following steps until there are no more `tool_calls` on `AIMessage`:
- If `AIMessage` has `tool_calls`, `"tools"` node executes
- The `"agent"` node executes again and returns `AIMessage`
5. Execution progresses to the special `END` value and outputs the final state.
And as a result, we get a list of all our chat messages as output.
</details>
<ol>
<li>LangGraph adds the input message to the internal state, then passes the state to the entrypoint node, <code>"agent"</code>.</li>
<li>The <code>"agent"</code> node executes, invoking the chat model.</li>
<li>The chat model returns an <code>AIMessage</code>. LangGraph adds this to the state.</li>
<li>Graph cycles the following steps until there are no more <code>tool_calls</code> on <code>AIMessage</code>:
<ul>
<li>If <code>AIMessage</code> has <code>tool_calls</code>, <code>"tools"</code> node executes</li>
<li>The <code>"agent"</code> node executes again and returns <code>AIMessage</code></li>
</ul>
</li>
<li>Execution progresses to the special <code>END</code> value and outputs the final state. And as a result, we get a list of all our chat messages as output.</li>
</ol>
</details>
</details>
## Documentation
+2 -1
View File
@@ -23,6 +23,7 @@ END = sys.intern("__end__")
"""The last (maybe virtual) node in graph-style Pregel."""
SELF = sys.intern("__self__")
"""The implicit branch that handles each node's Control values."""
PREVIOUS = sys.intern("__previous__")
# --- Reserved write keys ---
INPUT = sys.intern("__input__")
@@ -78,7 +79,7 @@ CONFIG_KEY_NODE_FINISHED = sys.intern("__pregel_node_finished")
# holds a callback to be called when a node is finished
CONFIG_KEY_SCRATCHPAD = sys.intern("__pregel_scratchpad")
# holds a mutable dict for temporary storage scoped to the current task
CONFIG_KEY_END = sys.intern("__pregel_previous")
CONFIG_KEY_PREVIOUS = sys.intern("__pregel_previous")
# holds the previous return value from a stateful Pregel graph.
# --- Other constants ---
+206 -289
View File
@@ -1,96 +1,64 @@
import asyncio
import concurrent
import concurrent.futures
import functools
import inspect
import types
from collections.abc import Iterator
from dataclasses import dataclass
from typing import (
Any,
Awaitable,
Callable,
Generic,
Optional,
TypeVar,
Union,
get_args,
get_origin,
overload,
)
from langchain_core.runnables.base import Runnable
from langchain_core.runnables.config import RunnableConfig
from langchain_core.runnables.graph import Graph, Node
from typing_extensions import ParamSpec
from langgraph.channels.ephemeral_value import EphemeralValue
from langgraph.channels.last_value import LastValue
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.constants import CONF, END, START, TAG_HIDDEN
from langgraph.constants import END, PREVIOUS, START, TAG_HIDDEN
from langgraph.pregel import Pregel
from langgraph.pregel.call import get_runnable_for_func
from langgraph.pregel.protocol import PregelProtocol
from langgraph.pregel.call import (
P,
SyncAsyncFuture,
T,
call,
get_runnable_for_entrypoint,
)
from langgraph.pregel.read import PregelNode
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
from langgraph.store.base import BaseStore
from langgraph.types import RetryPolicy, StreamMode, StreamWriter
P = ParamSpec("P")
P1 = TypeVar("P1")
T = TypeVar("T")
def call(
func: Callable[P, T],
*args: Any,
retry: Optional[RetryPolicy] = None,
**kwargs: Any,
) -> concurrent.futures.Future[T]:
from langgraph.constants import CONFIG_KEY_CALL
from langgraph.utils.config import get_config
config = get_config()
impl = config[CONF][CONFIG_KEY_CALL]
fut = impl(func, (args, kwargs), retry=retry, callbacks=config["callbacks"])
return fut
from langgraph.types import _DC_KWARGS, RetryPolicy, StreamMode
@overload
def task(
*, retry: Optional[RetryPolicy] = None
) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, asyncio.Future[T]]]: ...
@overload
def task( # type: ignore[overload-cannot-match]
*, retry: Optional[RetryPolicy] = None
) -> Callable[[Callable[P, T]], Callable[P, concurrent.futures.Future[T]]]: ...
*, name: Optional[str] = None, retry: Optional[RetryPolicy] = None
) -> Callable[[Callable[P, T]], Callable[P, SyncAsyncFuture[T]]]: ...
@overload
def task(
__func_or_none__: Callable[P, T],
) -> Callable[P, concurrent.futures.Future[T]]: ...
@overload
def task(
__func_or_none__: Callable[P, Awaitable[T]],
) -> Callable[P, asyncio.Future[T]]: ...
) -> Callable[P, SyncAsyncFuture[T]]: ...
def task(
__func_or_none__: Optional[Union[Callable[P, T], Callable[P, Awaitable[T]]]] = None,
*,
name: Optional[str] = None,
retry: Optional[RetryPolicy] = None,
) -> Union[
Callable[[Callable[P, Awaitable[T]]], Callable[P, asyncio.Future[T]]],
Callable[[Callable[P, T]], Callable[P, concurrent.futures.Future[T]]],
Callable[P, asyncio.Future[T]],
Callable[P, concurrent.futures.Future[T]],
Callable[[Callable[P, T]], Callable[P, SyncAsyncFuture[T]]],
Callable[P, SyncAsyncFuture[T]],
]:
"""Define a LangGraph task using the `task` decorator.
!!! warning "Experimental"
This is an experimental API that is subject to change.
Do not use for production code.
!!! warning "Beta"
The Functional API is currently in beta and is subject to change.
!!! important "Requires python 3.11 or higher for async functions"
The `task` decorator supports both sync and async functions. To use async
@@ -149,22 +117,24 @@ def task(
def decorator(
func: Union[Callable[P, Awaitable[T]], Callable[P, T]],
) -> Callable[P, concurrent.futures.Future[T]]:
if asyncio.iscoroutinefunction(func):
) -> Union[
Callable[P, concurrent.futures.Future[T]], Callable[P, asyncio.Future[T]]
]:
if name is not None:
if hasattr(func, "__func__"):
# handle class methods
# NOTE: we're modifying the instance method to avoid modifying
# the original class method in case it's shared across multiple tasks
instance_method = functools.partial(func.__func__, func.__self__) # type: ignore [union-attr]
instance_method.__name__ = name # type: ignore [attr-defined]
func = instance_method
else:
# handle regular functions / partials / callable classes, etc.
func.__name__ = name
@functools.wraps(func)
async def _tick(__allargs__: tuple) -> T:
return await func(*__allargs__[0], **__allargs__[1])
else:
@functools.wraps(func)
def _tick(__allargs__: tuple) -> T:
return func(*__allargs__[0], **__allargs__[1])
wrapper = functools.partial(call, _tick, retry=retry)
object.__setattr__(wrapper, "_is_pregel_task", True)
return functools.update_wrapper(wrapper, func)
call_func = functools.partial(call, func, retry=retry)
object.__setattr__(call_func, "_is_pregel_task", True)
return functools.update_wrapper(call_func, func)
if __func_or_none__ is not None:
return decorator(__func_or_none__)
@@ -172,35 +142,50 @@ def task(
return decorator
def entrypoint(
*,
checkpointer: Optional[BaseCheckpointSaver] = None,
store: Optional[BaseStore] = None,
config_schema: Optional[type[Any]] = None,
) -> Callable[[types.FunctionType], Pregel]:
R = TypeVar("R")
S = TypeVar("S")
# The decorator was wrapped in a class to support the `final` attribute.
# In this form, the `final` attribute should play nicely with IDE autocompletion,
# and type checking tools.
# In addition, we'll be able to surface this information in the API Reference.
class entrypoint:
"""Define a LangGraph workflow using the `entrypoint` decorator.
!!! warning "Experimental"
This is an experimental API that is subject to change.
Do not use for production code.
!!! warning "Beta"
The Functional API is currently in beta and is subject to change.
The decorated function must accept a single parameter, which serves as the input
### Function signature
The decorated function must accept a **single parameter**, which serves as the input
to the function. This input parameter can be of any type. Use a dictionary
to pass multiple parameters to the function.
to pass **multiple parameters** to the function.
The decorated function also has access to these optional parameters:
### Injectable parameters
- `writer`: A `StreamWriter` instance for writing data to a stream.
- `config`: A configuration object for accessing workflow settings.
- `previous`: The previous return value for the given thread (available only when
a checkpointer is provided).
The decorated function can request access to additional parameters
that will be injected automatically at run time. These parameters include:
The entrypoint decorator can be applied to sync functions, async functions,
generator functions, and async generator functions.
| Parameter | Description |
|------------------|----------------------------------------------------------------------------------------------------|
| **`store`** | An instance of [BaseStore][langgraph.store.base.BaseStore]. Useful for long-term memory. |
| **`writer`** | A [StreamWriter][langgraph.types.StreamWriter] instance for writing custom data to a stream. |
| **`config`** | A configuration object (aka RunnableConfig) that holds run-time configuration values. |
| **`previous`** | The previous return value for the given thread (available only when a checkpointer is provided). |
For generator functions, the `previous` parameter will represent a list of
the values previously yielded by the generator. During a run any values yielded
by the generator, will be written to the `custom` stream.
The entrypoint decorator can be applied to sync functions or async functions.
### State management
The **`previous`** parameter can be used to access the return value of the previous
invocation of the entrypoint on the same thread id. This value is only available
when a checkpointer is provided.
If you want **`previous`** to be different from the return value, you can use the
`entrypoint.final` object to return a value while saving a different value to the
checkpoint.
Args:
checkpointer: Specify a checkpointer to create a workflow that can persist
@@ -210,9 +195,6 @@ def entrypoint(
config_schema: Specifies the schema for the configuration object that will be
passed to the workflow.
Returns:
A decorator that converts a function into a Pregel graph.
Example: Using entrypoint and tasks
```python
import time
@@ -282,119 +264,116 @@ def entrypoint(
```python
from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import entrypoint, task
from langgraph.func import entrypoint
@entrypoint(checkpointer=MemorySaver())
def my_workflow(input_data: str, previous: Optional[str] = None) -> str:
return "world"
# highlight-next-line
config = {
"configurable": {
"thread_id":
"thread_id": "some_thread"
}
}
my_workflow.invoke("hello")
```
Example: Using entrypoint.final to save a value
The `entrypoint.final` object allows you to return a value while saving
a different value to the checkpoint. This value will be accessible
in the next invocation of the entrypoint via the `previous` parameter, as
long as the same thread id is used.
```python
from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import entrypoint
@entrypoint(checkpointer=MemorySaver())
def my_workflow(number: int, *, previous: Any = None) -> entrypoint.final[int, int]:
previous = previous or 0
# This will return the previous value to the caller, saving
# 2 * number to the checkpoint, which will be used in the next invocation
# for the `previous` parameter.
return entrypoint.final(value=previous, save=2 * number)
config = {
"configurable": {
"thread_id": "some_thread"
}
}
my_workflow.invoke(3, config) # 0 (previous was None)
my_workflow.invoke(1, config) # 6 (previous was 3 * 2 from the previous invocation)
```
"""
def _imp(func: types.FunctionType) -> Pregel:
def __init__(
self,
checkpointer: Optional[BaseCheckpointSaver] = None,
store: Optional[BaseStore] = None,
config_schema: Optional[type[Any]] = None,
) -> None:
"""Initialize the entrypoint decorator."""
self.checkpointer = checkpointer
self.store = store
self.config_schema = config_schema
@dataclass(**_DC_KWARGS)
class final(Generic[R, S]):
"""A primitive that can be returned from an entrypoint.
This primitive allows to save a value to the checkpointer distinct from the
return value from the entrypoint.
Example: Decoupling the return value and the save value
```python
from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import entrypoint
@entrypoint(checkpointer=MemorySaver())
def my_workflow(number: int, *, previous: Any = None) -> entrypoint.final[int, int]:
previous = previous or 0
# This will return the previous value to the caller, saving
# 2 * number to the checkpoint, which will be used in the next invocation
# for the `previous` parameter.
return entrypoint.final(value=previous, save=2 * number)
config = {
"configurable": {
"thread_id": "1"
}
}
my_workflow.invoke(3, config) # 0 (previous was None)
my_workflow.invoke(1, config) # 6 (previous was 3 * 2 from the previous invocation)
```
"""
value: R
"""Value to return. A value will always be returned even if it is None."""
save: S
"""The value for the state for the next checkpoint.
A value will always be saved even if it is None.
"""
def __call__(self, func: Callable[..., Any]) -> Pregel:
"""Convert a function into a Pregel graph.
Args:
func: The function to convert. Support both sync and async functions, as well
as generator and async generator functions.
func: The function to convert. Support both sync and async functions.
Returns:
A Pregel graph.
"""
# wrap generators in a function that writes to StreamWriter
if inspect.isgeneratorfunction(func):
original_sig = inspect.signature(func)
# Check if original signature has a writer argument with a matching type.
# If not, we'll inject it into the decorator, but not pass it
# to the wrapped function.
if "writer" in original_sig.parameters:
if inspect.isgeneratorfunction(func) or inspect.isasyncgenfunction(func):
raise NotImplementedError(
"Generators are not supported in the Functional API."
)
@functools.wraps(func)
def gen_wrapper(*args: Any, writer: StreamWriter, **kwargs: Any) -> Any:
chunks = []
for chunk in func(*args, writer=writer, **kwargs):
writer(chunk)
chunks.append(chunk)
return chunks
else:
@functools.wraps(func)
def gen_wrapper(*args: Any, writer: StreamWriter, **kwargs: Any) -> Any:
chunks = []
# Do not pass the writer argument to the wrapped function
# as it does not have a matching parameter
for chunk in func(*args, **kwargs):
writer(chunk)
chunks.append(chunk)
return chunks
# Create a new parameter for the writer argument
extra_param = inspect.Parameter(
"writer",
inspect.Parameter.KEYWORD_ONLY,
# The extra argument is a keyword-only argument
default=lambda _: None,
)
# Update the function's signature to include the extra argument
new_params = list(original_sig.parameters.values()) + [extra_param]
new_sig = original_sig.replace(parameters=new_params)
# Update the signature of the wrapper function
gen_wrapper.__signature__ = new_sig # type: ignore
bound = get_runnable_for_func(gen_wrapper)
stream_mode: StreamMode = "custom"
elif inspect.isasyncgenfunction(func):
original_sig = inspect.signature(func)
# Check if original signature has a writer argument with a matching type.
# If not, we'll inject it into the decorator, but not pass it
# to the wrapped function.
if "writer" in original_sig.parameters:
@functools.wraps(func)
async def agen_wrapper(
*args: Any, writer: StreamWriter, **kwargs: Any
) -> Any:
chunks = []
async for chunk in func(*args, writer=writer, **kwargs):
writer(chunk)
chunks.append(chunk)
return chunks
else:
@functools.wraps(func)
async def agen_wrapper(
*args: Any, writer: StreamWriter, **kwargs: Any
) -> Any:
chunks = []
async for chunk in func(*args, **kwargs):
writer(chunk)
chunks.append(chunk)
return chunks
# Create a new parameter for the writer argument
extra_param = inspect.Parameter(
"writer",
inspect.Parameter.KEYWORD_ONLY,
# The extra argument is a keyword-only argument
default=lambda _: None,
)
# Update the function's signature to include the extra argument
new_params = list(original_sig.parameters.values()) + [extra_param]
new_sig = original_sig.replace(parameters=new_params)
# Update the signature of the wrapper function
agen_wrapper.__signature__ = new_sig # type: ignore
bound = get_runnable_for_func(agen_wrapper)
stream_mode = "custom"
else:
bound = get_runnable_for_func(func)
stream_mode = "updates"
bound = get_runnable_for_entrypoint(func)
stream_mode: StreamMode = "updates"
# get input and output types
sig = inspect.signature(func)
@@ -407,127 +386,65 @@ def entrypoint(
is not inspect.Signature.empty
else Any
)
output_type = (
sig.return_annotation
if sig.return_annotation is not inspect.Signature.empty
else Any
)
return EntrypointPregel(
def _pluck_return_value(value: Any) -> Any:
"""Extract the return_ value the entrypoint.final object or passthrough."""
return value.value if isinstance(value, entrypoint.final) else value
def _pluck_save_value(value: Any) -> Any:
"""Get save value from the entrypoint.final object or passthrough."""
return value.save if isinstance(value, entrypoint.final) else value
output_type, save_type = Any, Any
if sig.return_annotation is not inspect.Signature.empty:
# User does not parameterize entrypoint.final properly
if (
sig.return_annotation is entrypoint.final
): # Un-parameterized entrypoint.final
output_type = save_type = Any
else:
origin = get_origin(sig.return_annotation)
if origin is entrypoint.final:
type_annotations = get_args(sig.return_annotation)
if len(type_annotations) != 2:
raise TypeError(
"Please an annotation for both the return_ and "
"the save values."
"For example, `-> entrypoint.final[int, str]` would assign a "
"return_ a type of `int` and save the type `str`."
)
output_type, save_type = get_args(sig.return_annotation)
else:
output_type = save_type = sig.return_annotation
return Pregel(
nodes={
func.__name__: PregelNode(
bound=bound,
triggers=[START],
channels=[START],
writers=[ChannelWrite([ChannelWriteEntry(END)], tags=[TAG_HIDDEN])],
writers=[
ChannelWrite(
[
ChannelWriteEntry(END, mapper=_pluck_return_value),
ChannelWriteEntry(PREVIOUS, mapper=_pluck_save_value),
],
tags=[TAG_HIDDEN],
)
],
)
},
channels={
START: EphemeralValue(input_type),
END: LastValue(output_type, END),
PREVIOUS: LastValue(save_type, PREVIOUS),
},
input_channels=START,
output_channels=END,
stream_channels=END,
stream_mode=stream_mode,
stream_eager=True,
checkpointer=checkpointer,
store=store,
config_type=config_schema,
checkpointer=self.checkpointer,
store=self.store,
config_type=self.config_schema,
)
return _imp
class EntrypointPregel(Pregel):
def get_graph(
self,
config: Optional[RunnableConfig] = None,
*,
xray: Union[int, bool] = False,
) -> Graph:
name, entrypoint = next(iter(self.nodes.items()))
graph = Graph()
node = Node(f"__{name}", name, entrypoint.bound, None)
graph.nodes[node.id] = node
candidates: list[tuple[Node, Union[Callable, PregelProtocol]]] = [
*_find_children(entrypoint.bound, node)
]
seen: set[Union[Callable, PregelProtocol]] = set()
for parent, child in candidates:
if child in seen:
continue
else:
seen.add(child)
if callable(child):
node = Node(f"__{child.__name__}", child.__name__, child, None) # type: ignore[arg-type]
graph.nodes[node.id] = node
graph.add_edge(parent, node, conditional=True)
graph.add_edge(node, parent)
candidates.extend(_find_children(child, node))
elif isinstance(child, Runnable):
if xray > 0:
graph = child.get_graph(config, xray=xray - 1 if xray else 0)
graph.trim_first_node()
graph.trim_last_node()
s, e = graph.extend(graph, prefix=child.name or "")
if s is None:
raise ValueError(
f"Could not extend subgraph '{child.name}' due to missing entrypoint"
)
else:
graph.add_edge(parent, s, conditional=True)
if e is not None:
graph.add_edge(e, parent)
else:
node = graph.add_node(child, child.name)
graph.add_edge(parent, node, conditional=True)
graph.add_edge(node, parent)
return graph
def _find_children(
candidate: Union[Callable, Runnable], parent: Node
) -> Iterator[tuple[Node, Union[Callable, PregelProtocol]]]:
from langchain_core.runnables.utils import get_function_nonlocals
from langgraph.utils.runnable import (
RunnableCallable,
RunnableLambda,
RunnableSeq,
RunnableSequence,
)
candidates: list[Union[Callable, Runnable]] = []
if callable(candidate) and getattr(candidate, "_is_pregel_task", False) is True:
candidates.extend(
nl.__self__ if hasattr(nl, "__self__") else nl
for nl in get_function_nonlocals(
candidate.__wrapped__
if hasattr(candidate, "__wrapped__") and callable(candidate.__wrapped__)
else candidate
)
)
else:
candidates.append(candidate)
for c in candidates:
if callable(c) and getattr(c, "_is_pregel_task", False) is True:
yield (parent, c)
elif isinstance(c, PregelProtocol):
yield (parent, c)
elif isinstance(c, RunnableSequence) or isinstance(c, RunnableSeq):
candidates.extend(c.steps)
elif isinstance(c, RunnableLambda):
candidates.extend(c.deps)
elif isinstance(c, RunnableCallable):
if c.func is not None:
candidates.extend(
nl.__self__ if hasattr(nl, "__self__") else nl
for nl in get_function_nonlocals(c.func)
)
elif c.afunc is not None:
candidates.extend(
nl.__self__ if hasattr(nl, "__self__") else nl
for nl in get_function_nonlocals(c.afunc)
)
+21 -8
View File
@@ -379,14 +379,27 @@ class StateGraph(Graph):
if input_hint := hints.get(first_parameter_name):
if isinstance(input_hint, type) and get_type_hints(input_hint):
input = input_hint
if (
(rtn := hints.get("return"))
and get_origin(rtn) is Command
and (rargs := get_args(rtn))
and get_origin(rargs[0]) is Literal
and (vals := get_args(rargs[0]))
):
ends = vals
if rtn := hints.get("return"):
# Handle Union types
rtn_origin = get_origin(rtn)
if rtn_origin is Union:
rtn_args = get_args(rtn)
# Look for Command in the union
for arg in rtn_args:
arg_origin = get_origin(arg)
if arg_origin is Command:
rtn = arg
rtn_origin = arg_origin
break
# Check if it's a Command type
if (
rtn_origin is Command
and (rargs := get_args(rtn))
and get_origin(rargs[0]) is Literal
and (vals := get_args(rargs[0]))
):
ends = vals
except (TypeError, StopIteration):
pass
if input is not None:
@@ -1,4 +1,7 @@
import functools
import inspect
from typing import (
Any,
Callable,
Literal,
Optional,
@@ -20,7 +23,6 @@ from langchain_core.tools import BaseTool
from pydantic import BaseModel
from typing_extensions import Annotated, TypedDict
from langgraph._api.deprecation import deprecated_parameter
from langgraph.errors import ErrorCode, create_error_message
from langgraph.graph import END, StateGraph
from langgraph.graph.graph import CompiledGraph
@@ -34,6 +36,7 @@ from langgraph.utils.runnable import RunnableCallable
StructuredResponse = Union[dict, BaseModel]
StructuredResponseSchema = Union[dict, type[BaseModel]]
F = TypeVar("F", bound=Callable[..., Any])
# We create the AgentState that we will pass around
@@ -55,7 +58,7 @@ class AgentState(TypedDict):
StateSchema = TypeVar("StateSchema", bound=AgentState)
StateSchemaType = Type[StateSchema]
STATE_MODIFIER_RUNNABLE_NAME = "StateModifier"
PROMPT_RUNNABLE_NAME = "Prompt"
MessagesModifier = Union[
SystemMessage,
@@ -64,7 +67,7 @@ MessagesModifier = Union[
Runnable[Sequence[BaseMessage], Sequence[BaseMessage]],
]
StateModifier = Union[
Prompt = Union[
SystemMessage,
str,
Callable[[StateSchema], Sequence[BaseMessage]],
@@ -72,75 +75,84 @@ StateModifier = Union[
]
def _get_state_modifier_runnable(
state_modifier: Optional[StateModifier], store: Optional[BaseStore] = None
) -> Runnable:
state_modifier_runnable: Runnable
if state_modifier is None:
state_modifier_runnable = RunnableCallable(
lambda state: state["messages"], name=STATE_MODIFIER_RUNNABLE_NAME
def _get_prompt_runnable(prompt: Optional[Prompt]) -> Runnable:
prompt_runnable: Runnable
if prompt is None:
prompt_runnable = RunnableCallable(
lambda state: state["messages"], name=PROMPT_RUNNABLE_NAME
)
elif isinstance(state_modifier, str):
_system_message: BaseMessage = SystemMessage(content=state_modifier)
state_modifier_runnable = RunnableCallable(
elif isinstance(prompt, str):
_system_message: BaseMessage = SystemMessage(content=prompt)
prompt_runnable = RunnableCallable(
lambda state: [_system_message] + state["messages"],
name=STATE_MODIFIER_RUNNABLE_NAME,
name=PROMPT_RUNNABLE_NAME,
)
elif isinstance(state_modifier, SystemMessage):
state_modifier_runnable = RunnableCallable(
lambda state: [state_modifier] + state["messages"],
name=STATE_MODIFIER_RUNNABLE_NAME,
elif isinstance(prompt, SystemMessage):
prompt_runnable = RunnableCallable(
lambda state: [prompt] + state["messages"],
name=PROMPT_RUNNABLE_NAME,
)
elif callable(state_modifier):
state_modifier_runnable = RunnableCallable(
state_modifier,
name=STATE_MODIFIER_RUNNABLE_NAME,
elif inspect.iscoroutinefunction(prompt):
prompt_runnable = RunnableCallable(
None,
prompt,
name=PROMPT_RUNNABLE_NAME,
)
elif isinstance(state_modifier, Runnable):
state_modifier_runnable = state_modifier
elif callable(prompt):
prompt_runnable = RunnableCallable(
prompt,
name=PROMPT_RUNNABLE_NAME,
)
elif isinstance(prompt, Runnable):
prompt_runnable = prompt
else:
raise ValueError(
f"Got unexpected type for `state_modifier`: {type(state_modifier)}"
)
raise ValueError(f"Got unexpected type for `prompt`: {type(prompt)}")
return state_modifier_runnable
return prompt_runnable
def _convert_messages_modifier_to_state_modifier(
def _convert_messages_modifier_to_prompt(
messages_modifier: MessagesModifier,
) -> StateModifier:
state_modifier: StateModifier
) -> Prompt:
prompt: Prompt
if isinstance(messages_modifier, (str, SystemMessage)):
return messages_modifier
elif callable(messages_modifier):
def state_modifier(state: AgentState) -> Sequence[BaseMessage]:
def prompt(state: AgentState) -> Sequence[BaseMessage]:
return messages_modifier(state["messages"])
return state_modifier
return prompt
elif isinstance(messages_modifier, Runnable):
state_modifier = (lambda state: state["messages"]) | messages_modifier
return state_modifier
prompt = (lambda state: state["messages"]) | messages_modifier
return prompt
raise ValueError(
f"Got unexpected type for `messages_modifier`: {type(messages_modifier)}"
)
def _get_model_preprocessing_runnable(
state_modifier: Optional[StateModifier],
messages_modifier: Optional[MessagesModifier],
store: Optional[BaseStore],
) -> Runnable:
# Add the state or message modifier, if exists
if state_modifier is not None and messages_modifier is not None:
raise ValueError(
"Expected value for either state_modifier or messages_modifier, got values for both"
)
def _convert_modifier_to_prompt(func: F) -> F:
"""Decorator that converts state_modifier/messages_modifier kwargs to prompt kwarg."""
if state_modifier is None and messages_modifier is not None:
state_modifier = _convert_messages_modifier_to_state_modifier(messages_modifier)
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
prompt = kwargs.get("prompt")
state_modifier = kwargs.pop("state_modifier", None)
messages_modifier = kwargs.pop("messages_modifier", None)
if sum(p is not None for p in (prompt, state_modifier, messages_modifier)) > 1:
raise ValueError(
"Expected only one of prompt, state_modifier, or messages_modifier, got multiple values"
)
return _get_state_modifier_runnable(state_modifier, store)
if state_modifier is not None:
prompt = state_modifier
elif messages_modifier is not None:
prompt = _convert_messages_modifier_to_prompt(messages_modifier)
kwargs["prompt"] = prompt
return func(*args, **kwargs)
return cast(F, wrapper)
def _should_bind_tools(model: LanguageModelLike, tools: Sequence[BaseTool]) -> bool:
@@ -221,14 +233,13 @@ def _validate_chat_history(
raise ValueError(error_message)
@deprecated_parameter("messages_modifier", "0.1.9", "state_modifier", removal="0.3.0")
@_convert_modifier_to_prompt
def create_react_agent(
model: Union[str, LanguageModelLike],
tools: Union[ToolExecutor, Sequence[BaseTool], ToolNode],
*,
state_schema: Optional[StateSchemaType] = None,
messages_modifier: Optional[MessagesModifier] = None,
state_modifier: Optional[StateModifier] = None,
prompt: Optional[Prompt] = None,
response_format: Optional[
Union[StructuredResponseSchema, tuple[str, StructuredResponseSchema]]
] = None,
@@ -247,26 +258,15 @@ def create_react_agent(
state_schema: An optional state schema that defines graph state.
Must have `messages` and `is_last_step` keys.
Defaults to `AgentState` that defines those two keys.
messages_modifier: An optional
messages modifier. This applies to messages BEFORE they are passed into the LLM.
prompt: An optional prompt for the LLM. Can take a few different forms:
Can take a few different forms:
- SystemMessage: this is added to the beginning of the list of messages.
- str: This is converted to a SystemMessage and added to the beginning of the list of messages.
- Callable: This function should take in a list of messages and the output is then passed to the language model.
- Runnable: This runnable should take in a list of messages and the output is then passed to the language model.
!!! Warning
`messages_modifier` parameter is deprecated as of version 0.1.9 and will be removed in 0.2.0
state_modifier: An optional
state modifier. This takes full graph state BEFORE the LLM is called and prepares the input to LLM.
Can take a few different forms:
- SystemMessage: this is added to the beginning of the list of messages in state["messages"].
- str: This is converted to a SystemMessage and added to the beginning of the list of messages in state["messages"].
- SystemMessage: this is added to the beginning of the list of messages in state["messages"].
- Callable: This function should take in full graph state and the output is then passed to the language model.
- Runnable: This runnable should take in full graph state and the output is then passed to the language model.
!!! Note
Prior to `v0.2.68`, the prompt was set using `state_modifier` / `messages_modifier` parameters.
response_format: An optional schema for the final agent output.
If provided, output will be formatted to match the given schema and returned in the 'structured_response' state key.
@@ -382,7 +382,7 @@ def create_react_agent(
```pycon
>>> system_prompt = "You are a helpful bot named Fred."
>>> graph = create_react_agent(model, tools, state_modifier=system_prompt)
>>> graph = create_react_agent(model, tools, prompt=system_prompt)
>>> inputs = {"messages": [("user", "What's your name? And what's the weather in SF?")]}
>>> for s in graph.stream(inputs, stream_mode="values"):
... message = s["messages"][-1]
@@ -414,11 +414,8 @@ def create_react_agent(
... ("placeholder", "{messages}"),
... ("user", "Remember, always be polite!"),
... ])
>>> def format_for_model(state: AgentState):
... # You can do more complex modifications here
... return prompt.invoke({"messages": state["messages"]})
>>>
>>> graph = create_react_agent(model, tools, state_modifier=format_for_model)
>>> graph = create_react_agent(model, tools, prompt=prompt)
>>> inputs = {"messages": [("user", "What's your name? And what's the weather in SF?")]}
>>> for s in graph.stream(inputs, stream_mode="values"):
... message = s["messages"][-1]
@@ -446,7 +443,7 @@ def create_react_agent(
... messages: Annotated[list[BaseMessage], add_messages]
... is_last_step: IsLastStep
>>>
>>> graph = create_react_agent(model, tools, state_schema=CustomState, state_modifier=prompt)
>>> graph = create_react_agent(model, tools, state_schema=CustomState, prompt=prompt)
>>> inputs = {"messages": [("user", "What's today's date? And what's the weather in SF?")], "today": "July 16, 2004"}
>>> for s in graph.stream(inputs, stream_mode="values"):
... message = s["messages"][-1]
@@ -532,7 +529,7 @@ def create_react_agent(
>>> from langgraph.checkpoint.memory import MemorySaver
>>> from langgraph.store.memory import InMemoryStore
>>> store = InMemoryStore()
>>> graph = create_react_agent(model, [save_memory], state_modifier=prepare_model_inputs, store=store, checkpointer=MemorySaver())
>>> graph = create_react_agent(model, [save_memory], prompt=prepare_model_inputs, store=store, checkpointer=MemorySaver())
>>> config = {"configurable": {"thread_id": "thread-1", "user_id": "1"}}
>>> inputs = {"messages": [("user", "Hey I'm Will, how's it going?")]}
@@ -612,11 +609,7 @@ def create_react_agent(
if _should_bind_tools(model, tool_classes) and tool_calling_enabled:
model = cast(BaseChatModel, model).bind_tools(tool_classes)
# we're passing store here for validation
preprocessor = _get_model_preprocessing_runnable(
state_modifier, messages_modifier, store
)
model_runnable = preprocessor | model
model_runnable = _get_prompt_runnable(prompt) | model
# If any of the tools are configured to return_directly after running,
# our graph needs to check if these were called
+26 -23
View File
@@ -1,3 +1,5 @@
import functools
import itertools
import sys
from collections import defaultdict, deque
from functools import partial
@@ -37,7 +39,7 @@ from langgraph.constants import (
CONFIG_KEY_CHECKPOINT_MAP,
CONFIG_KEY_CHECKPOINT_NS,
CONFIG_KEY_CHECKPOINTER,
CONFIG_KEY_END,
CONFIG_KEY_PREVIOUS,
CONFIG_KEY_READ,
CONFIG_KEY_SCRATCHPAD,
CONFIG_KEY_SEND,
@@ -46,11 +48,11 @@ from langgraph.constants import (
EMPTY_SEQ,
ERROR,
INTERRUPT,
MISSING,
NO_WRITES,
NS_END,
NS_SEP,
NULL_TASK_ID,
PREVIOUS,
PULL,
PUSH,
RESERVED,
@@ -62,7 +64,7 @@ from langgraph.constants import (
)
from langgraph.errors import EmptyChannelError, InvalidUpdateError
from langgraph.managed.base import ManagedValueMapping
from langgraph.pregel.call import get_runnable_for_func
from langgraph.pregel.call import get_runnable_for_task
from langgraph.pregel.io import read_channel, read_channels
from langgraph.pregel.log import logger
from langgraph.pregel.manager import ChannelsManager
@@ -324,7 +326,7 @@ def apply_writes(
@overload
def prepare_next_tasks(
checkpoint: Checkpoint,
pending_writes: Sequence[PendingWrite],
pending_writes: list[PendingWrite],
processes: Mapping[str, PregelNode],
channels: Mapping[str, BaseChannel],
managed: ManagedValueMapping,
@@ -341,7 +343,7 @@ def prepare_next_tasks(
@overload
def prepare_next_tasks(
checkpoint: Checkpoint,
pending_writes: Sequence[PendingWrite],
pending_writes: list[PendingWrite],
processes: Mapping[str, PregelNode],
channels: Mapping[str, BaseChannel],
managed: ManagedValueMapping,
@@ -357,7 +359,7 @@ def prepare_next_tasks(
def prepare_next_tasks(
checkpoint: Checkpoint,
pending_writes: Sequence[PendingWrite],
pending_writes: list[PendingWrite],
processes: Mapping[str, PregelNode],
channels: Mapping[str, BaseChannel],
managed: ManagedValueMapping,
@@ -418,7 +420,7 @@ def prepare_single_task(
task_id_checksum: Optional[str],
*,
checkpoint: Checkpoint,
pending_writes: Sequence[PendingWrite],
pending_writes: list[PendingWrite],
processes: Mapping[str, PregelNode],
channels: Mapping[str, BaseChannel],
managed: ManagedValueMapping,
@@ -439,7 +441,7 @@ def prepare_single_task(
# (PUSH, parent task path, idx of PUSH write, id of parent task, Call)
task_path_t = cast(tuple[str, tuple, int, str, Call], task_path)
call = task_path_t[-1]
proc_ = get_runnable_for_func(call.func)
proc_ = get_runnable_for_task(call.func)
name = proc_.name
if name is None:
raise ValueError("`call` functions must have a `__name__` attribute")
@@ -508,9 +510,6 @@ def prepare_single_task(
pending_writes,
task_id,
),
CONFIG_KEY_END: checkpoint["channel_values"].get(
"__end__", None
),
},
),
triggers,
@@ -620,8 +619,8 @@ def prepare_single_task(
pending_writes,
task_id,
),
CONFIG_KEY_END: checkpoint["channel_values"].get(
"__end__", None
CONFIG_KEY_PREVIOUS: checkpoint["channel_values"].get(
PREVIOUS, None
),
},
),
@@ -744,8 +743,8 @@ def prepare_single_task(
pending_writes,
task_id,
),
CONFIG_KEY_END: checkpoint["channel_values"].get(
"__end__", None
CONFIG_KEY_PREVIOUS: checkpoint["channel_values"].get(
PREVIOUS, None
),
},
),
@@ -761,23 +760,27 @@ def prepare_single_task(
def _scratchpad(
pending_writes: Sequence[PendingWrite],
pending_writes: list[PendingWrite],
task_id: str,
) -> PregelScratchpad:
null_resume_write = next(
(w for w in pending_writes if w[0] == NULL_TASK_ID and w[1] == RESUME), None
)
# using itertools.count as an atomic counter (+= 1 is not thread-safe)
return PregelScratchpad(
# call
call_counter=0,
call_counter=itertools.count(0).__next__,
# interrupt
interrupt_counter=-1,
interrupt_counter=itertools.count(0).__next__,
resume=next(
(w[2] for w in pending_writes if w[0] == task_id and w[1] == RESUME), []
),
null_resume=next(
(w[2] for w in pending_writes if w[0] == NULL_TASK_ID and w[1] == RESUME),
MISSING,
),
null_resume=null_resume_write[2] if null_resume_write is not None else None,
_consume_null_resume=functools.partial(pending_writes.remove, null_resume_write)
if null_resume_write is not None
else lambda: None,
# subgraph
subgraph_counter=0,
subgraph_counter=itertools.count(0).__next__,
)
+120 -11
View File
@@ -1,12 +1,25 @@
"""Utility to convert a user provided function into a Runnable with a ChannelWrite."""
import concurrent.futures
import functools
import inspect
import sys
import types
from typing import Any, Callable, Optional
from typing import Any, Callable, Generator, Generic, Optional, TypeVar, cast
from langgraph.constants import RETURN
from langchain_core.runnables import Runnable
from typing_extensions import ParamSpec
from langgraph.constants import CONF, CONFIG_KEY_CALL, RETURN, TAG_HIDDEN
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
from langgraph.utils.runnable import RunnableSeq, coerce_to_runnable
from langgraph.types import RetryPolicy
from langgraph.utils.config import get_config
from langgraph.utils.runnable import (
RunnableCallable,
RunnableSeq,
is_async_callable,
run_in_executor,
)
##
# Utilities borrowed from cloudpickle.
@@ -107,18 +120,114 @@ def _lookup_module_and_qualname(
return module, name
def get_runnable_for_func(func: Callable[..., Any]) -> RunnableSeq:
if func in CACHE:
return CACHE[func]
def _explode_args_trace_inputs(
sig: inspect.Signature, input: tuple[tuple[Any, ...], dict[str, Any]]
) -> dict[str, Any]:
args, kwargs = input
bound = sig.bind_partial(*args, **kwargs)
bound.apply_defaults()
arguments = dict(bound.arguments)
arguments.pop("self", None)
arguments.pop("cls", None)
for param_name, param in sig.parameters.items():
if param.kind == inspect.Parameter.VAR_KEYWORD:
# Update with the **kwargs, and remove the original entry
# This is to help flatten out keyword arguments
if param_name in arguments:
arguments.update(arguments.pop(param_name))
return arguments
def get_runnable_for_entrypoint(func: Callable[..., Any]) -> RunnableSeq:
key = (func, False)
if key in CACHE:
return CACHE[key]
else:
if is_async_callable(func):
run = RunnableCallable(
None, func, name=func.__name__, trace=False, recurse=False
)
else:
afunc = functools.update_wrapper(
functools.partial(run_in_executor, None, func), func
)
run = RunnableCallable(
func,
afunc,
name=func.__name__,
trace=False,
recurse=False,
)
if not _lookup_module_and_qualname(func):
return run
return CACHE.setdefault(key, run)
def get_runnable_for_task(func: Callable[..., Any]) -> RunnableSeq:
key = (func, True)
if key in CACHE:
return CACHE[key]
else:
if hasattr(func, "__name__"):
name = func.__name__
elif hasattr(func, "func"):
name = func.func.__name__
elif hasattr(func, "__class__"):
name = func.__class__.__name__
else:
name = str(func)
if is_async_callable(func):
run = RunnableCallable(
None,
func,
explode_args=True,
name=name,
trace=False,
recurse=False,
)
else:
run = RunnableCallable(
func,
functools.wraps(func)(functools.partial(run_in_executor, None, func)),
explode_args=True,
name=name,
trace=False,
recurse=False,
)
seq = RunnableSeq(
coerce_to_runnable(func, name=None, trace=False),
ChannelWrite([ChannelWriteEntry(RETURN)]),
name=func.__name__,
run,
ChannelWrite([ChannelWriteEntry(RETURN)], tags=[TAG_HIDDEN]),
name=name,
trace_inputs=functools.partial(
_explode_args_trace_inputs, inspect.signature(func)
),
)
if not _lookup_module_and_qualname(func):
return seq
return CACHE.setdefault(func, seq)
return CACHE.setdefault(key, seq)
CACHE: dict[Callable[..., Any], RunnableSeq] = {}
CACHE: dict[tuple[Callable[..., Any], bool], Runnable] = {}
P = ParamSpec("P")
P1 = TypeVar("P1")
T = TypeVar("T")
class SyncAsyncFuture(Generic[T], concurrent.futures.Future[T]):
def __await__(self) -> Generator[T, None, T]:
yield cast(T, ...)
def call(
func: Callable[P, T],
*args: Any,
retry: Optional[RetryPolicy] = None,
**kwargs: Any,
) -> SyncAsyncFuture[T]:
config = get_config()
impl = config[CONF][CONFIG_KEY_CALL]
fut = impl(func, (args, kwargs), retry=retry, callbacks=config["callbacks"])
return fut
+10 -17
View File
@@ -1,6 +1,5 @@
import asyncio
import concurrent.futures
import sys
import time
from contextlib import ExitStack
from contextvars import copy_context
@@ -22,6 +21,7 @@ from langchain_core.runnables.config import get_executor_for_config
from typing_extensions import ParamSpec
from langgraph.errors import GraphBubbleUp
from langgraph.utils.future import CONTEXT_NOT_SUPPORTED, run_coroutine_threadsafe
P = ParamSpec("P")
T = TypeVar("T")
@@ -132,8 +132,7 @@ class AsyncBackgroundExecutor(AsyncContextManager):
ignoring CancelledError"""
def __init__(self, config: RunnableConfig) -> None:
self.context_not_supported = sys.version_info < (3, 11)
self.tasks: dict[asyncio.Task, tuple[bool, bool]] = {}
self.tasks: dict[asyncio.Future, tuple[bool, bool]] = {}
self.sentinel = object()
self.loop = asyncio.get_running_loop()
if max_concurrency := config.get("max_concurrency"):
@@ -150,23 +149,23 @@ class AsyncBackgroundExecutor(AsyncContextManager):
__name__: Optional[str] = None,
__cancel_on_exit__: bool = False,
__reraise_on_exit__: bool = True,
__next_tick__: bool = False,
__next_tick__: bool = False, # noop in async (always True)
**kwargs: P.kwargs,
) -> asyncio.Task[T]:
) -> asyncio.Future[T]:
coro = cast(Coroutine[None, None, T], fn(*args, **kwargs))
if self.semaphore:
coro = gated(self.semaphore, coro)
if __next_tick__:
coro = anext_tick(coro)
if self.context_not_supported:
task = self.loop.create_task(coro, name=__name__)
if CONTEXT_NOT_SUPPORTED:
task = run_coroutine_threadsafe(coro, self.loop, name=__name__)
else:
task = self.loop.create_task(coro, name=__name__, context=copy_context())
task = run_coroutine_threadsafe(
coro, self.loop, name=__name__, context=copy_context()
)
self.tasks[task] = (__cancel_on_exit__, __reraise_on_exit__)
task.add_done_callback(self.done)
return task
def done(self, task: asyncio.Task) -> None:
def done(self, task: asyncio.Future) -> None:
try:
if exc := task.exception():
# This exception is an interruption signal, not an error
@@ -219,9 +218,3 @@ def next_tick(fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
"""A function that yields control to other threads before running another function."""
time.sleep(0)
return fn(*args, **kwargs)
async def anext_tick(coro: Coroutine[None, None, T]) -> T:
"""A coroutine that yields control to event loop before running another coroutine."""
await asyncio.sleep(0)
return await coro
+1 -1
View File
@@ -89,7 +89,7 @@ def map_command(
raise TypeError(
f"In Command.goto, expected Send/str, got {type(send).__name__}"
)
if cmd.resume:
if cmd.resume is not None:
if isinstance(cmd.resume, dict) and all(is_task_id(k) for k in cmd.resume):
for tid, resume in cmd.resume.items():
existing: list[Any] = next(
+16 -9
View File
@@ -54,7 +54,6 @@ from langgraph.constants import (
ERROR,
INPUT,
INTERRUPT,
MISSING,
NS_SEP,
NULL_TASK_ID,
PUSH,
@@ -229,20 +228,23 @@ class PregelLoop(LoopProtocol):
if self.stream is not None and CONFIG_KEY_STREAM in config[CONF]:
self.stream = DuplexStream(self.stream, config[CONF][CONFIG_KEY_STREAM])
scratchpad: Optional[PregelScratchpad] = config[CONF].get(CONFIG_KEY_SCRATCHPAD)
if not self.config[CONF].get(CONFIG_KEY_DELEGATE) and scratchpad is not None:
if scratchpad["subgraph_counter"]:
if not self.config[CONF].get(CONFIG_KEY_DELEGATE) and isinstance(
scratchpad, PregelScratchpad
):
# if count is > 0, append to checkpoint_ns
# if count is 0, leave as is
if cnt := scratchpad.subgraph_counter():
self.config = patch_configurable(
self.config,
{
CONFIG_KEY_CHECKPOINT_NS: NS_SEP.join(
(
config[CONF][CONFIG_KEY_CHECKPOINT_NS],
str(scratchpad["subgraph_counter"]),
str(cnt),
)
)
},
)
scratchpad["subgraph_counter"] += 1
if not self.is_nested and config[CONF].get(CONFIG_KEY_CHECKPOINT_NS):
self.config = patch_configurable(
self.config,
@@ -563,9 +565,14 @@ class PregelLoop(LoopProtocol):
)
# take resume value from parent
if scratchpad := configurable.get(CONFIG_KEY_SCRATCHPAD):
if scratchpad["null_resume"] is not MISSING:
self.put_writes(NULL_TASK_ID, [(RESUME, scratchpad["null_resume"])])
if scratchpad := cast(
Optional[PregelScratchpad], configurable.get(CONFIG_KEY_SCRATCHPAD)
):
if (
isinstance(scratchpad, PregelScratchpad)
and scratchpad.null_resume is not None
):
self.put_writes(NULL_TASK_ID, [(RESUME, scratchpad.null_resume)])
# map command to writes
if isinstance(self.input, Command):
if self.input.resume is not None and not self.checkpointer:
@@ -1084,6 +1091,6 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
return await exit_task
except asyncio.CancelledError as e:
# Bubble up the exit task upon cancellation to permit the API
# consumer to await it before e.g., re-using the DB connection.
# consumer to await it before e.g., reusing the DB connection.
e.args = (*e.args, exit_task)
raise
+35 -21
View File
@@ -39,7 +39,7 @@ from langgraph.errors import GraphBubbleUp, GraphInterrupt
from langgraph.pregel.algo import Call
from langgraph.pregel.executor import Submit
from langgraph.pregel.retry import arun_with_retry, run_with_retry
from langgraph.types import PregelExecutableTask, RetryPolicy
from langgraph.types import PregelExecutableTask, PregelScratchpad, RetryPolicy
from langgraph.utils.future import chain_future
F = TypeVar("F", concurrent.futures.Future, asyncio.Future)
@@ -75,6 +75,7 @@ class FuturesDict(Generic[F, E], dict[F, Optional[PregelExecutableTask]]):
super().__setitem__(key, value) # type: ignore[index]
if value is not None:
with self.lock:
self.event.clear()
self.counter += 1
key.add_done_callback(partial(self.on_done, value))
@@ -134,8 +135,7 @@ class PregelRunner:
return task.config[CONF][CONFIG_KEY_SEND](writes)
# schedule PUSH tasks, collect futures
scratchpad = task.config[CONF][CONFIG_KEY_SCRATCHPAD]
scratchpad.setdefault("call_counter", 0)
scratchpad: PregelScratchpad = task.config[CONF][CONFIG_KEY_SCRATCHPAD]
rtn: dict[int, Optional[concurrent.futures.Future]] = {}
for idx, w in enumerate(writes):
# bail if not a PUSH write
@@ -143,9 +143,9 @@ class PregelRunner:
continue
# schedule the next task, if the callback returns one
wcall = calls[idx] if calls else None
cnt = scratchpad["call_counter"]
scratchpad["call_counter"] += 1
if next_task := self.schedule_task(task, cnt, wcall):
if next_task := self.schedule_task(
task, scratchpad.call_counter(), wcall
):
if fut := next(
(
f
@@ -213,9 +213,7 @@ class PregelRunner:
assert fut is not None, "writer did not return a future for call"
# return a chained future to ensure commit() callback is called
# before the returned future is resolved, to ensure stream order etc
sfut: concurrent.futures.Future[Any] = concurrent.futures.Future()
chain_future(fut, sfut)
return sfut
return chain_future(fut, concurrent.futures.Future())
tasks = tuple(tasks)
futures = FuturesDict(
@@ -298,6 +296,8 @@ class PregelRunner:
futures.event.wait(
timeout=(max(0, end_time - time.monotonic()) if end_time else None)
)
# give control back to the caller
yield
# panic on failure or timeout
_panic_or_proceed(
futures.done.union(f for f, t in futures.items() if t is not None),
@@ -323,8 +323,7 @@ class PregelRunner:
return task.config[CONF][CONFIG_KEY_SEND](writes)
# schedule PUSH tasks, collect futures
scratchpad = task.config[CONF][CONFIG_KEY_SCRATCHPAD]
scratchpad.setdefault("call_counter", 0)
scratchpad: PregelScratchpad = task.config[CONF][CONFIG_KEY_SCRATCHPAD]
rtn: dict[int, Optional[asyncio.Future]] = {}
for idx, w in enumerate(writes):
# bail if not a PUSH write
@@ -332,9 +331,9 @@ class PregelRunner:
continue
# schedule the next task, if the callback returns one
wcall = calls[idx] if calls is not None else None
cnt = scratchpad["call_counter"]
scratchpad["call_counter"] += 1
if next_task := self.schedule_task(task, cnt, wcall):
if next_task := self.schedule_task(
task, scratchpad.call_counter(), wcall
):
# if the parent task was retried,
# the next task might already be running
if fut := next(
@@ -400,10 +399,6 @@ class PregelRunner:
retry: Optional[RetryPolicy] = None,
callbacks: Callbacks = None,
) -> Union[asyncio.Future[Any], concurrent.futures.Future[Any]]:
if not asyncio.iscoroutinefunction(func):
raise RuntimeError(
"In an async context use func.to_thread(...) to invoke tasks"
)
(fut,) = writer(
task,
[(PUSH, None)],
@@ -412,9 +407,26 @@ class PregelRunner:
assert fut is not None, "writer did not return a future for call"
# return a chained future to ensure commit() callback is called
# before the returned future is resolved, to ensure stream order etc
sfut: asyncio.Future[Any] = asyncio.Future(loop=loop)
chain_future(fut, sfut)
return sfut
try:
in_async = asyncio.current_task() is not None
except RuntimeError:
in_async = False
# if in async context return an async future
# otherwise return a chained sync future
if in_async:
if isinstance(fut, asyncio.Task):
sfut: Union[asyncio.Future[Any], concurrent.futures.Future[Any]] = (
asyncio.Future(loop=loop)
)
loop.call_soon_threadsafe(chain_future, fut, sfut)
return sfut
else:
# already wrapped in a future
return fut
else:
sfut = concurrent.futures.Future()
loop.call_soon_threadsafe(chain_future, fut, sfut)
return sfut
loop = asyncio.get_event_loop()
tasks = tuple(tasks)
@@ -506,6 +518,8 @@ class PregelRunner:
futures.event.wait(),
timeout=(max(0, end_time - loop.time()) if end_time else None),
)
# give control back to the caller
yield
# cancel waiter task
for fut in futures:
fut.cancel()
+28 -18
View File
@@ -16,10 +16,11 @@ from typing import (
TypeVar,
Union,
cast,
get_type_hints,
)
from langchain_core.runnables import Runnable, RunnableConfig
from typing_extensions import Self, TypedDict
from typing_extensions import Self
from langgraph.checkpoint.base import BaseCheckpointSaver, CheckpointMetadata
@@ -289,6 +290,8 @@ class Command(Generic[N], ToolOutputMixin):
for t in self.update
):
return self.update
elif hints := get_type_hints(type(self.update)):
return [(k, getattr(self.update, k)) for k in hints]
elif self.update is not None:
return [("__root__", self.update)]
else:
@@ -339,15 +342,25 @@ class LoopProtocol:
self.stop = stop
class PregelScratchpad(TypedDict):
@dataclasses.dataclass(**{**_DC_KWARGS, "frozen": False})
class PregelScratchpad:
# call
call_counter: int
call_counter: Callable[[], int]
# interrupt
interrupt_counter: int
interrupt_counter: Callable[[], int]
resume: list[Any]
null_resume: Any
null_resume: Optional[Any]
_consume_null_resume: Callable[[], None]
# subgraph
subgraph_counter: int
subgraph_counter: Callable[[], int]
def consume_null_resume(self) -> Any:
if self.null_resume is not None:
value = self.null_resume
self._consume_null_resume()
self.null_resume = None
return value
raise ValueError("No null resume to consume")
def interrupt(value: Any) -> Any:
@@ -449,7 +462,6 @@ def interrupt(value: Any) -> Any:
CONFIG_KEY_CHECKPOINT_NS,
CONFIG_KEY_SCRATCHPAD,
CONFIG_KEY_SEND,
MISSING,
NS_SEP,
RESUME,
)
@@ -459,19 +471,17 @@ def interrupt(value: Any) -> Any:
conf = get_config()["configurable"]
# track interrupt index
scratchpad: PregelScratchpad = conf[CONFIG_KEY_SCRATCHPAD]
scratchpad["interrupt_counter"] += 1
idx = scratchpad["interrupt_counter"]
idx = scratchpad.interrupt_counter()
# find previous resume values
if scratchpad["resume"]:
if idx < len(scratchpad["resume"]):
return scratchpad["resume"][idx]
if scratchpad.resume:
if idx < len(scratchpad.resume):
return scratchpad.resume[idx]
# find current resume value
if scratchpad["null_resume"] is not MISSING:
assert len(scratchpad["resume"]) == idx, (scratchpad["resume"], idx)
v = scratchpad["null_resume"]
scratchpad["null_resume"] = MISSING
scratchpad["resume"].append(v)
conf[CONFIG_KEY_SEND]([(RESUME, scratchpad["resume"])])
if scratchpad.null_resume is not None:
assert len(scratchpad.resume) == idx, (scratchpad.resume, idx)
v = scratchpad.consume_null_resume()
scratchpad.resume.append(v)
conf[CONFIG_KEY_SEND]([(RESUME, scratchpad.resume)])
return v
# no resume value found
raise GraphInterrupt(
+80 -3
View File
@@ -1,9 +1,16 @@
import asyncio
import concurrent.futures
from typing import Union
import contextvars
import inspect
import sys
import types
from typing import Awaitable, Coroutine, Generator, Optional, TypeVar, Union, cast
T = TypeVar("T")
AnyFuture = Union[asyncio.Future, concurrent.futures.Future]
CONTEXT_NOT_SUPPORTED = sys.version_info < (3, 11)
def _get_loop(fut: asyncio.Future) -> asyncio.AbstractEventLoop:
# Tries to call Future.get_loop() if it's available.
@@ -52,10 +59,11 @@ def _copy_future_state(source: AnyFuture, dest: asyncio.Future) -> None:
The other Future may be a concurrent.futures.Future.
"""
if dest.done():
return
assert source.done()
if dest.cancelled():
return
assert not dest.done()
if source.cancelled():
dest.cancel()
else:
@@ -112,10 +120,11 @@ def _chain_future(source: AnyFuture, destination: AnyFuture) -> None:
source.add_done_callback(_call_set_state)
def chain_future(source: AnyFuture, destination: AnyFuture) -> None:
def chain_future(source: AnyFuture, destination: AnyFuture) -> AnyFuture:
# adapted from asyncio.run_coroutine_threadsafe
try:
_chain_future(source, destination)
return destination
except (SystemExit, KeyboardInterrupt):
raise
except BaseException as exc:
@@ -125,3 +134,71 @@ def chain_future(source: AnyFuture, destination: AnyFuture) -> None:
else:
destination.set_exception(exc)
raise
def _ensure_future(
coro_or_future: Union[Coroutine[None, None, T], Awaitable[T]],
*,
loop: asyncio.AbstractEventLoop,
name: Optional[str] = None,
context: Optional[contextvars.Context] = None,
) -> asyncio.Task[T]:
called_wrap_awaitable = False
if not asyncio.iscoroutine(coro_or_future):
if inspect.isawaitable(coro_or_future):
coro_or_future = cast(
Coroutine[None, None, T], _wrap_awaitable(coro_or_future)
)
called_wrap_awaitable = True
else:
raise TypeError(
"An asyncio.Future, a coroutine or an awaitable is required."
f" Got {type(coro_or_future).__name__} instead."
)
try:
if CONTEXT_NOT_SUPPORTED:
return loop.create_task(coro_or_future, name=name)
else:
return loop.create_task(coro_or_future, name=name, context=context)
except RuntimeError:
if not called_wrap_awaitable:
coro_or_future.close()
raise
@types.coroutine
def _wrap_awaitable(awaitable: Awaitable[T]) -> Generator[None, None, T]:
"""Helper for asyncio.ensure_future().
Wraps awaitable (an object with __await__) into a coroutine
that will later be wrapped in a Task by ensure_future().
"""
return (yield from awaitable.__await__())
def run_coroutine_threadsafe(
coro: Coroutine[None, None, T],
loop: asyncio.AbstractEventLoop,
name: Optional[str] = None,
context: Optional[contextvars.Context] = None,
) -> asyncio.Future[T]:
"""Submit a coroutine object to a given event loop.
Return a asyncio.Future to access the result.
"""
future: asyncio.Future[T] = asyncio.Future(loop=loop)
def callback() -> None:
try:
chain_future(
_ensure_future(coro, loop=loop, name=name, context=context), future
)
except (SystemExit, KeyboardInterrupt):
raise
except BaseException as exc:
future.set_exception(exc)
raise
loop.call_soon_threadsafe(callback, context=context)
return future
+31 -17
View File
@@ -36,7 +36,7 @@ from typing_extensions import TypeGuard
from langgraph.constants import (
CONF,
CONFIG_KEY_END,
CONFIG_KEY_PREVIOUS,
CONFIG_KEY_STORE,
CONFIG_KEY_STREAM_WRITER,
)
@@ -85,7 +85,7 @@ KWARGS_CONFIG_KEYS: tuple[tuple[str, tuple[Any, ...], str, Any], ...] = (
(
sys.intern("previous"),
(ANY_TYPE,),
CONFIG_KEY_END,
CONFIG_KEY_PREVIOUS,
inspect.Parameter.empty,
),
)
@@ -120,6 +120,7 @@ class RunnableCallable(Runnable):
tags: Optional[Sequence[str]] = None,
trace: bool = True,
recurse: bool = True,
explode_args: bool = False,
**kwargs: Any,
) -> None:
self.name = name
@@ -141,6 +142,7 @@ class RunnableCallable(Runnable):
self.kwargs = kwargs
self.trace = trace
self.recurse = recurse
self.explode_args = explode_args
# check signature
if func is None and afunc is None:
raise ValueError("At least one of func or afunc must be provided.")
@@ -176,7 +178,12 @@ class RunnableCallable(Runnable):
)
if config is None:
config = ensure_config()
kwargs = {**self.kwargs, **kwargs}
if self.explode_args:
args, _kwargs = input
kwargs = {**self.kwargs, **_kwargs, **kwargs}
else:
args = (input,)
kwargs = {**self.kwargs, **kwargs}
if self.func_accepts_config:
kwargs["config"] = config
_conf = config[CONF]
@@ -208,7 +215,7 @@ class RunnableCallable(Runnable):
child_config = patch_config(config, callbacks=run_manager.get_child())
context = copy_context()
context.run(_set_config_context, child_config)
ret = context.run(self.func, input, **kwargs)
ret = context.run(self.func, *args, **kwargs)
except BaseException as e:
run_manager.on_chain_error(e)
raise
@@ -216,7 +223,7 @@ class RunnableCallable(Runnable):
run_manager.on_chain_end(ret)
else:
context.run(_set_config_context, config)
ret = context.run(self.func, input, **kwargs)
ret = context.run(self.func, *args, **kwargs)
if isinstance(ret, Runnable) and self.recurse:
return ret.invoke(input, config)
return ret
@@ -228,7 +235,12 @@ class RunnableCallable(Runnable):
return self.invoke(input, config)
if config is None:
config = ensure_config()
kwargs = {**self.kwargs, **kwargs}
if self.explode_args:
args, _kwargs = input
kwargs = {**self.kwargs, **_kwargs, **kwargs}
else:
args = (input,)
kwargs = {**self.kwargs, **kwargs}
if self.func_accepts_config:
kwargs["config"] = config
_conf = config[CONF]
@@ -258,7 +270,7 @@ class RunnableCallable(Runnable):
try:
child_config = patch_config(config, callbacks=run_manager.get_child())
context.run(_set_config_context, child_config)
coro = cast(Coroutine[None, None, Any], self.afunc(input, **kwargs))
coro = cast(Coroutine[None, None, Any], self.afunc(*args, **kwargs))
if ASYNCIO_ACCEPTS_CONTEXT:
ret = await asyncio.create_task(coro, context=context)
else:
@@ -271,10 +283,10 @@ class RunnableCallable(Runnable):
else:
context.run(_set_config_context, config)
if ASYNCIO_ACCEPTS_CONTEXT:
coro = cast(Coroutine[None, None, Any], self.afunc(input, **kwargs))
coro = cast(Coroutine[None, None, Any], self.afunc(*args, **kwargs))
ret = await asyncio.create_task(coro, context=context)
else:
ret = await self.afunc(input, **kwargs)
ret = await self.afunc(*args, **kwargs)
if isinstance(ret, Runnable) and self.recurse:
return await ret.ainvoke(input, config)
return ret
@@ -347,6 +359,7 @@ class RunnableSeq(Runnable):
self,
*steps: RunnableLike,
name: Optional[str] = None,
trace_inputs: Optional[Callable[[Any], Any]] = None,
) -> None:
"""Create a new RunnableSeq.
@@ -371,6 +384,7 @@ class RunnableSeq(Runnable):
)
self.steps = steps_flat
self.name = name
self.trace_inputs = trace_inputs
def __or__(
self,
@@ -432,7 +446,7 @@ class RunnableSeq(Runnable):
# start the root run
run_manager = callback_manager.on_chain_start(
None,
input,
self.trace_inputs(input) if self.trace_inputs is not None else input,
name=config.get("run_name") or self.get_name(),
run_id=config.pop("run_id", None),
)
@@ -442,7 +456,7 @@ class RunnableSeq(Runnable):
for i, step in enumerate(self.steps):
# mark each step as a child run
config = patch_config(
config, callbacks=run_manager.get_child(f"seq:step:{i+1}")
config, callbacks=run_manager.get_child(f"seq:step:{i + 1}")
)
if i == 0:
input = step.invoke(input, config, **kwargs)
@@ -469,7 +483,7 @@ class RunnableSeq(Runnable):
# start the root run
run_manager = await callback_manager.on_chain_start(
None,
input,
self.trace_inputs(input) if self.trace_inputs is not None else input,
name=config.get("run_name") or self.get_name(),
run_id=config.pop("run_id", None),
)
@@ -479,7 +493,7 @@ class RunnableSeq(Runnable):
for i, step in enumerate(self.steps):
# mark each step as a child run
config = patch_config(
config, callbacks=run_manager.get_child(f"seq:step:{i+1}")
config, callbacks=run_manager.get_child(f"seq:step:{i + 1}")
)
if i == 0:
input = await step.ainvoke(input, config, **kwargs)
@@ -506,7 +520,7 @@ class RunnableSeq(Runnable):
# start the root run
run_manager = callback_manager.on_chain_start(
None,
input,
self.trace_inputs(input) if self.trace_inputs is not None else input,
name=config.get("run_name") or self.get_name(),
run_id=config.pop("run_id", None),
)
@@ -519,7 +533,7 @@ class RunnableSeq(Runnable):
for idx, step in enumerate(self.steps):
config = patch_config(
config,
callbacks=run_manager.get_child(f"seq:step:{idx+1}"),
callbacks=run_manager.get_child(f"seq:step:{idx + 1}"),
)
if idx == 0:
iterator = step.stream(input, config, **kwargs)
@@ -569,7 +583,7 @@ class RunnableSeq(Runnable):
# start the root run
run_manager = await callback_manager.on_chain_start(
None,
input,
self.trace_inputs(input) if self.trace_inputs is not None else input,
name=config.get("run_name") or self.get_name(),
run_id=config.pop("run_id", None),
)
@@ -583,7 +597,7 @@ class RunnableSeq(Runnable):
for idx, step in enumerate(self.steps):
config = patch_config(
config,
callbacks=run_manager.get_child(f"seq:step:{idx+1}"),
callbacks=run_manager.get_child(f"seq:step:{idx + 1}"),
)
if idx == 0:
aiterator = step.astream(input, config, **kwargs)
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
version = "0.2.65"
version = "0.2.68"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"

Some files were not shown because too many files have changed in this diff Show More