mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-19 06:05:44 +02:00
Compare commits
82
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac64f50383 | ||
|
|
25239891bc | ||
|
|
0bdd27ade4 | ||
|
|
ab0048981a | ||
|
|
e18f2b3795 | ||
|
|
a11f9b3535 | ||
|
|
29db5a8672 | ||
|
|
444faec6e6 | ||
|
|
e4a5c8fd28 | ||
|
|
0f1b0bfba3 | ||
|
|
0e79407973 | ||
|
|
efa51fe22c | ||
|
|
11f2501c98 | ||
|
|
71e6002a3c | ||
|
|
26e3ded701 | ||
|
|
4fcd1690f9 | ||
|
|
eb33a873c8 | ||
|
|
91aa66f4cf | ||
|
|
4476640702 | ||
|
|
727e3f1730 | ||
|
|
943dd28863 | ||
|
|
aa9d253978 | ||
|
|
a11c6cfe6b | ||
|
|
15c79dc3c3 | ||
|
|
a65c9f1e04 | ||
|
|
9912ae1053 | ||
|
|
7db29042b4 | ||
|
|
f8033a20c1 | ||
|
|
5e3aa495d9 | ||
|
|
a54689affd | ||
|
|
02ea523897 | ||
|
|
18c6637e6c | ||
|
|
4c02a4c501 | ||
|
|
784b6afa11 | ||
|
|
b35b892d74 | ||
|
|
e7aaf21121 | ||
|
|
54115ac796 | ||
|
|
76be64adcb | ||
|
|
2cf98725c4 | ||
|
|
a33c59626a | ||
|
|
56d3b759c7 | ||
|
|
32bf81c559 | ||
|
|
de332ec31c | ||
|
|
ce30965df4 | ||
|
|
b98a7b09a3 | ||
|
|
86b6afc982 | ||
|
|
47122ce88b | ||
|
|
002b048674 | ||
|
|
a58f5dacca | ||
|
|
cc397b1629 | ||
|
|
3a48049194 | ||
|
|
e5f0db0af3 | ||
|
|
93854ed721 | ||
|
|
83a7acc57c | ||
|
|
b218cc76a7 | ||
|
|
d021f476db | ||
|
|
d978cb0392 | ||
|
|
605fe4ea11 | ||
|
|
46907b6cf9 | ||
|
|
582856b30c | ||
|
|
e476897177 | ||
|
|
421f7c0238 | ||
|
|
3095bfce9c | ||
|
|
4cfe0b2d4c | ||
|
|
a16def5140 | ||
|
|
1e730d124c | ||
|
|
21a7105655 | ||
|
|
8c88e203bc | ||
|
|
2a1224966c | ||
|
|
c4460e5dd2 | ||
|
|
46056363b3 | ||
|
|
ba672604a6 | ||
|
|
8b1597a385 | ||
|
|
adff439d4e | ||
|
|
1654062957 | ||
|
|
6b707cbfc5 | ||
|
|
30311f3fb9 | ||
|
|
0f458fbaff | ||
|
|
d7bae594f0 | ||
|
|
7603809a9f | ||
|
|
311fe3970c | ||
|
|
67a800dd98 |
@@ -85,7 +85,8 @@ jobs:
|
||||
if [ "${{ github.event_name }}" == "schedule" ] || [ "${{ github.event_name }}" == "workflow_dispatch" ] || ([ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/main" ]); then
|
||||
echo "Running link check on all HTML files matching notebooks in docs directory..."
|
||||
poetry run pytest -v \
|
||||
--check-links-ignore "https://(api|web|docs|academy)\.smith\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://academy\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://x.com/.*" \
|
||||
--check-links-ignore "https://github\.com/.*" \
|
||||
--check-links-ignore "http://localhost:8123/.*" \
|
||||
@@ -106,7 +107,8 @@ jobs:
|
||||
if [ -n "${CHANGED_FILES}" ]; then
|
||||
echo "Running link check on HTML files matching changed notebook files..."
|
||||
poetry run pytest -v \
|
||||
--check-links-ignore "https://(api|web|docs|academy)\.smith\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://academy\.langchain\.com/.*" \
|
||||
--check-links-ignore "http://localhost:8123/.*" \
|
||||
--check-links-ignore "http://localhost:2024.*" \
|
||||
--check-links-ignore "http://127.0.0.1:.*" \
|
||||
|
||||
@@ -47,9 +47,7 @@ pip install -U langgraph
|
||||
|
||||
## Example
|
||||
|
||||
One of the central concepts of LangGraph is state. Each graph execution creates a state that is passed between nodes in the graph as they execute, and each node updates this internal state with its return value after it executes. The way that the graph updates its internal state is defined by either the type of graph chosen or a custom function.
|
||||
|
||||
Let's take a look at a simple example of an agent that can use a search tool.
|
||||
Let's build a tool-calling [ReAct-style](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#react-implementation) agent that uses a search tool!
|
||||
|
||||
```shell
|
||||
pip install langchain-anthropic
|
||||
@@ -66,10 +64,58 @@ export LANGSMITH_TRACING=true
|
||||
export LANGSMITH_API_KEY=lsv2_sk_...
|
||||
```
|
||||
|
||||
```python
|
||||
from typing import Annotated, Literal, TypedDict
|
||||
The simplest way to create a tool-calling agent in LangGraph is to use `create_react_agent`:
|
||||
|
||||
<details open>
|
||||
<summary>High-level implementation</summary>
|
||||
|
||||
```python
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.tools import tool
|
||||
|
||||
# Define the tools for the agent to use
|
||||
@tool
|
||||
def search(query: str):
|
||||
"""Call to surf the web."""
|
||||
# This is a placeholder, but don't tell the LLM that...
|
||||
if "sf" in query.lower() or "san francisco" in query.lower():
|
||||
return "It's 60 degrees and foggy."
|
||||
return "It's 90 degrees and sunny."
|
||||
|
||||
|
||||
tools = [search]
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0)
|
||||
|
||||
# Initialize memory to persist state between graph runs
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
app = create_react_agent(model, tools, checkpointer=checkpointer)
|
||||
|
||||
# Use the agent
|
||||
final_state = app.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
config={"configurable": {"thread_id": 42}}
|
||||
)
|
||||
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?"
|
||||
```
|
||||
</details>
|
||||
|
||||
> [!TIP]
|
||||
> LangGraph is a **low-level** framework that allows you to implement any custom agent
|
||||
architectures. Click on the low-level implementation below to see how to implement a
|
||||
tool-calling agent from scratch.
|
||||
|
||||
<details>
|
||||
<summary>Low-level implementation</summary>
|
||||
|
||||
```python
|
||||
from typing import Literal
|
||||
|
||||
from langchain_core.messages import HumanMessage
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.tools import tool
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
@@ -91,7 +137,7 @@ tools = [search]
|
||||
|
||||
tool_node = ToolNode(tools)
|
||||
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-20240620", temperature=0).bind_tools(tools)
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0).bind_tools(tools)
|
||||
|
||||
# Define the function that determines whether to continue or not
|
||||
def should_continue(state: MessagesState) -> Literal["tools", END]:
|
||||
@@ -145,23 +191,20 @@ checkpointer = MemorySaver()
|
||||
# Note that we're (optionally) passing the memory when compiling the graph
|
||||
app = workflow.compile(checkpointer=checkpointer)
|
||||
|
||||
# Use the Runnable
|
||||
# Use the agent
|
||||
final_state = app.invoke(
|
||||
{"messages": [HumanMessage(content="what is the weather in sf")]},
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
config={"configurable": {"thread_id": 42}}
|
||||
)
|
||||
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?"
|
||||
```
|
||||
</details>
|
||||
|
||||
Now when we pass the same `"thread_id"`, the conversation context is retained via the saved state (i.e. stored list of messages)
|
||||
|
||||
```python
|
||||
final_state = app.invoke(
|
||||
{"messages": [HumanMessage(content="what about ny")]},
|
||||
{"messages": [{"role": "user", "content": "what about ny"}]},
|
||||
config={"configurable": {"thread_id": 42}}
|
||||
)
|
||||
final_state["messages"][-1].content
|
||||
|
||||
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
@@ -1 +0,0 @@
|
||||
eNqlWQ9wFNUZD1IdZKytoNZ2VJ4nGqHZy93lDyTq2HAQEgiSklOEhEn39t7dLdndt9m3m8uR0hZEUZlaDopUxVYxJDZNERSmWsVOnVpBq4JVW2SqdhQVtRZbO1Vspd/3dvf+JAHtyAxDsvv2ff9+v9/3vcfqwR5qcZUZ44ZVw6aWrNjwC8+tHrRot0O5vWZAp3aaJfpbF7bF7nUs9eAlads2eX1lpWyqQdmw0xYzVSWoML2yJ1ypU87lFOX9cZbIvnx2S19Al3s7bdZFDR6oJ+FQpLqCBPxV8KS9L2AxjcJPAYdTKwBvFQauGDY+ai7XiUZly1CNFGmRjdRcSzbTQRJljpYgWeaQBCOc6ZRYlMM6JU2YQVSbJJlFdHpVYGUFKRiQOVe5DT6XWgEXbNor7EWpZcuqoWUvIs3lCRKnJC2bZpbYrGAg74YwAj4ESYyRFLWJnaZEZ9wmjinZTErINiWykSCQG9OiaUiB2kOJasB3uoyJrgArmkYgcPFtTO5RtSzx7FAjpRoUTWuMdcEClcPGQdICliBgCNxOy7bvBDFYJohx2VlTBCtCEuHnnzCmdYItXGXIuvtMmOx0TXZCjI5m887lnBm4SjVMB/PSFwAwWFn8oBB92tFlQ1INCVyXwEWTZJjVldRYBs0G1IRv0+kMhVtro22LIlUti7obLPWarvjSucmU0h1YuaykQKMR0D7CfdfDEShp7+sIOJbWAT93BHx4ZjKZYIrSrqyUkhNQHS4gOsppSVYrO2C/Dn9Dd5cmXAelEnVpweAamgnPcpvqnGRU24VBNA1gESUuYHM5J7OyBNmUsqDIgNu0txfwqwcMQN0J1K2LZkmCKiqyj5gMXvMKwh0ovMyJaTEFOIJfWzTpGAl4V2JCTiQgFZxy4SAsj2tUJywJO2EQqqyhw9SymMUJckHlkNwgaWqOtYArPUzrgW+5DS7SlKrImvbZPkOYvsOSLnfhMljA0JBfeXATtmIZfIfAlBHMugaeAgk0TY4zSwAfmGVnKDU8O7TXBOKp3KWLLitpRD5a1zQ1RQ0FPI8h/lOOmqAknhVR27A1ZrJQCdhHY5aXFK6mDDUJscHnmBkROu6fZpmSZILPkLYMtZKORhBlwvWiqMAjA+RKh0TAWiEiHEQG/uqmJh6LoIJYeIf7UtWmA0w6nEgoXMeBwqkUvlBkU46rmmqrFOwib8E/2B9iFVAiaTWVBsnroRr6jJgEchAGMi2CKvYKYwFNcSzqKsHomgFO4KXCLIsqdlGFacL9OJmE56BJQbIYIY0hccc0meXKitjKckxEppKmSpcPUwGFLGiIqpQsKoEoNWQAJf9/EMDFzgWmBTuEgp2A3ZA924lTweuMbCvpq3qurJvVGr22JrakVU+Nwepmw5XRHkAR88SXIxywEPgvoDkNLmhAKeyL4ASCJ893EVuFGxlWUyaOoYI0kqZiWWkp1kIPuNBFLCYjuZEeHAst4gaDUBDDhsCDQQx3mdBwT6k7P5eIotjyTsF1WJ2UNU5H6OoJGl9glswBCsxVOa/teD0AsiOgCW5jtkSGRB5Ety3qYQRK6tjF6TFl4LLiaLIlICc2F0Z7bcT06L5xGVJzWkFEQKaoRctBaBHWzeXQNJMMRLC+w+gwwsGiDuxLr6A/vCak6CWkXdEYp+AFJIApqgB+qXi7WAZPssI81ANaRRrJxohiUezgeWko1rlgKdbBFDdBG5OelOou/5BnWOvPEJUgxhUJjoEiLzONTHH4GOElKIocWAFvfeIKmSvS40xahap6kv9FNN7rSZSfANIY3xcUecxDVZDMh2o0Qu5B2byoJdKcl5n60tR7cfMSIXKDFo4Jv0zZ1eVREmrn6+b14KBnL1rQu3qIl3qSJDjhAaNIEn2Y+xtXEE3tQuChcQ4zFtgGAsJH8HtePizao9KMb3G2J6jFkYpEJ91UQKQpmJlt7qq9XeqTnJBNlHINpwrYQMfkZtL57JeUGTY1KE3QhMh4dZBcA1WJghiQOb0y9jQ36wthC+o+KEK0OtZ4MoIP6BEMzw53seklTC4kHjyzRClcueXu3Azq4I0zuH0FiTv2CdxXDehqJvN62ZjTFBqGsRs/Gj0IYdw1QZHrlIfWvDCIxj2Sbeg5H7GmfOy+7vFDLdq7iCJ+rxdffb72zr9wf8dwa4OktaipNWHiwQk30FjaVVyvzEKnvQUnbYUu0UqRCcevkqHGBb6sWIx7LPWOmVgPjjiCn/2k8dL2AbVC6XW1BYHYW0F06E+qBA4VuqxnWZezBA+uKmQFKU+FCT9vcJYRiZgBdS8Z3NwULE6rGh2jE8JJyyi3840wrxcKS+TpAVgVPcRLhOtNCSO8z7noBe6cA9O3o9hufVlhlsSdgImlOk7EcDhP7pHbFEs1AdFGj2oxI98+ZsL0SQ2ahBkuX1FRAlCGHuj80GqwqIXxtHCEHWEJdB3iFz7F8/lwJ7hiSPudvEjXBTgamkcMuSUHAh3Gc0ispuJs6B2OBQc0ioOflLAArkZRr8HYmpM4fpTDl6PvAioQbpzisUwkHcQI4AqzTkL0LiifCK/YpfwhALumX4ORUCoSGC/OkX3RUx1PbDLiRgISpsCGcPwKkiUwMekwzdskgwOef44X7dQUFIBZIQ8gzCentmjMjnny0bt0UsUKCcEoOi/4nReKIT4oMMBDUqmwiUEVM704f6+C/QuvGTBHLh3y5csz4KRToH+UhaBEPG6mkeoVI6njkVu2VI6qIGDExBQ2xrDkEmjEzc4YV0eLYLUexz3cIbLoIkn4Cjssw6socEXD9YomOwkqVUk1EnhhUFuKhCLVodpIyJ/Iveuqz3d1AlQXRIWAcXHDiFsdBm90dQWA3pe2/AUR1FhRHASeN5xaDkdF9yyIZu1romjw4vaHup1WNjhMq0Tc3bkNQtQF9rNEH8TGwbHvARTw6OOBVvbdE/c8wfzdTycH8OmyuAICTgqii2u7wo1Q38hQi3fKo94xi++mAAtQh8BKvCfy9BoPOu3epsuKlrL4cuhhsHTZysE0lZHcP+pPw7CQ2znqAvJ+SBw1bQk6FUP6536ZWqGaFcDcpAbpHFKwrgKDuaEuSk1J1iDfA+5XuR0gfBpwEt9XYhmHPTRJ6Mvo10MIOglQadi5XzX4flS2Zu008CEUrKoORnb0Qp/CC0UQM0kDcuQGTPH+keIXpqx0wT6SdxebG3A/3l68hvHctgWysrCtZEvMdG6bbOm11Q8WP7ccaOM6zQ1GW0eb814WzFUFw+Fg3c6SjXnWUHLbxGFyZz7J+U+GgBtVUqhWCoW3+1nSANp2OtdfE66rvQ/AagL86PUD2P0dvrofOf+HvYPe1e/WhfP9cr5SNql/NpQntyeWdipIpJa0QWdH8pHwjPrqqvrwDDJ3QWw46tmJjVmNnTE4RvMkVGSOX/1BJe0YXTQxFB2z7gcDhbiQbRoQ0pa8e2+oFv6a668OhUIHLz3pSovq7jyX66+qq6v7jH3xHtnO7cL4pFCdFKmNuVHWVC89SMb60r089/wZQH/Ao6knWVnwx19NTrp6bH/CM5YOeU5LaiL3KPzcGQo3Leiec+3VrXOiicWWmWhhctPSbt67u1eCk7aTkGzoa1TyDvu5gyQeScrJ+Mx4zcyZtYnacCQSisvVkXCoKqHEw6FQ5N4eVc4NhYNhkmIspdH7o41SFE6GVGoTsMkNzl5ydcOC5ujwddIiFmeQv5gMeTbgJDLQhnOwlRsSpoHhFh2Azxc1LMntmqnU1YRoGKwoNdVVtTOkOYsX7fABlAdIP8qD+J+KVQOuJD1xCpmybkKZ+DM+tqGBHQqdccPxifc8Hbx6ffLIjR9fe/ah57eeV/+dzcYFZzQmK3ZHjjTFls7Y+/0pb9z99ooHFry78rv9r/z2o5deGFi47MGZnxz5d+XT7cePPvNge+VfNt//0vaXDsQan1z11LR5p18e2XTzpHnj2v7RnHsxNrD5ttU//+GXf9J+x8otdz23IbTx2dvWHV7+gfpwaNPOx+cND3zKe2+YfuXXPv7wvmfOeY001B+ftWPThHcmDB+tLa+8Yu3l+6YExhkb1gyf3XjTY3zi9B8cOWvFdJaruLFs4ZnnNO5bf9oD33t3fvean163J7SbP3SRNPGb4Q3XvD13/ZVPxH7wz77H3npk65xXz2068o0JF/e9/9ppD39yYbfJH1r9xtS1557//jrlzsm798+fel5gyc9umSvvj7+57xcbh7/13HirvWn65Peevye1fsLWWwOzJ973wuoNzhX69ZMSZ/540YHT2J7bDkzbf+GSzmkbLzl6x2XaW62XfnTJ/k0XrbtrMdsr3b27buuk3lfmvbdT+5LZ9uieVZ92tp/6113Dq+LfLjvlj5OnPp6+4L3IE93GrS8e2nbLmksHJ22a1nn48s1r/vbI4WPx/77+60Nb/v72MavvgwfW/WYBPe+T5rV9U37/5u/0wLbzq2u+0vzQ+t69y9+5/vZjt7/8aHbPBS+eHj16503dWzbw7lP/fFZX5MnXX31q6qZ7bl5z6r+effXDiw8EJ7dHat766vSNg1v+tLbtucbDuZm3H9N2/6fj6/uObe99rP7C4+PKyo4fH1+2qmv/rqbxZWX/A7X9M1M=
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
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
|
||||
@@ -1 +1 @@
|
||||
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
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
File diff suppressed because one or more lines are too long
@@ -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
@@ -1 +1 @@
|
||||
eNqFVX9sE1UcH2AGCBkIWUL4IS91IJFdd+26si4IKeVHBoyNrbiNH+mud6+9W6/3jrt327oxJ+AkgEQPEkkwRoHRQh1jg8kiAjEYfkRBZCRAQYVBgBBQQEATIeK7WwdDCPaP5u6974/P+3w+33crYlVQUQUk9WkWJAwVhsXkRdVXxBS4VIMqfj8ahphHXFNRYYl3q6YIiXE8xrKal5XFyIKVkTCvIFlgrSwKZ1XZssJQVZkgVJv8iIuc70vXWcJMjQ+jEJRUSx6w0XZHJrD0RJGVRXUWBYmQPFk0FSoWsssiAkXCxlL+m2EgQkaRBCkI5jJScJbCyLwVeJAmciCCNMAhoKIwBApUSRzLAyQBAYMAUkAYTrXULzHaIQ6KRjlWZDQOUtlUDqUiSYKYshNAtNNOG30xQmISksSETUiYqRLEiK+7tI/00ESs+ipJspHAQZVVBNmgzAh2gyQEKAUFCQJEdsJCLeRMNIQgWYE84UGogpmAYVlNYbDxJHEAK5qKSWCygxUsUGFAE83EapJjHlWCJAIjkqBWQwWY+hhqAcaPNAxIPYXQBmAV+Scl8iWZrKq8SZUfAqYHHklUIlbjAIIR4lNZHoYZcoI6i0zEhAoWTGnqLGak+fSfo/auZEASEQoBTTZZjMgmdSpWiGiW+nqyZphJUCBnkJssuqRXKPJXQhaT0CX1MR4yHLHkR008UrHe9pzJdhHioIwpKLGIIw30ncFaQc4EHAyIhM44a+hquliPhyCUKUYkfEe7s/RWRpZFgWWM/SxDxuak2SgDy/PbccOTFLGqhPUOdw+OrKIImQkJ0NZsh9XeWkOpmBEkkZiaEolH9ahs7n/Te0Nm2BCpQyXnTY92J7f0jkGqvq2AYQtLnilpMK1vY5Sw07Gn97qiScRfUI95ip5vl9x82i7barNZXW3PFFYjEqtvCzCiCtuekPwkJU5mI5uinRRta+lhSSTWxry+NcdJbydelYn74MooKYk1dUUTUQQePxZLTveWwjk9av6aMqxpOlFHP+DltUxgd4ISKANj9oBtUp7DnudwglkF3mZPso33hWK0eRVi/QARZEaP+DGW16QQ5OKeF8qesDw9ljFsIplHTCWvNiKW8ao3OWiaTox/aaRCBkQwLiG9Kdvlcv1PXeMuwnq7cT6KdlF2p7f7lDmOhQnwoszu+zGJJ2rgIYgyXhL5FE9PNHhp9IvxOJwL40nQlMDp+8mzj7a5aiIq43RyPJybq0nl1UX8tII5tq9qKFZEGkdh8pGAlGmIGqwngJ9zORyuSU6bzZ5jc7rogB8GOKffxkJbLkP+t1YJjB63WW0giFBQhLs8MykPQ64cqsS0jR6bXj7PXZDvaS6jipEfEf68DOFZQhKMlkCF2FGPm63JgCswStKL3eV6ey7ryqE5cnUHnDm5rM1OzSgtbu0x0BODNBm3g/kxWh7tvpEO92keu3ZAivnr512/CF2gBzc+fvXoxoPH3NLNlb7S+7vht6+vST9Hj9h04kgl15m+I2Pmx4+nnCuvjqyffuPOssk/DfSsHFe1b+e1tVd/P1jX+cvdvDGZF/cvGzWlwTV4EOY9hVcmFj9YNG1V/uWyO3H3iT2bv5s3ck2Zf0y9tL0s/fag9MBuv8vXcv3sD6685sZ151ofvVsbqsSjr54uvtx/9rV+R7vGTF6cc2fDFw/qp743MKP4jx0ScyVSt3HZxUN7MkZknLw0v2JO6plheEvq11lz1xwZtfx6auWNv6dTbR+87b4UzP+sA9y61PXWzGOzN/x56rZn9KWOIXed9Loz87n+Z1Ibfz5Z4Ukd436jYmfHpAEVruN3bv2TNn7Yl83s4pIjV1yl9w4Pbyjoqik5G3mwFH2+70LXOzUr0xLVG6QV261DF1hOnGqvnd/VNvyvROvkrlDUUbG64dCELrrzlcaKtIWuxXuviOs+LR4QHXpAH+loj02onyWuuX5vyKrKztLVj/h7Yx/izfc3rQ4dWq9+mHbv/pDvTzfG7InXJn5y8/D5zdzDjr1lLT9e9xFZHj/ul/JbQ+HxYN+UlH8BR110QQ==
|
||||
eNqlVntQVFUYx8ByRrMZTJ0azeOqOCB32QewLtoIrtAgg2zLQ7Bgudx7dvfC3Xtu9wGuPBRMJJ3Rrppmk1qxLMKAgppmQKZWamOjvUXNHmZTM/lIx2pGGzr3sjwUfDTtHztnz/kev+/3fb9vtqaxFAoig7gRLQwnQYGkJPxDVGoaBfiSDEXp5YAXSh5E++0ZmVn1ssB0z/BIEi8mxMSQPKMnOckjIJ6h9BTyxpQaY7xQFEk3FP1FiPadDQ0t13nJpU4JlUBO1CUAo8EUGw10fVb45oVynYBYiE86WYSCDr9SCEPhJPUqFXAQ0kBEXgjgUh4KEnDLDE1yFAQuJIAimWFphnMDkgNJqQDH5CQ9sCGZpYEPySBYBSBFkRGlfjcvnKerzFeBIBqyaiKKJWUaEmYijhARx0GJMGGohniTQUUkIcQGwXKkVwMrkaUM63OKkBQoj1OAosxKorMYO6sONBQpgeFVMlXjJNBrByDnZjgIEH7xMstwZSoaTB0vQA9miCmF0YCkKFkgJfXE0UASZFHChsEMepAtQpfMao5l2EcrUyNJQthBLIMC0GpW+wjIIiRLAMcTMDEAluJvHCKV4/Gt6NFoKoKA7IOHHQWfXi2AUU2cIuWBXhJXUK7jcZsx/YzWtHKdZqmd7ip1cCQVEotQCZB5jUUfr1EnSgJuma6yEt+pDWIESKvkBoPmDzJFRcWQkjTTAeo9spfknAMtHYZwxzB9F5AXV6o5/8cS/w/w/MpGDyRprLL1fg8SJaV9iG52445DXiIgRyF1mJVW9zKGjwY0dLF4DpopdSA1YSrNJRDyBMniQQn0eiltJM+zDEWq7zHq/LUE9UOoWIY+N6syIzSlKAeS+nDE2H1Y5hww6M2xelPbUgKzxnAs1inBkhhSgNfeOwY/8CRVguMQwRWiBHqddw22QaLSkE5SGZl3hFRHRGkgBW987N7B94LMYWFApdFmH5ou+DiQzqw3GvWW9jsCiz6OUhpcJCvC9n6S+12asajNhCGeMBh39bHEYk1KHqXeYjHuxCLjsWzgygAOKclijR93BJ483hhcWO9kpPV180LIRP8C3B2lK0VgooHRAhbiHYTjxwFjfILRnGCMA8+lZ7XYgmmyhm1Ge5aANevCDUnua34j5ZG5Ekg324Zte7duoCx1S7B4kUhEcM/hZqk/FX+swWDojrivpYDHnuHUjH6z1Wp9QFzMDJSUfWp9mDzCaMkKVmleMnweTV1E7+IPogqoqDCuqAfaD2Dr84l4CJ97IIxb0j1zOG+8GYdAbJitZZv1YPsBiEGfmQ/jc2+IYDj3u+jrTTT9PpaDieu1Bve1viee5mDnCYZWOvHZaTDGZtvd0CUtKXE7KIfJlGaZv9DmWFxfypBKs1FvBG6E3CzcbUshbCReqkSmJiGlcUHeoqT0VFtLLuFARQjPUhaJZ45DHAxkQgFLU2mmWCTTeNkJMIDdHUl5yj6rwWwhjVYqzmo1zHbRLmI+3iF9YuoXi1/dlNp/jepA73b+eETHlLWjQrRPKLvhMHfOMKbWuerijImN/J7YWRWRaya98WTXmMjakKOFO8Pdo99Nzt70hT/pQuWrHftGf7NxumPeiatbxnoezzmTlrll45yC6G3Lu/6p8pVd9d5ou2KJ9LZ9+N5fm5BSVzxq9Jjzl1aEt4e/tja38FRtQou9LOqpp5dPdq5bta1+9vbNX+ZZd2zOfcs18URyS9Uzi5KnXd5TkRv1or3u0/e5npE500LXh7Ver3pCmXpa1L2SOCdj6pGmAxF/fzbizzeBvb6mfVf1MbqDrJ90viUx7uefRrak5D+WOqoo7XSg4ODeqBO/ebovFdRO3Z+jL0ytTnmEONTZVFmYTkfNnTAl7GT+tum3TK9vPdWkn/l91UcH67fc/CV7c+f2y2dvrT53efyEX7N2yEfDbh4Ly7tasSZ38jXr82eoVRbfhZCcxLrtps+dtyPyboztsV6pyXQ/SlX80BoXduTZrlvu/Tnj+BWJRyI94cevtY6v+731g08u/rG4Orz72+pNbXPck8M3TJlxquDgV/LIwI8rs1ZbNiRcH2c9XBx/KMo+6ztHa2gTK1ZW1c+9FBp5Y/m6zNsW09aT647Nw13q6QkNebv460Odj4SE/AtyqB7z
|
||||
@@ -1 +0,0 @@
|
||||
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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
@@ -1 +0,0 @@
|
||||
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
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
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dN972SP/KqafcCW9MfE3e7f+y8b/agwuPrf37JoXputubDp2cXRnK78szlot3gsJKa54ecmXn2yfW71julATevviRzk749etHtlSU3/0tHpS3ow/hWdTq+NTnh58ZfvBBUcvV2Scb7XMulGouim9cm/JjtemDS4Lm9dOhpUFqy/QE4aWHth8+IXNrLrlyvHpSftGzW3JuRr6vPrukszN9W3n7780L7Jl3vKJu5fJdZP2mcP36ytafzV/6TNTr7boznakvFl3P+FmVd6FKHTixOmnli2aGLvwcfG1r5/6+pru1/unsi0t8SPOCM8NWjG7zDMpJl/IO1UlPl13y6ie88m1X/zuXJr3sPd6rnfWv7LPTDk5NNET/Oz6vPhR3pikTW1LY1fn15wbFrucP/jGrmOJFaPj7rau2t+2Lazzb0uIKxltBktO+5o11lGbcuhPf9MUwlfeqfQ0ndplLLAkhjVrynYV76yakHs9c4Jq43n0JpeZ39naOu7K4Huc0FV2b7KFzTftfL3xyFTh28qy48NHVH34BZU0Zu/18ubItcIo/esj04QLKbrfLvuo7kTVZeFgRNWuNUz3rZduP/PhgVub9r186bhj6QJNZ1n6YbrEYjBHR68YrU84f+jFjleiN5fbn18+fRp/1jV+2JBEj22C6XJqyHMxacKgqe6Smux3pSrZ9sX0vJSMisrThk+m1rV3XHr30j9s3zbtOfPayqB/50fMiQu/9qv9Xo1pQor65pg5jqYRHwimr70zPv7Fhajww8MmhBo7m56d0uXYPbJpzKmPT78VnHF9wbX6/dnZ+z7fM8s4/oNV42+v2RD+t5kbgqdHRL46TD3qzu0zmYd2hsXMP998PPWnUds+GXuySWe8eeRA16Vka35428kdf9gxpFqK3WkbN7/b+nrNOxvHxlJznz+5YHJR67MH15lD54dGu375VE7o+49v23px57X2nMoZk/Z+dOeEaU77lht7NkXPGlRTfvq/g8gtd8k5fxLWuis+Gzvee2jrwb9n5r4dX/DmXmP2qpXVh9p19cW3E7+4ZV+Xs8rwx3m59J3Clm/ylhtWl27/pSml4UjE8WNdK2e9UN59496f+c//MSQo6P79IUHd11LTXSOCgv4XTCOo8w==
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
@@ -561,6 +561,16 @@
|
||||
},
|
||||
"resource": {
|
||||
"$ref": "#/components/schemas/ResourceService"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"AWAITING_DATABASE",
|
||||
"READY",
|
||||
"AWAITING_DELETE",
|
||||
"UNKNOWN"
|
||||
],
|
||||
"description": "Deployment status of the project.\n\nNon-terminal statuses: `AWAITING_DATABASE`, `AWAITING_DELETE`. All other statuses are terminal."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+372
-212
@@ -4,20 +4,26 @@ The LangGraph command line interface includes commands to build and run a LangGr
|
||||
|
||||
## Installation
|
||||
|
||||
1. Ensure that Docker is installed (e.g. `docker --version`).
|
||||
2. Install the `langgraph-cli` package:
|
||||
|
||||
=== "pip"
|
||||
```bash
|
||||
pip install langgraph-cli
|
||||
```
|
||||
1. Ensure that Docker is installed (e.g. `docker --version`).
|
||||
2. Install the CLI package:
|
||||
|
||||
=== "Homebrew (MacOS only)"
|
||||
=== "Python"
|
||||
```bash
|
||||
pip install langgraph-cli
|
||||
|
||||
# Install via Homebrew
|
||||
brew install langgraph-cli
|
||||
```
|
||||
|
||||
3. Run the command `langgraph --help` to confirm that the CLI is installed.
|
||||
|
||||
=== "JS"
|
||||
```bash
|
||||
npx @langchain/langgraph-cli
|
||||
|
||||
# Install globally, will be available as `langgraphjs`
|
||||
npm install -g @langchain/langgraph-cli
|
||||
```
|
||||
|
||||
3. Run the command `langgraph --help` or `npx @langchain/langgraph-cli --help` to confirm that the CLI is working correctly.
|
||||
|
||||
[](){#langgraph.json}
|
||||
|
||||
@@ -25,17 +31,6 @@ The LangGraph command line interface includes commands to build and run a LangGr
|
||||
|
||||
The LangGraph CLI requires a JSON configuration file with the following keys:
|
||||
|
||||
| Key | Description |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `dependencies` | **Required**. Array of dependencies for LangGraph Cloud API server. Dependencies can be one of the following: (1) `"."`, which will look for local Python packages, (2) `pyproject.toml`, `setup.py` or `requirements.txt` in the app directory `"./local_package"`, or (3) a package name. |
|
||||
| `graphs` | **Required**. Mapping from graph ID to path where the compiled graph or a function that makes a graph is defined. Example: <ul><li>`./your_package/your_file.py:variable`, where `variable` is an instance of `langgraph.graph.state.CompiledStateGraph`</li><li>`./your_package/your_file.py:make_graph`, where `make_graph` is a function that takes a config dictionary (`langchain_core.runnables.RunnableConfig`) and creates an instance of `langgraph.graph.state.StateGraph` / `langgraph.graph.state.CompiledStateGraph`.</li></ul> |
|
||||
| `auth` | _(Added in v0.0.11)_ Auth configuration containing the path to your authentication handler. Example: `./your_package/auth.py:auth`, where `auth` is an instance of `langgraph_sdk.Auth`. See [authentication guide](../../concepts/auth.md) for details. |
|
||||
| `env` | Path to `.env` file or a mapping from environment variable to its value. |
|
||||
| `store` | Configuration for adding semantic search to the BaseStore. Contains the following fields: <ul><li>`index`: Configuration for semantic search indexing with fields:<ul><li>`embed`: Embedding provider (e.g., "openai:text-embedding-3-small") or path to custom embedding function</li><li>`dims`: Dimension size of the embedding model. Used to initialize the vector table.</li><li>`fields` (optional): List of fields to index. Defaults to `["$"]`, meaningto index entire documents. Can be specific fields like `["text", "summary", "some.value"]`</li></ul></li></ul> |
|
||||
| `python_version` | `3.11` or `3.12`. Defaults to `3.11`. |
|
||||
| `pip_config_file` | Path to `pip` config file. |
|
||||
| `dockerfile_lines` | Array of additional lines to add to Dockerfile following the import from parent image. |
|
||||
|
||||
<div class="admonition tip">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>
|
||||
@@ -43,253 +38,418 @@ The LangGraph CLI requires a JSON configuration file with the following keys:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
=== "Python"
|
||||
|
||||
| Key | Description |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <span style="white-space: nowrap;">`dependencies`</span> | **Required**. Array of dependencies for LangGraph Cloud API server. Dependencies can be one of the following: (1) `"."`, which will look for local Python packages, (2) `pyproject.toml`, `setup.py` or `requirements.txt` in the app directory `"./local_package"`, or (3) a package name. |
|
||||
| <span style="white-space: nowrap;">`graphs`</span> | **Required**. Mapping from graph ID to path where the compiled graph or a function that makes a graph is defined. Example: <ul><li>`./your_package/your_file.py:variable`, where `variable` is an instance of `langgraph.graph.state.CompiledStateGraph`</li><li>`./your_package/your_file.py:make_graph`, where `make_graph` is a function that takes a config dictionary (`langchain_core.runnables.RunnableConfig`) and creates an instance of `langgraph.graph.state.StateGraph` / `langgraph.graph.state.CompiledStateGraph`.</li></ul> |
|
||||
| <span style="white-space: nowrap;">`auth`</span> | _(Added in v0.0.11)_ Auth configuration containing the path to your authentication handler. Example: `./your_package/auth.py:auth`, where `auth` is an instance of `langgraph_sdk.Auth`. See [authentication guide](../../concepts/auth.md) for details. |
|
||||
| <span style="white-space: nowrap;">`env`</span> | Path to `.env` file or a mapping from environment variable to its value. |
|
||||
| <span style="white-space: nowrap;">`store`</span> | Configuration for adding semantic search to the BaseStore. Contains the following fields: <ul><li>`index`: Configuration for semantic search indexing with fields:<ul><li>`embed`: Embedding provider (e.g., "openai:text-embedding-3-small") or path to custom embedding function</li><li>`dims`: Dimension size of the embedding model. Used to initialize the vector table.</li><li>`fields` (optional): List of fields to index. Defaults to `["$"]`, which means to index entire documents. Can be specific fields like `["text", "summary", "some.value"]`</li></ul></li></ul> |
|
||||
| <span style="white-space: nowrap;">`python_version`</span> | `3.11` or `3.12`. Defaults to `3.11`. |
|
||||
| <span style="white-space: nowrap;">`node_version`</span> | Specify `node_version: 20` to use LangGraph.js. |
|
||||
| <span style="white-space: nowrap;">`pip_config_file`</span> | Path to `pip` config file. |
|
||||
| <span style="white-space: nowrap;">`dockerfile_lines`</span> | Array of additional lines to add to Dockerfile following the import from parent image. |
|
||||
|
||||
=== "JS"
|
||||
|
||||
| Key | Description |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <span style="white-space: nowrap;">`graphs`</span> | **Required**. Mapping from graph ID to path where the compiled graph or a function that makes a graph is defined. Example: <ul><li>`./src/graph.ts:variable`, where `variable` is an instance of `CompiledStateGraph`</li><li>`./src/graph.ts:makeGraph`, where `makeGraph` is a function that takes a config dictionary (`LangGraphRunnableConfig`) and creates an instance of `StateGraph` / `CompiledStateGraph`.</li></ul> |
|
||||
| <span style="white-space: nowrap;">`env`</span> | Path to `.env` file or a mapping from environment variable to its value. |
|
||||
| <span style="white-space: nowrap;">`store`</span> | Configuration for adding semantic search to the BaseStore. Contains the following fields: <ul><li>`index`: Configuration for semantic search indexing with fields:<ul><li>`embed`: Embedding provider (e.g., "openai:text-embedding-3-small") or path to custom embedding function</li><li>`dims`: Dimension size of the embedding model. Used to initialize the vector table.</li><li>`fields` (optional): List of fields to index. Defaults to `["$"]`, which means to index entire documents. Can be specific fields like `["text", "summary", "some.value"]`</li></ul></li></ul> |
|
||||
| <span style="white-space: nowrap;">`node_version`</span> | Specify `node_version: 20` to use LangGraph.js. |
|
||||
| <span style="white-space: nowrap;">`dockerfile_lines`</span> | Array of additional lines to add to Dockerfile following the import from parent image. |
|
||||
|
||||
### Examples
|
||||
|
||||
#### Basic Configuration
|
||||
=== "Python"
|
||||
|
||||
#### Basic Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"chat": "./chat/graph.py:graph"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Adding semantic search to the store
|
||||
|
||||
All deployments come with a DB-backed BaseStore. Adding an "index" configuration to your `langgraph.json` will enable [semantic search](../deployment/semantic_search.md) within the BaseStore of your deployment.
|
||||
|
||||
The `fields` configuration determines which parts of your documents to embed:
|
||||
|
||||
- If omitted or set to `["$"]`, the entire document will be embedded
|
||||
- To embed specific fields, use JSON path notation: `["metadata.title", "content.text"]`
|
||||
- Documents missing specified fields will still be stored but won't have embeddings for those fields
|
||||
- You can still override which fields to embed on a specific item at `put` time using the `index` parameter
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"memory_agent": "./agent/graph.py:graph"
|
||||
},
|
||||
"store": {
|
||||
"index": {
|
||||
"embed": "openai:text-embedding-3-small",
|
||||
"dims": 1536,
|
||||
"fields": ["$"]
|
||||
```json
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"chat": "./chat/graph.py:graph"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
!!! note "Common model dimensions"
|
||||
- openai:text-embedding-3-large: 3072
|
||||
- openai:text-embedding-3-small: 1536
|
||||
- openai:text-embedding-ada-002: 1536
|
||||
- cohere:embed-english-v3.0: 1024
|
||||
- cohere:embed-english-light-v3.0: 384
|
||||
- cohere:embed-multilingual-v3.0: 1024
|
||||
- cohere:embed-multilingual-light-v3.0: 384
|
||||
#### Adding semantic search to the store
|
||||
|
||||
#### Semantic search with a custom embedding function
|
||||
All deployments come with a DB-backed BaseStore. Adding an "index" configuration to your `langgraph.json` will enable [semantic search](../deployment/semantic_search.md) within the BaseStore of your deployment.
|
||||
|
||||
If you want to use semantic search with a custom embedding function, you can pass a path to a custom embedding function:
|
||||
The `fields` configuration determines which parts of your documents to embed:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"memory_agent": "./agent/graph.py:graph"
|
||||
},
|
||||
"store": {
|
||||
"index": {
|
||||
"embed": "./embeddings.py:embed_texts",
|
||||
"dims": 768,
|
||||
"fields": ["text", "summary"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- If omitted or set to `["$"]`, the entire document will be embedded
|
||||
- To embed specific fields, use JSON path notation: `["metadata.title", "content.text"]`
|
||||
- Documents missing specified fields will still be stored but won't have embeddings for those fields
|
||||
- You can still override which fields to embed on a specific item at `put` time using the `index` parameter
|
||||
|
||||
The `embed` field in store configuration can reference a custom function that takes a list of strings and returns a list of embeddings. Example implementation:
|
||||
|
||||
```python
|
||||
# embeddings.py
|
||||
def embed_texts(texts: list[str]) -> list[list[float]]:
|
||||
"""Custom embedding function for semantic search."""
|
||||
# Implementation using your preferred embedding model
|
||||
return [[0.1, 0.2, ...] for _ in texts] # dims-dimensional vectors
|
||||
```
|
||||
|
||||
#### Adding custom authentication
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"chat": "./chat/graph.py:graph"
|
||||
},
|
||||
"auth": {
|
||||
"path": "./auth.py:auth",
|
||||
"openapi": {
|
||||
"securitySchemes": {
|
||||
"apiKeyAuth": {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-API-Key"
|
||||
}
|
||||
```json
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"memory_agent": "./agent/graph.py:graph"
|
||||
},
|
||||
"security": [
|
||||
{"apiKeyAuth": []}
|
||||
]
|
||||
},
|
||||
"disable_studio_auth": false
|
||||
}
|
||||
}
|
||||
```
|
||||
"store": {
|
||||
"index": {
|
||||
"embed": "openai:text-embedding-3-small",
|
||||
"dims": 1536,
|
||||
"fields": ["$"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! note "Common model dimensions"
|
||||
- `openai:text-embedding-3-large`: 3072
|
||||
- `openai:text-embedding-3-small`: 1536
|
||||
- `openai:text-embedding-ada-002`: 1536
|
||||
- `cohere:embed-english-v3.0`: 1024
|
||||
- `cohere:embed-english-light-v3.0`: 384
|
||||
- `cohere:embed-multilingual-v3.0`: 1024
|
||||
- `cohere:embed-multilingual-light-v3.0`: 384
|
||||
|
||||
#### Semantic search with a custom embedding function
|
||||
|
||||
If you want to use semantic search with a custom embedding function, you can pass a path to a custom embedding function:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"memory_agent": "./agent/graph.py:graph"
|
||||
},
|
||||
"store": {
|
||||
"index": {
|
||||
"embed": "./embeddings.py:embed_texts",
|
||||
"dims": 768,
|
||||
"fields": ["text", "summary"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `embed` field in store configuration can reference a custom function that takes a list of strings and returns a list of embeddings. Example implementation:
|
||||
|
||||
```python
|
||||
# embeddings.py
|
||||
def embed_texts(texts: list[str]) -> list[list[float]]:
|
||||
"""Custom embedding function for semantic search."""
|
||||
# Implementation using your preferred embedding model
|
||||
return [[0.1, 0.2, ...] for _ in texts] # dims-dimensional vectors
|
||||
```
|
||||
|
||||
#### Adding custom authentication
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"chat": "./chat/graph.py:graph"
|
||||
},
|
||||
"auth": {
|
||||
"path": "./auth.py:auth",
|
||||
"openapi": {
|
||||
"securitySchemes": {
|
||||
"apiKeyAuth": {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-API-Key"
|
||||
}
|
||||
},
|
||||
"security": [{ "apiKeyAuth": [] }]
|
||||
},
|
||||
"disable_studio_auth": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See the [authentication conceptual guide](../../concepts/auth.md) for details, and the [setting up custom authentication](../../tutorials/auth/getting_started.md) guide for a practical walk through of the process.
|
||||
|
||||
|
||||
=== "JS"
|
||||
|
||||
#### Basic Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"graphs": {
|
||||
"chat": "./src/graph.ts:graph"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See the [authentication conceptual guide](../../concepts/auth.md) for details, and the [setting up custom authentication](../../tutorials/auth/getting_started.md) guide for a practical walk through of the process.
|
||||
|
||||
## Commands
|
||||
|
||||
The base command for the LangGraph CLI is `langgraph`.
|
||||
|
||||
**Usage**
|
||||
|
||||
```
|
||||
langgraph [OPTIONS] COMMAND [ARGS]
|
||||
```
|
||||
=== "Python"
|
||||
|
||||
The base command for the LangGraph CLI is `langgraph`.
|
||||
|
||||
```
|
||||
langgraph [OPTIONS] COMMAND [ARGS]
|
||||
```
|
||||
=== "JS"
|
||||
|
||||
The base command for the LangGraph.js CLI is `langgraphjs`.
|
||||
|
||||
```
|
||||
npx @langchain/langgraph-cli [OPTIONS] COMMAND [ARGS]
|
||||
```
|
||||
|
||||
We recommend using `npx` to always use the latest version of the CLI.
|
||||
|
||||
### `dev`
|
||||
|
||||
Run LangGraph API server in development mode with hot reloading and debugging capabilities. This lightweight server requires no Docker installation and is suitable for development and testing. State is persisted to a local directory.
|
||||
=== "Python"
|
||||
|
||||
!!! note "Python only"
|
||||
Run LangGraph API server in development mode with hot reloading and debugging capabilities. This lightweight server requires no Docker installation and is suitable for development and testing. State is persisted to a local directory.
|
||||
|
||||
Currently, the CLI only supports Python >= 3.11.
|
||||
JS support is coming soon.
|
||||
!!! note
|
||||
|
||||
**Installation**
|
||||
Currently, the CLI only supports Python >= 3.11.
|
||||
|
||||
This command requires the "inmem" extra to be installed:
|
||||
**Installation**
|
||||
|
||||
```bash
|
||||
pip install -U "langgraph-cli[inmem]"
|
||||
```
|
||||
This command requires the "inmem" extra to be installed:
|
||||
|
||||
**Usage**
|
||||
```bash
|
||||
pip install -U "langgraph-cli[inmem]"
|
||||
```
|
||||
|
||||
```
|
||||
langgraph dev [OPTIONS]
|
||||
```
|
||||
**Usage**
|
||||
|
||||
**Options**
|
||||
```
|
||||
langgraph dev [OPTIONS]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| ----------------------------- | ---------------- | ----------------------------------------------------------------------------------- |
|
||||
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables |
|
||||
| `--host TEXT` | `127.0.0.1` | Host to bind the server to |
|
||||
| `--port INTEGER` | `2024` | Port to bind the server to |
|
||||
| `--no-reload` | | Disable auto-reload |
|
||||
| `--n-jobs-per-worker INTEGER` | | Number of jobs per worker. Default is 10 |
|
||||
| `--no-browser` | | Disable automatic browser opening |
|
||||
| `--debug-port INTEGER` | | Port for debugger to listen on |
|
||||
| `--help` | | Display command documentation |
|
||||
**Options**
|
||||
|
||||
| Option | Default | Description |
|
||||
| ----------------------------- | ---------------- | ----------------------------------------------------------------------------------- |
|
||||
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables |
|
||||
| `--host TEXT` | `127.0.0.1` | Host to bind the server to |
|
||||
| `--port INTEGER` | `2024` | Port to bind the server to |
|
||||
| `--no-reload` | | Disable auto-reload |
|
||||
| `--n-jobs-per-worker INTEGER` | | Number of jobs per worker. Default is 10 |
|
||||
| `--debug-port INTEGER` | | Port for debugger to listen on |
|
||||
| `--help` | | Display command documentation |
|
||||
|
||||
|
||||
=== "JS"
|
||||
|
||||
Run LangGraph API server in development mode with hot reloading capabilities. This lightweight server requires no Docker installation and is suitable for development and testing. State is persisted to a local directory.
|
||||
|
||||
**Usage**
|
||||
|
||||
```
|
||||
npx @langchain/langgraph-cli dev [OPTIONS]
|
||||
```
|
||||
|
||||
**Options**
|
||||
|
||||
| Option | Default | Description |
|
||||
| ----------------------------- | ---------------- | ----------------------------------------------------------------------------------- |
|
||||
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables |
|
||||
| `--host TEXT` | `127.0.0.1` | Host to bind the server to |
|
||||
| `--port INTEGER` | `2024` | Port to bind the server to |
|
||||
| `--no-reload` | | Disable auto-reload |
|
||||
| `--n-jobs-per-worker INTEGER` | | Number of jobs per worker. Default is 10 |
|
||||
| `--debug-port INTEGER` | | Port for debugger to listen on |
|
||||
| `--help` | | Display command documentation |
|
||||
|
||||
### `build`
|
||||
|
||||
Build LangGraph Cloud API server Docker image.
|
||||
=== "Python"
|
||||
|
||||
**Usage**
|
||||
Build LangGraph Cloud API server Docker image.
|
||||
|
||||
```
|
||||
langgraph build [OPTIONS]
|
||||
```
|
||||
**Usage**
|
||||
|
||||
**Options**
|
||||
```
|
||||
langgraph build [OPTIONS]
|
||||
```
|
||||
|
||||
**Options**
|
||||
|
||||
| Option | Default | Description |
|
||||
| -------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--platform TEXT` | | Target platform(s) to build the Docker image for. Example: `langgraph build --platform linux/amd64,linux/arm64` |
|
||||
| `-t, --tag TEXT` | | **Required**. Tag for the Docker image. Example: `langgraph build -t my-image` |
|
||||
| `--pull / --no-pull` | `--pull` | Build with latest remote Docker image. Use `--no-pull` for running the LangGraph Cloud API server with locally built images. |
|
||||
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables. |
|
||||
| `--help` | | Display command documentation. |
|
||||
|
||||
=== "JS"
|
||||
|
||||
Build LangGraph Cloud API server Docker image.
|
||||
|
||||
**Usage**
|
||||
|
||||
```
|
||||
npx @langchain/langgraph-cli build [OPTIONS]
|
||||
```
|
||||
|
||||
**Options**
|
||||
|
||||
| Option | Default | Description |
|
||||
| -------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--platform TEXT` | | Target platform(s) to build the Docker image for. Example: `langgraph build --platform linux/amd64,linux/arm64` |
|
||||
| `-t, --tag TEXT` | | **Required**. Tag for the Docker image. Example: `langgraph build -t my-image` |
|
||||
| `--no-pull` | | Use locally built images. Defaults to `false` to build with latest remote Docker image. |
|
||||
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables. |
|
||||
| `--help` | | Display command documentation. |
|
||||
|
||||
| Option | Default | Description |
|
||||
| -------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--platform TEXT` | | Target platform(s) to build the Docker image for. Example: `langgraph build --platform linux/amd64,linux/arm64` |
|
||||
| `-t, --tag TEXT` | | **Required**. Tag for the Docker image. Example: `langgraph build -t my-image` |
|
||||
| `--pull / --no-pull` | `--pull` | Build with latest remote Docker image. Use `--no-pull` for running the LangGraph Cloud API server with locally built images. |
|
||||
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables. |
|
||||
| `--help` | | Display command documentation. |
|
||||
|
||||
### `up`
|
||||
|
||||
Start LangGraph API server. For local testing, requires a LangSmith API key with access to LangGraph Cloud closed beta. Requires a license key for production use.
|
||||
=== "Python"
|
||||
|
||||
**Usage**
|
||||
Start LangGraph API server. For local testing, requires a LangSmith API key with access to LangGraph Cloud closed beta. Requires a license key for production use.
|
||||
|
||||
```
|
||||
langgraph up [OPTIONS]
|
||||
```
|
||||
**Usage**
|
||||
|
||||
**Options**
|
||||
```
|
||||
langgraph up [OPTIONS]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| ---------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--wait` | | Wait for services to start before returning. Implies --detach |
|
||||
| `--postgres-uri TEXT` | Local database | Postgres URI to use for the database. |
|
||||
| `--watch` | | Restart on file changes |
|
||||
| `--debugger-base-url TEXT` | `http://127.0.0.1:[PORT]` | URL used by the debugger to access LangGraph API. |
|
||||
| `--debugger-port INTEGER` | | Pull the debugger image locally and serve the UI on specified port |
|
||||
| `--verbose` | | Show more output from the server logs. |
|
||||
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables. |
|
||||
| `-d, --docker-compose FILE` | | Path to docker-compose.yml file with additional services to launch. |
|
||||
| `-p, --port INTEGER` | `8123` | Port to expose. Example: `langgraph up --port 8000` |
|
||||
| `--pull / --no-pull` | `pull` | Pull latest images. Use `--no-pull` for running the server with locally-built images. Example: `langgraph up --no-pull` |
|
||||
| `--recreate / --no-recreate` | `no-recreate` | Recreate containers even if their configuration and image haven't changed |
|
||||
| `--help` | | Display command documentation. |
|
||||
**Options**
|
||||
|
||||
| Option | Default | Description |
|
||||
| ---------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--wait` | | Wait for services to start before returning. Implies --detach |
|
||||
| `--postgres-uri TEXT` | Local database | Postgres URI to use for the database. |
|
||||
| `--watch` | | Restart on file changes |
|
||||
| `--debugger-base-url TEXT` | `http://127.0.0.1:[PORT]` | URL used by the debugger to access LangGraph API. |
|
||||
| `--debugger-port INTEGER` | | Pull the debugger image locally and serve the UI on specified port |
|
||||
| `--verbose` | | Show more output from the server logs. |
|
||||
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables. |
|
||||
| `-d, --docker-compose FILE` | | Path to docker-compose.yml file with additional services to launch. |
|
||||
| `-p, --port INTEGER` | `8123` | Port to expose. Example: `langgraph up --port 8000` |
|
||||
| `--pull / --no-pull` | `pull` | Pull latest images. Use `--no-pull` for running the server with locally-built images. Example: `langgraph up --no-pull` |
|
||||
| `--recreate / --no-recreate` | `no-recreate` | Recreate containers even if their configuration and image haven't changed |
|
||||
| `--help` | | Display command documentation. |
|
||||
|
||||
=== "JS"
|
||||
|
||||
Start LangGraph API server. For local testing, requires a LangSmith API key with access to LangGraph Cloud closed beta. Requires a license key for production use.
|
||||
|
||||
**Usage**
|
||||
|
||||
```
|
||||
npx @langchain/langgraph-cli up [OPTIONS]
|
||||
```
|
||||
|
||||
**Options**
|
||||
|
||||
| Option | Default | Description |
|
||||
| ---------------------------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| <span style="white-space: nowrap;">`--wait`</span> | | Wait for services to start before returning. Implies --detach |
|
||||
| <span style="white-space: nowrap;">`--postgres-uri TEXT`</span> | Local database | Postgres URI to use for the database. |
|
||||
| <span style="white-space: nowrap;">`--watch`</span> | | Restart on file changes |
|
||||
| <span style="white-space: nowrap;">`-c, --config FILE`</span> | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables. |
|
||||
| <span style="white-space: nowrap;">`-d, --docker-compose FILE`</span> | | Path to docker-compose.yml file with additional services to launch. |
|
||||
| <span style="white-space: nowrap;">`-p, --port INTEGER`</span> | `8123` | Port to expose. Example: `langgraph up --port 8000` |
|
||||
| <span style="white-space: nowrap;">`--no-pull`</span> | | Use locally built images. Defaults to `false` to build with latest remote Docker image. |
|
||||
| <span style="white-space: nowrap;">`--recreate`</span> | | Recreate containers even if their configuration and image haven't changed |
|
||||
| <span style="white-space: nowrap;">`--help`</span> | | Display command documentation. |
|
||||
|
||||
### `dockerfile`
|
||||
|
||||
Generate a Dockerfile for building a LangGraph Cloud API server Docker image.
|
||||
=== "Python"
|
||||
|
||||
**Usage**
|
||||
Generate a Dockerfile for building a LangGraph Cloud API server Docker image.
|
||||
|
||||
```
|
||||
langgraph dockerfile [OPTIONS] SAVE_PATH
|
||||
```
|
||||
**Usage**
|
||||
|
||||
**Options**
|
||||
```
|
||||
langgraph dockerfile [OPTIONS] SAVE_PATH
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `-c, --config FILE` | `langgraph.json` | Path to the [configuration file](#configuration-file) declaring dependencies, graphs and environment variables. |
|
||||
| `--help` | | Show this message and exit. |
|
||||
**Options**
|
||||
|
||||
Example:
|
||||
| Option | Default | Description |
|
||||
| ------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `-c, --config FILE` | `langgraph.json` | Path to the [configuration file](#configuration-file) declaring dependencies, graphs and environment variables. |
|
||||
| `--help` | | Show this message and exit. |
|
||||
|
||||
```bash
|
||||
langgraph dockerfile -c langgraph.json Dockerfile
|
||||
```
|
||||
Example:
|
||||
|
||||
This generates a Dockerfile that looks similar to:
|
||||
```bash
|
||||
langgraph dockerfile -c langgraph.json Dockerfile
|
||||
```
|
||||
|
||||
```dockerfile
|
||||
FROM langchain/langgraph-api:3.11
|
||||
This generates a Dockerfile that looks similar to:
|
||||
|
||||
ADD ./pipconf.txt /pipconfig.txt
|
||||
```dockerfile
|
||||
FROM langchain/langgraph-api:3.11
|
||||
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt langchain_community langchain_anthropic langchain_openai wikipedia scikit-learn
|
||||
ADD ./pipconf.txt /pipconfig.txt
|
||||
|
||||
ADD ./graphs /deps/__outer_graphs/src
|
||||
RUN set -ex && \
|
||||
for line in '[project]' \
|
||||
'name = "graphs"' \
|
||||
'version = "0.1"' \
|
||||
'[tool.setuptools.package-data]' \
|
||||
'"*" = ["**/*"]'; do \
|
||||
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \
|
||||
done
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt langchain_community langchain_anthropic langchain_openai wikipedia scikit-learn
|
||||
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
ADD ./graphs /deps/__outer_graphs/src
|
||||
RUN set -ex && \
|
||||
for line in '[project]' \
|
||||
'name = "graphs"' \
|
||||
'version = "0.1"' \
|
||||
'[tool.setuptools.package-data]' \
|
||||
'"*" = ["**/*"]'; do \
|
||||
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \
|
||||
done
|
||||
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph", "storm": "/deps/__outer_graphs/src/storm.py:graph"}'
|
||||
```
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
|
||||
???+ note "Updating your langgraph.json file"
|
||||
The `langgraph dockerfile` command translates all the configuration in your `langgraph.json` file into Dockerfile commands. When using this command, you will have to re-run it whenever you update your `langgraph.json` file. Otherwise, your changes will not be reflected when you build or run the dockerfile.
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph", "storm": "/deps/__outer_graphs/src/storm.py:graph"}'
|
||||
```
|
||||
|
||||
???+ note "Updating your langgraph.json file"
|
||||
The `langgraph dockerfile` command translates all the configuration in your `langgraph.json` file into Dockerfile commands. When using this command, you will have to re-run it whenever you update your `langgraph.json` file. Otherwise, your changes will not be reflected when you build or run the dockerfile.
|
||||
|
||||
=== "JS"
|
||||
|
||||
Generate a Dockerfile for building a LangGraph Cloud API server Docker image.
|
||||
|
||||
**Usage**
|
||||
|
||||
```
|
||||
npx @langchain/langgraph-cli dockerfile [OPTIONS] SAVE_PATH
|
||||
```
|
||||
|
||||
**Options**
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `-c, --config FILE` | `langgraph.json` | Path to the [configuration file](#configuration-file) declaring dependencies, graphs and environment variables. |
|
||||
| `--help` | | Show this message and exit. |
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
npx @langchain/langgraph-cli dockerfile -c langgraph.json Dockerfile
|
||||
```
|
||||
|
||||
This generates a Dockerfile that looks similar to:
|
||||
|
||||
```dockerfile
|
||||
FROM langchain/langgraphjs-api:20
|
||||
|
||||
ADD . /deps/agent
|
||||
|
||||
RUN cd /deps/agent && yarn install
|
||||
|
||||
ENV LANGSERVE_GRAPHS='{"agent":"./src/react_agent/graph.ts:graph"}'
|
||||
|
||||
WORKDIR /deps/agent
|
||||
|
||||
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts
|
||||
```
|
||||
|
||||
???+ note "Updating your langgraph.json file"
|
||||
The `npx @langchain/langgraph-cli dockerfile` command translates all the configuration in your `langgraph.json` file into Dockerfile commands. When using this command, you will have to re-run it whenever you update your `langgraph.json` file. Otherwise, your changes will not be reflected when you build or run the dockerfile.
|
||||
|
||||
@@ -57,6 +57,7 @@ With a Self-Hosted Lite deployment, you are responsible for managing the infrast
|
||||
|
||||
You’ll build a Docker image using the [LangGraph CLI](./langgraph_cli.md), which can then be deployed on your own infrastructure.
|
||||
|
||||
[Cron jobs](../cloud/how-tos/cron_jobs.md) are not available for Self-Hosted Lite deployments.
|
||||
|
||||
For more information, please see:
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
::: langgraph.func
|
||||
options:
|
||||
members:
|
||||
- task
|
||||
- entrypoint
|
||||
@@ -6,7 +6,7 @@ support it.
|
||||
One way this can occur is if you are using a [fanout](https://langchain-ai.github.io/langgraph/how-tos/map-reduce/)
|
||||
or other parallel execution in your graph and you have defined a graph like this:
|
||||
|
||||
```python
|
||||
```python hl_lines="2"
|
||||
class State(TypedDict):
|
||||
some_key: str
|
||||
|
||||
@@ -31,7 +31,7 @@ there is uncertainty around how to update the internal state.
|
||||
|
||||
To get around this, you can define a reducer that combines multiple values:
|
||||
|
||||
```python
|
||||
```python hl_lines="5-6"
|
||||
import operator
|
||||
from typing import Annotated
|
||||
|
||||
|
||||
+537
-1229
File diff suppressed because one or more lines are too long
@@ -83,7 +83,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"execution_count": 2,
|
||||
"id": "f04c6778-403b-4b49-9b93-678e910d5cec",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -126,7 +126,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"execution_count": 3,
|
||||
"id": "df2bd80b-c477-4d74-8faa-1c0548622239",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -162,7 +162,11 @@
|
||||
"llm = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def supervisor_node(state: MessagesState) -> Command[Literal[*members, \"__end__\"]]:\n",
|
||||
"class State(MessagesState):\n",
|
||||
" next: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def supervisor_node(state: State) -> Command[Literal[*members, \"__end__\"]]:\n",
|
||||
" messages = [\n",
|
||||
" {\"role\": \"system\", \"content\": system_prompt},\n",
|
||||
" ] + state[\"messages\"]\n",
|
||||
@@ -171,7 +175,7 @@
|
||||
" if goto == \"FINISH\":\n",
|
||||
" goto = END\n",
|
||||
"\n",
|
||||
" return Command(goto=goto)"
|
||||
" return Command(goto=goto, update={\"next\": goto})"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -201,7 +205,7 @@
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def research_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
"def research_node(state: State) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
" result = research_agent.invoke(state)\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
@@ -217,7 +221,7 @@
|
||||
"code_agent = create_react_agent(llm, tools=[python_repl_tool])\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def code_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
"def code_node(state: State) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
" result = code_agent.invoke(state)\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
@@ -229,7 +233,7 @@
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"builder = StateGraph(MessagesState)\n",
|
||||
"builder = StateGraph(State)\n",
|
||||
"builder.add_edge(START, \"supervisor\")\n",
|
||||
"builder.add_node(\"supervisor\", supervisor_node)\n",
|
||||
"builder.add_node(\"researcher\", research_node)\n",
|
||||
|
||||
@@ -293,6 +293,10 @@
|
||||
"from langchain_core.messages import HumanMessage, trim_messages\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class State(MessagesState):\n",
|
||||
" next: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def make_supervisor_node(llm: BaseChatModel, members: list[str]) -> str:\n",
|
||||
" options = [\"FINISH\"] + members\n",
|
||||
" system_prompt = (\n",
|
||||
@@ -308,7 +312,7 @@
|
||||
"\n",
|
||||
" next: Literal[*options]\n",
|
||||
"\n",
|
||||
" def supervisor_node(state: MessagesState) -> Command[Literal[*members, \"__end__\"]]:\n",
|
||||
" def supervisor_node(state: State) -> Command[Literal[*members, \"__end__\"]]:\n",
|
||||
" \"\"\"An LLM-based router.\"\"\"\n",
|
||||
" messages = [\n",
|
||||
" {\"role\": \"system\", \"content\": system_prompt},\n",
|
||||
@@ -318,7 +322,7 @@
|
||||
" if goto == \"FINISH\":\n",
|
||||
" goto = END\n",
|
||||
"\n",
|
||||
" return Command(goto=goto)\n",
|
||||
" return Command(goto=goto, update={\"next\": goto})\n",
|
||||
"\n",
|
||||
" return supervisor_node"
|
||||
]
|
||||
@@ -358,7 +362,7 @@
|
||||
"search_agent = create_react_agent(llm, tools=[tavily_tool])\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def search_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
"def search_node(state: State) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
" result = search_agent.invoke(state)\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
@@ -374,7 +378,7 @@
|
||||
"web_scraper_agent = create_react_agent(llm, tools=[scrape_webpages])\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def web_scraper_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
"def web_scraper_node(state: State) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
" result = web_scraper_agent.invoke(state)\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
@@ -410,7 +414,7 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"research_builder = StateGraph(MessagesState)\n",
|
||||
"research_builder = StateGraph(State)\n",
|
||||
"research_builder.add_node(\"supervisor\", research_supervisor_node)\n",
|
||||
"research_builder.add_node(\"search\", search_node)\n",
|
||||
"research_builder.add_node(\"web_scraper\", web_scraper_node)\n",
|
||||
@@ -528,7 +532,7 @@
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def doc_writing_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
"def doc_writing_node(state: State) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
" result = doc_writer_agent.invoke(state)\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
@@ -551,7 +555,7 @@
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def note_taking_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
"def note_taking_node(state: State) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
" result = note_taking_agent.invoke(state)\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
@@ -569,7 +573,7 @@
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def chart_generating_node(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
"def chart_generating_node(state: State) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
" result = chart_generating_agent.invoke(state)\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
@@ -610,7 +614,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Create the graph here\n",
|
||||
"paper_writing_builder = StateGraph(MessagesState)\n",
|
||||
"paper_writing_builder = StateGraph(State)\n",
|
||||
"paper_writing_builder.add_node(\"supervisor\", doc_writing_supervisor_node)\n",
|
||||
"paper_writing_builder.add_node(\"doc_writer\", doc_writing_node)\n",
|
||||
"paper_writing_builder.add_node(\"note_taker\", note_taking_node)\n",
|
||||
@@ -730,7 +734,7 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def call_research_team(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
"def call_research_team(state: State) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
" response = research_graph.invoke({\"messages\": state[\"messages\"][-1]})\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
@@ -744,7 +748,7 @@
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def call_paper_writing_team(state: MessagesState) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
"def call_paper_writing_team(state: State) -> Command[Literal[\"supervisor\"]]:\n",
|
||||
" response = paper_writing_graph.invoke({\"messages\": state[\"messages\"][-1]})\n",
|
||||
" return Command(\n",
|
||||
" update={\n",
|
||||
@@ -759,7 +763,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define the graph.\n",
|
||||
"super_builder = StateGraph(MessagesState)\n",
|
||||
"super_builder = StateGraph(State)\n",
|
||||
"super_builder.add_node(\"supervisor\", teams_supervisor_node)\n",
|
||||
"super_builder.add_node(\"research_team\", call_research_team)\n",
|
||||
"super_builder.add_node(\"writing_team\", call_paper_writing_team)\n",
|
||||
|
||||
@@ -20,6 +20,7 @@ theme:
|
||||
- content.action.edit
|
||||
- content.tooltips
|
||||
- header.autohide
|
||||
- navigation.indexes
|
||||
- navigation.expand
|
||||
- navigation.footer
|
||||
- navigation.instant
|
||||
@@ -368,6 +369,7 @@ nav:
|
||||
- Errors: reference/errors.md
|
||||
- Types: reference/types.md
|
||||
- Constants: reference/constants.md
|
||||
- Functional API: reference/func.md
|
||||
- LangGraph Platform:
|
||||
- Server API: "cloud/reference/api/api_ref.md"
|
||||
- CLI: "cloud/reference/cli.md"
|
||||
|
||||
@@ -39,14 +39,14 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
"""Asynchronous Postgres-backed store with optional vector search using pgvector.
|
||||
|
||||
!!! example "Examples"
|
||||
Basic setup and key-value storage:
|
||||
Basic setup and usage:
|
||||
```python
|
||||
from langgraph.store.postgres import AsyncPostgresStore
|
||||
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
"postgresql://user:pass@localhost:5432/dbname"
|
||||
) as store:
|
||||
await store.setup()
|
||||
conn_string = "postgresql://user:pass@localhost:5432/dbname"
|
||||
|
||||
async with AsyncPostgresStore.from_conn_string(conn_string) as store:
|
||||
await store.setup() # Run migrations. Done once
|
||||
|
||||
# Store and retrieve data
|
||||
await store.aput(("users", "123"), "prefs", {"theme": "dark"})
|
||||
@@ -58,38 +58,41 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
from langchain.embeddings import init_embeddings
|
||||
from langgraph.store.postgres import AsyncPostgresStore
|
||||
|
||||
conn_string = "postgresql://user:pass@localhost:5432/dbname"
|
||||
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
"postgresql://user:pass@localhost:5432/dbname",
|
||||
conn_string,
|
||||
index={
|
||||
"dims": 1536,
|
||||
"embed": init_embeddings("openai:text-embedding-3-small"),
|
||||
"fields": ["text"] # specify which fields to embed. Default is the whole serialized value
|
||||
}
|
||||
) as store:
|
||||
await store.setup() # Do this once to run migrations
|
||||
await store.setup() # Run migrations. Done once
|
||||
|
||||
# Store documents
|
||||
await store.aput(("docs",), "doc1", {"text": "Python tutorial"})
|
||||
await store.aput(("docs",), "doc2", {"text": "TypeScript guide"})
|
||||
# Don't index the following
|
||||
await store.aput(("docs",), "doc3", {"text": "Other guide"}, index=False)
|
||||
await store.aput(("docs",), "doc3", {"text": "Other guide"}, index=False) # don't index
|
||||
|
||||
# Search by similarity
|
||||
results = await store.asearch(("docs",), query="python programming")
|
||||
results = await store.asearch(("docs",), "programming guides", limit=2)
|
||||
```
|
||||
|
||||
Using connection pooling for better performance:
|
||||
```python
|
||||
from langgraph.store.postgres import AsyncPostgresStore, PoolConfig
|
||||
|
||||
conn_string = "postgresql://user:pass@localhost:5432/dbname"
|
||||
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
"postgresql://user:pass@localhost:5432/dbname",
|
||||
conn_string,
|
||||
pool_config=PoolConfig(
|
||||
min_size=5,
|
||||
max_size=20
|
||||
)
|
||||
) as store:
|
||||
await store.setup()
|
||||
await store.setup() # Run migrations. Done once
|
||||
# Use store with connection pooling...
|
||||
```
|
||||
|
||||
@@ -102,7 +105,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
Note:
|
||||
Semantic search is disabled by default. You can enable it by providing an `index` configuration
|
||||
when creating the store. Without this configuration, all `index` arguments passed to
|
||||
`put` or `aput`will have no effect.
|
||||
`put` or `aput` will have no effect.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
|
||||
@@ -536,18 +536,35 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
||||
"""Postgres-backed store with optional vector search using pgvector.
|
||||
|
||||
!!! example "Examples"
|
||||
Basic setup and key-value storage:
|
||||
Basic setup and usage:
|
||||
```python
|
||||
from langgraph.store.postgres import PostgresStore
|
||||
from psycopg import Connection
|
||||
|
||||
conn_string = "postgresql://user:pass@localhost:5432/dbname"
|
||||
|
||||
# Using direct connection
|
||||
with Connection.connect(conn_string) as conn:
|
||||
store = PostgresStore(conn)
|
||||
store.setup() # Run migrations. Done once
|
||||
|
||||
# Store and retrieve data
|
||||
store.put(("users", "123"), "prefs", {"theme": "dark"})
|
||||
item = store.get(("users", "123"), "prefs")
|
||||
```
|
||||
|
||||
Or using the convenient from_conn_string helper:
|
||||
```python
|
||||
from langgraph.store.postgres import PostgresStore
|
||||
|
||||
store = PostgresStore(
|
||||
connection_string="postgresql://user:pass@localhost:5432/dbname"
|
||||
)
|
||||
store.setup()
|
||||
conn_string = "postgresql://user:pass@localhost:5432/dbname"
|
||||
|
||||
# Store and retrieve data
|
||||
store.put(("users", "123"), "prefs", {"theme": "dark"})
|
||||
item = store.get(("users", "123"), "prefs")
|
||||
with PostgresStore.from_conn_string(conn_string) as store:
|
||||
store.setup()
|
||||
|
||||
# Store and retrieve data
|
||||
store.put(("users", "123"), "prefs", {"theme": "dark"})
|
||||
item = store.get(("users", "123"), "prefs")
|
||||
```
|
||||
|
||||
Vector search using LangChain embeddings:
|
||||
@@ -555,23 +572,25 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
||||
from langchain.embeddings import init_embeddings
|
||||
from langgraph.store.postgres import PostgresStore
|
||||
|
||||
store = PostgresStore(
|
||||
connection_string="postgresql://user:pass@localhost:5432/dbname",
|
||||
conn_string = "postgresql://user:pass@localhost:5432/dbname"
|
||||
|
||||
with PostgresStore.from_conn_string(
|
||||
conn_string,
|
||||
index={
|
||||
"dims": 1536,
|
||||
"embed": init_embeddings("openai:text-embedding-3-small"),
|
||||
"fields": ["text"] # specify which fields to embed. Default is the whole serialized value
|
||||
}
|
||||
)
|
||||
store.setup() # Do this once to run migrations
|
||||
) as store:
|
||||
store.setup() # Do this once to run migrations
|
||||
|
||||
# Store documents
|
||||
store.put(("docs",), "doc1", {"text": "Python tutorial"})
|
||||
store.put(("docs",), "doc2", {"text": "TypeScript guide"})
|
||||
store.put(("docs",), "doc2", {"text": "Other guide"}, index=False) # don't index
|
||||
# Store documents
|
||||
store.put(("docs",), "doc1", {"text": "Python tutorial"})
|
||||
store.put(("docs",), "doc2", {"text": "TypeScript guide"})
|
||||
store.put(("docs",), "doc2", {"text": "Other guide"}, index=False) # don't index
|
||||
|
||||
# Search by similarity
|
||||
results = store.search(("docs",), query="python programming")
|
||||
# Search by similarity
|
||||
results = store.search(("docs",), "programming guides", limit=2)
|
||||
```
|
||||
|
||||
Note:
|
||||
|
||||
@@ -605,6 +605,11 @@ def dev(
|
||||
) from None
|
||||
|
||||
config_json = langgraph_cli.config.validate_config_file(pathlib.Path(config))
|
||||
if config_json.get("node_version"):
|
||||
raise click.UsageError(
|
||||
"In-mem server for JS graphs is not supported in this version of the LangGraph CLI. Please use `npx @langchain/langgraph-cli` instead."
|
||||
) from None
|
||||
|
||||
cwd = os.getcwd()
|
||||
sys.path.append(cwd)
|
||||
dependencies = config_json.get("dependencies", [])
|
||||
|
||||
@@ -469,10 +469,11 @@ def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image:
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
npm, yarn, pnpm = [
|
||||
npm, yarn, pnpm, bun = [
|
||||
test_file("package-lock.json"),
|
||||
test_file("yarn.lock"),
|
||||
test_file("pnpm-lock.yaml"),
|
||||
test_file("bun.lockb"),
|
||||
]
|
||||
|
||||
if yarn:
|
||||
@@ -481,6 +482,8 @@ def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image:
|
||||
install_cmd = "pnpm i --frozen-lockfile"
|
||||
elif npm:
|
||||
install_cmd = "npm ci"
|
||||
elif bun:
|
||||
install_cmd = "bun i"
|
||||
else:
|
||||
install_cmd = "npm i"
|
||||
store_config = config.get("store")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-cli"
|
||||
version = "0.1.67"
|
||||
version = "0.1.68"
|
||||
description = "CLI for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
+57
-14
@@ -47,9 +47,7 @@ pip install -U langgraph
|
||||
|
||||
## Example
|
||||
|
||||
One of the central concepts of LangGraph is state. Each graph execution creates a state that is passed between nodes in the graph as they execute, and each node updates this internal state with its return value after it executes. The way that the graph updates its internal state is defined by either the type of graph chosen or a custom function.
|
||||
|
||||
Let's take a look at a simple example of an agent that can use a search tool.
|
||||
Let's build a tool-calling [ReAct-style](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#react-implementation) agent that uses a search tool!
|
||||
|
||||
```shell
|
||||
pip install langchain-anthropic
|
||||
@@ -66,10 +64,58 @@ export LANGSMITH_TRACING=true
|
||||
export LANGSMITH_API_KEY=lsv2_sk_...
|
||||
```
|
||||
|
||||
```python
|
||||
from typing import Annotated, Literal, TypedDict
|
||||
The simplest way to create a tool-calling agent in LangGraph is to use `create_react_agent`:
|
||||
|
||||
<details open>
|
||||
<summary>High-level implementation</summary>
|
||||
|
||||
```python
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.tools import tool
|
||||
|
||||
# Define the tools for the agent to use
|
||||
@tool
|
||||
def search(query: str):
|
||||
"""Call to surf the web."""
|
||||
# This is a placeholder, but don't tell the LLM that...
|
||||
if "sf" in query.lower() or "san francisco" in query.lower():
|
||||
return "It's 60 degrees and foggy."
|
||||
return "It's 90 degrees and sunny."
|
||||
|
||||
|
||||
tools = [search]
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0)
|
||||
|
||||
# Initialize memory to persist state between graph runs
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
app = create_react_agent(model, tools, checkpointer=checkpointer)
|
||||
|
||||
# Use the agent
|
||||
final_state = app.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
config={"configurable": {"thread_id": 42}}
|
||||
)
|
||||
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?"
|
||||
```
|
||||
</details>
|
||||
|
||||
> [!TIP]
|
||||
> LangGraph is a **low-level** framework that allows you to implement any custom agent
|
||||
architectures. Click on the low-level implementation below to see how to implement a
|
||||
tool-calling agent from scratch.
|
||||
|
||||
<details>
|
||||
<summary>Low-level implementation</summary>
|
||||
|
||||
```python
|
||||
from typing import Literal
|
||||
|
||||
from langchain_core.messages import HumanMessage
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.tools import tool
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
@@ -91,7 +137,7 @@ tools = [search]
|
||||
|
||||
tool_node = ToolNode(tools)
|
||||
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-20240620", temperature=0).bind_tools(tools)
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0).bind_tools(tools)
|
||||
|
||||
# Define the function that determines whether to continue or not
|
||||
def should_continue(state: MessagesState) -> Literal["tools", END]:
|
||||
@@ -145,23 +191,20 @@ checkpointer = MemorySaver()
|
||||
# Note that we're (optionally) passing the memory when compiling the graph
|
||||
app = workflow.compile(checkpointer=checkpointer)
|
||||
|
||||
# Use the Runnable
|
||||
# Use the agent
|
||||
final_state = app.invoke(
|
||||
{"messages": [HumanMessage(content="what is the weather in sf")]},
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
config={"configurable": {"thread_id": 42}}
|
||||
)
|
||||
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?"
|
||||
```
|
||||
</details>
|
||||
|
||||
Now when we pass the same `"thread_id"`, the conversation context is retained via the saved state (i.e. stored list of messages)
|
||||
|
||||
```python
|
||||
final_state = app.invoke(
|
||||
{"messages": [HumanMessage(content="what about ny")]},
|
||||
{"messages": [{"role": "user", "content": "what about ny"}]},
|
||||
config={"configurable": {"thread_id": 42}}
|
||||
)
|
||||
final_state["messages"][-1].content
|
||||
|
||||
@@ -78,6 +78,8 @@ 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")
|
||||
# holds the previous return value from a stateful Pregel graph.
|
||||
|
||||
# --- Other constants ---
|
||||
PUSH = sys.intern("__pregel_push")
|
||||
|
||||
@@ -4,6 +4,7 @@ import concurrent.futures
|
||||
import functools
|
||||
import inspect
|
||||
import types
|
||||
from collections.abc import Iterator
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
@@ -14,6 +15,9 @@ from typing import (
|
||||
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
|
||||
@@ -22,6 +26,7 @@ from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.constants import CONF, END, 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.read import PregelNode
|
||||
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.store.base import BaseStore
|
||||
@@ -81,6 +86,67 @@ def task(
|
||||
Callable[P, asyncio.Future[T]],
|
||||
Callable[P, concurrent.futures.Future[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.
|
||||
|
||||
!!! important "Requires python 3.11 or higher for async functions"
|
||||
The `task` decorator supports both sync and async functions. To use async
|
||||
functions, ensure that you are using Python 3.11 or higher.
|
||||
|
||||
Tasks can only be called from within an [entrypoint][langgraph.func.entrypoint] or
|
||||
from within a StateGraph. A task can be called like a regular function with the
|
||||
following differences:
|
||||
|
||||
- When a checkpointer is enabled, the function inputs and outputs must be serializable.
|
||||
- The decorated function can only be called from within an entrypoint or StateGraph.
|
||||
- Calling the function produces a future. This makes it easy to parallelize tasks.
|
||||
|
||||
Args:
|
||||
retry: An optional retry policy to use for the task in case of a failure.
|
||||
|
||||
Returns:
|
||||
A callable function when used as a decorator.
|
||||
|
||||
Example: Sync Task
|
||||
```python
|
||||
from langgraph.func import entrypoint, task
|
||||
|
||||
@task
|
||||
def add_one(a: int) -> int:
|
||||
return a + 1
|
||||
|
||||
@entrypoint()
|
||||
def add_one(numbers: list[int]) -> list[int]:
|
||||
futures = [add_one(n) for n in numbers]
|
||||
results = [f.result() for f in futures]
|
||||
return results
|
||||
|
||||
# Call the entrypoint
|
||||
add_one.invoke([1, 2, 3]) # Returns [2, 3, 4]
|
||||
```
|
||||
|
||||
Example: Async Task
|
||||
```python
|
||||
import asyncio
|
||||
from langgraph.func import entrypoint, task
|
||||
|
||||
@task
|
||||
async def add_one(a: int) -> int:
|
||||
return a + 1
|
||||
|
||||
@entrypoint()
|
||||
async def add_one(numbers: list[int]) -> list[int]:
|
||||
futures = [add_one(n) for n in numbers]
|
||||
return asyncio.gather(*futures)
|
||||
|
||||
# Call the entrypoint
|
||||
await add_one.ainvoke([1, 2, 3]) # Returns [2, 3, 4]
|
||||
```
|
||||
"""
|
||||
|
||||
def decorator(
|
||||
func: Union[Callable[P, Awaitable[T]], Callable[P, T]],
|
||||
) -> Callable[P, concurrent.futures.Future[T]]:
|
||||
@@ -96,9 +162,9 @@ def task(
|
||||
def _tick(__allargs__: tuple) -> T:
|
||||
return func(*__allargs__[0], **__allargs__[1])
|
||||
|
||||
return functools.update_wrapper(
|
||||
functools.partial(call, _tick, retry=retry), func
|
||||
)
|
||||
wrapper = functools.partial(call, _tick, retry=retry)
|
||||
object.__setattr__(wrapper, "_is_pregel_task", True)
|
||||
return functools.update_wrapper(wrapper, func)
|
||||
|
||||
if __func_or_none__ is not None:
|
||||
return decorator(__func_or_none__)
|
||||
@@ -110,23 +176,219 @@ def entrypoint(
|
||||
*,
|
||||
checkpointer: Optional[BaseCheckpointSaver] = None,
|
||||
store: Optional[BaseStore] = None,
|
||||
config_schema: Optional[type[Any]] = None,
|
||||
) -> Callable[[types.FunctionType], Pregel]:
|
||||
"""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.
|
||||
|
||||
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.
|
||||
|
||||
The decorated function also has access to these optional 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 entrypoint decorator can be applied to sync functions, async functions,
|
||||
generator functions, and async generator functions.
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
checkpointer: Specify a checkpointer to create a workflow that can persist
|
||||
its state across runs.
|
||||
store: A generalized key-value store. Some implementations may support
|
||||
semantic search capabilities through an optional `index` configuration.
|
||||
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
|
||||
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import interrupt, Command
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
@task
|
||||
def compose_essay(topic: str) -> str:
|
||||
time.sleep(1.0) # Simulate slow operation
|
||||
return f"An essay about {topic}"
|
||||
|
||||
@entrypoint(checkpointer=MemorySaver())
|
||||
def review_workflow(topic: str) -> dict:
|
||||
\"\"\"Manages the workflow for generating and reviewing an essay.
|
||||
|
||||
The workflow includes:
|
||||
1. Generating an essay about the given topic.
|
||||
2. Interrupting the workflow for human review of the generated essay.
|
||||
|
||||
Upon resuming the workflow, compose_essay task will not be re-executed
|
||||
as its result is cached by the checkpointer.
|
||||
|
||||
Args:
|
||||
topic (str): The subject of the essay.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the generated essay and the human review.
|
||||
\"\"\"
|
||||
essay_future = compose_essay(topic)
|
||||
essay = essay_future.result()
|
||||
human_review = interrupt({
|
||||
\"question\": \"Please provide a review\",
|
||||
\"essay\": essay
|
||||
})
|
||||
return {
|
||||
\"essay\": essay,
|
||||
\"review\": human_review,
|
||||
}
|
||||
|
||||
# Example configuration for the workflow
|
||||
config = {
|
||||
\"configurable\": {
|
||||
\"thread_id\": \"some_thread\"
|
||||
}
|
||||
}
|
||||
|
||||
# Topic for the essay
|
||||
topic = \"cats\"
|
||||
|
||||
# Stream the workflow to generate the essay and await human review
|
||||
for result in review_workflow.stream(topic, config):
|
||||
print(result)
|
||||
|
||||
# Example human review provided after the interrupt
|
||||
human_review = \"This essay is great.\"
|
||||
|
||||
# Resume the workflow with the provided human review
|
||||
for result in review_workflow.stream(Command(resume=human_review), config):
|
||||
print(result)
|
||||
```
|
||||
|
||||
Example: Accessing the previous return value
|
||||
When a checkpointer is enabled the function can access the previous return value
|
||||
of the previous invocation on the same thread id.
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.func import entrypoint, task
|
||||
|
||||
@entrypoint(checkpointer=MemorySaver())
|
||||
def my_workflow(input_data: str, previous: Optional[str] = None) -> str:
|
||||
return "world"
|
||||
|
||||
# highlight-next-line
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id":
|
||||
}
|
||||
}
|
||||
my_workflow.invoke("hello")
|
||||
```
|
||||
"""
|
||||
|
||||
def _imp(func: types.FunctionType) -> 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.
|
||||
|
||||
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:
|
||||
|
||||
def gen_wrapper(*args: Any, writer: StreamWriter, **kwargs: Any) -> Any:
|
||||
for chunk in func(*args, **kwargs):
|
||||
writer(chunk)
|
||||
@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:
|
||||
|
||||
async def agen_wrapper(
|
||||
*args: Any, writer: StreamWriter, **kwargs: Any
|
||||
) -> Any:
|
||||
async for chunk in func(*args, **kwargs):
|
||||
writer(chunk)
|
||||
@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"
|
||||
@@ -134,7 +396,24 @@ def entrypoint(
|
||||
bound = get_runnable_for_func(func)
|
||||
stream_mode = "updates"
|
||||
|
||||
return Pregel(
|
||||
# get input and output types
|
||||
sig = inspect.signature(func)
|
||||
first_parameter_name = next(iter(sig.parameters.keys()), None)
|
||||
if not first_parameter_name:
|
||||
raise ValueError("Entrypoint function must have at least one parameter")
|
||||
input_type = (
|
||||
sig.parameters[first_parameter_name].annotation
|
||||
if sig.parameters[first_parameter_name].annotation
|
||||
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(
|
||||
nodes={
|
||||
func.__name__: PregelNode(
|
||||
bound=bound,
|
||||
@@ -143,7 +422,10 @@ def entrypoint(
|
||||
writers=[ChannelWrite([ChannelWriteEntry(END)], tags=[TAG_HIDDEN])],
|
||||
)
|
||||
},
|
||||
channels={START: EphemeralValue(Any), END: LastValue(Any, END)},
|
||||
channels={
|
||||
START: EphemeralValue(input_type),
|
||||
END: LastValue(output_type, END),
|
||||
},
|
||||
input_channels=START,
|
||||
output_channels=END,
|
||||
stream_channels=END,
|
||||
@@ -151,6 +433,101 @@ def entrypoint(
|
||||
stream_eager=True,
|
||||
checkpointer=checkpointer,
|
||||
store=store,
|
||||
config_type=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)
|
||||
)
|
||||
|
||||
@@ -1831,6 +1831,12 @@ class Pregel(PregelProtocol):
|
||||
interrupt_after=interrupt_after,
|
||||
debug=debug,
|
||||
)
|
||||
# set up subgraph checkpointing
|
||||
if self.checkpointer is True:
|
||||
ns = cast(str, config[CONF][CONFIG_KEY_CHECKPOINT_NS])
|
||||
config[CONF][CONFIG_KEY_CHECKPOINT_NS] = NS_SEP.join(
|
||||
part.split(NS_END)[0] for part in ns.split(NS_SEP)
|
||||
)
|
||||
# set up messages stream mode
|
||||
if "messages" in stream_modes:
|
||||
run_manager.inheritable_handlers.append(
|
||||
|
||||
@@ -37,6 +37,7 @@ from langgraph.constants import (
|
||||
CONFIG_KEY_CHECKPOINT_MAP,
|
||||
CONFIG_KEY_CHECKPOINT_NS,
|
||||
CONFIG_KEY_CHECKPOINTER,
|
||||
CONFIG_KEY_END,
|
||||
CONFIG_KEY_READ,
|
||||
CONFIG_KEY_SCRATCHPAD,
|
||||
CONFIG_KEY_SEND,
|
||||
@@ -507,6 +508,9 @@ def prepare_single_task(
|
||||
pending_writes,
|
||||
task_id,
|
||||
),
|
||||
CONFIG_KEY_END: checkpoint["channel_values"].get(
|
||||
"__end__", None
|
||||
),
|
||||
},
|
||||
),
|
||||
triggers,
|
||||
@@ -616,6 +620,9 @@ def prepare_single_task(
|
||||
pending_writes,
|
||||
task_id,
|
||||
),
|
||||
CONFIG_KEY_END: checkpoint["channel_values"].get(
|
||||
"__end__", None
|
||||
),
|
||||
},
|
||||
),
|
||||
triggers,
|
||||
@@ -737,6 +744,9 @@ def prepare_single_task(
|
||||
pending_writes,
|
||||
task_id,
|
||||
),
|
||||
CONFIG_KEY_END: checkpoint["channel_values"].get(
|
||||
"__end__", None
|
||||
),
|
||||
},
|
||||
),
|
||||
triggers,
|
||||
|
||||
@@ -345,11 +345,11 @@ class PregelLoop(LoopProtocol):
|
||||
(PUSH, task.path, write_idx, task.id, call),
|
||||
None,
|
||||
checkpoint=self.checkpoint,
|
||||
pending_writes=[(task.id, *w) for w in task.writes],
|
||||
pending_writes=self.checkpoint_pending_writes,
|
||||
processes=self.nodes,
|
||||
channels=self.channels,
|
||||
managed=self.managed,
|
||||
config=self.config,
|
||||
config=task.config,
|
||||
step=self.step,
|
||||
for_execution=True,
|
||||
store=self.store,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import threading
|
||||
import time
|
||||
from functools import partial
|
||||
from typing import (
|
||||
@@ -7,11 +8,13 @@ from typing import (
|
||||
AsyncIterator,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Generic,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Optional,
|
||||
Sequence,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
@@ -39,6 +42,56 @@ from langgraph.pregel.retry import arun_with_retry, run_with_retry
|
||||
from langgraph.types import PregelExecutableTask, RetryPolicy
|
||||
from langgraph.utils.future import chain_future
|
||||
|
||||
F = TypeVar("F", concurrent.futures.Future, asyncio.Future)
|
||||
E = TypeVar("E", threading.Event, asyncio.Event)
|
||||
|
||||
|
||||
class FuturesDict(Generic[F, E], dict[F, Optional[PregelExecutableTask]]):
|
||||
event: E
|
||||
callback: Callable[[PregelExecutableTask, Optional[BaseException]], None]
|
||||
counter: int
|
||||
done: set[F]
|
||||
lock: threading.Lock
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
event: E,
|
||||
callback: Callable[[PregelExecutableTask, Optional[BaseException]], None],
|
||||
future_type: Type[F],
|
||||
# used for generic typing, newer py supports FutureDict[...](...)
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.lock = threading.Lock()
|
||||
self.event = event
|
||||
self.callback = callback
|
||||
self.counter = 0
|
||||
self.done: set[F] = set()
|
||||
|
||||
def __setitem__(
|
||||
self,
|
||||
key: F,
|
||||
value: Optional[PregelExecutableTask],
|
||||
) -> None:
|
||||
super().__setitem__(key, value) # type: ignore[index]
|
||||
if value is not None:
|
||||
with self.lock:
|
||||
self.counter += 1
|
||||
key.add_done_callback(partial(self.on_done, value))
|
||||
|
||||
def on_done(
|
||||
self,
|
||||
task: PregelExecutableTask,
|
||||
fut: F,
|
||||
) -> None:
|
||||
try:
|
||||
self.callback(task, _exception(fut))
|
||||
finally:
|
||||
with self.lock:
|
||||
self.done.add(fut)
|
||||
self.counter -= 1
|
||||
if self.counter == 0 or _should_stop_others(self.done):
|
||||
self.event.set()
|
||||
|
||||
|
||||
class PregelRunner:
|
||||
"""Responsible for executing a set of Pregel tasks concurrently, committing
|
||||
@@ -138,7 +191,6 @@ class PregelRunner:
|
||||
# updates from this tick are committed/streamed first
|
||||
__next_tick__=True,
|
||||
)
|
||||
fut.add_done_callback(partial(self.commit, next_task))
|
||||
futures[fut] = next_task
|
||||
rtn[idx] = fut
|
||||
return [rtn.get(i) for i in range(len(writes))]
|
||||
@@ -151,17 +203,26 @@ class PregelRunner:
|
||||
retry: Optional[RetryPolicy] = None,
|
||||
callbacks: Callbacks = None,
|
||||
) -> concurrent.futures.Future[Any]:
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
raise RuntimeError("In an sync context async tasks cannot be called")
|
||||
(fut,) = writer(
|
||||
task,
|
||||
[(PUSH, None)],
|
||||
calls=[Call(func, input, retry=retry, callbacks=callbacks)],
|
||||
)
|
||||
assert fut is not None, "writer did not return a future for call"
|
||||
return fut
|
||||
# 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
|
||||
|
||||
tasks = tuple(tasks)
|
||||
futures: dict[concurrent.futures.Future, Optional[PregelExecutableTask]] = {}
|
||||
done_futures: set[concurrent.futures.Future] = set()
|
||||
futures = FuturesDict(
|
||||
callback=self.commit,
|
||||
event=threading.Event(),
|
||||
future_type=concurrent.futures.Future,
|
||||
)
|
||||
# give control back to the caller
|
||||
yield
|
||||
# fast path if single task with no timeout and no waiter
|
||||
@@ -178,12 +239,12 @@ class PregelRunner:
|
||||
)
|
||||
self.commit(t, None)
|
||||
except Exception as exc:
|
||||
self.commit(t, None, exc)
|
||||
self.commit(t, exc)
|
||||
if reraise and futures:
|
||||
# will be re-raised after futures are done
|
||||
fut: concurrent.futures.Future = concurrent.futures.Future()
|
||||
fut.set_exception(exc)
|
||||
done_futures.add(fut)
|
||||
futures.done.add(fut)
|
||||
elif reraise:
|
||||
raise
|
||||
if not futures: # maybe `t` schuduled another task
|
||||
@@ -206,7 +267,6 @@ class PregelRunner:
|
||||
},
|
||||
__reraise_on_exit__=reraise,
|
||||
)
|
||||
fut.add_done_callback(partial(self.commit, t))
|
||||
futures[fut] = t
|
||||
# execute tasks, and wait for one to fail or all to finish.
|
||||
# each task is independent from all other concurrent tasks
|
||||
@@ -226,9 +286,6 @@ class PregelRunner:
|
||||
# waiter task finished, schedule another
|
||||
if inflight and get_waiter is not None:
|
||||
futures[get_waiter()] = None
|
||||
else:
|
||||
# store for panic check
|
||||
done_futures.add(fut)
|
||||
else:
|
||||
# remove references to loop vars
|
||||
del fut, task
|
||||
@@ -237,13 +294,13 @@ class PregelRunner:
|
||||
break
|
||||
# give control back to the caller
|
||||
yield
|
||||
# wait for pending done callbacks
|
||||
# if a 2nd future finishes while `wait` is returning, it's possible
|
||||
# that done callbacks for the 2nd future aren't called until next tick
|
||||
time.sleep(0)
|
||||
# wait for done callbacks
|
||||
futures.event.wait(
|
||||
timeout=(max(0, end_time - time.monotonic()) if end_time else None)
|
||||
)
|
||||
# panic on failure or timeout
|
||||
_panic_or_proceed(
|
||||
done_futures.union(f for f, t in futures.items() if t is not None),
|
||||
futures.done.union(f for f, t in futures.items() if t is not None),
|
||||
panic=reraise,
|
||||
)
|
||||
|
||||
@@ -293,7 +350,7 @@ class PregelRunner:
|
||||
rtn[idx] = fut
|
||||
elif next_task.writes:
|
||||
# if it already ran, return the result
|
||||
fut = asyncio.Future()
|
||||
fut = asyncio.Future(loop=loop)
|
||||
ret = next(
|
||||
(v for c, v in next_task.writes if c == RETURN), MISSING
|
||||
)
|
||||
@@ -331,7 +388,6 @@ class PregelRunner:
|
||||
__next_tick__=True,
|
||||
),
|
||||
)
|
||||
fut.add_done_callback(partial(self.commit, next_task))
|
||||
futures[fut] = next_task
|
||||
rtn[idx] = fut
|
||||
return [rtn.get(i) for i in range(len(writes))]
|
||||
@@ -344,23 +400,29 @@ 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)],
|
||||
calls=[Call(func, input, retry=retry, callbacks=callbacks)],
|
||||
)
|
||||
assert fut is not None, "writer did not return a future for call"
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return fut
|
||||
# adapted from asyncio.run_coroutine_threadsafe
|
||||
sfut: concurrent.futures.Future = concurrent.futures.Future()
|
||||
loop.call_soon_threadsafe(chain_future, fut, sfut)
|
||||
# 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
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
tasks = tuple(tasks)
|
||||
futures: dict[asyncio.Future, Optional[PregelExecutableTask]] = {}
|
||||
done_futures: set[asyncio.Future] = set()
|
||||
futures = FuturesDict(
|
||||
callback=self.commit,
|
||||
event=asyncio.Event(),
|
||||
future_type=asyncio.Future,
|
||||
)
|
||||
# give control back to the caller
|
||||
yield
|
||||
# fast path if single task with no waiter and no timeout
|
||||
@@ -378,12 +440,12 @@ class PregelRunner:
|
||||
)
|
||||
self.commit(t, None)
|
||||
except Exception as exc:
|
||||
self.commit(t, None, exc)
|
||||
self.commit(t, exc)
|
||||
if reraise and futures:
|
||||
# will be re-raised after futures are done
|
||||
fut: asyncio.Future = loop.create_future()
|
||||
fut.set_exception(exc)
|
||||
done_futures.add(fut)
|
||||
futures.done.add(fut)
|
||||
elif reraise:
|
||||
raise
|
||||
if not futures: # maybe `t` schuduled another task
|
||||
@@ -412,7 +474,6 @@ class PregelRunner:
|
||||
__reraise_on_exit__=reraise,
|
||||
),
|
||||
)
|
||||
fut.add_done_callback(partial(self.commit, t))
|
||||
futures[fut] = t
|
||||
# execute tasks, and wait for one to fail or all to finish.
|
||||
# each task is independent from all other concurrent tasks
|
||||
@@ -432,9 +493,6 @@ class PregelRunner:
|
||||
# waiter task finished, schedule another
|
||||
if inflight and get_waiter is not None:
|
||||
futures[get_waiter()] = None
|
||||
else:
|
||||
# store for panic check
|
||||
done_futures.add(fut)
|
||||
else:
|
||||
# remove references to loop vars
|
||||
del fut, task
|
||||
@@ -443,16 +501,17 @@ class PregelRunner:
|
||||
break
|
||||
# give control back to the caller
|
||||
yield
|
||||
# wait for pending done callbacks
|
||||
# if a 2nd future finishes while `wait` is returning, it's possible
|
||||
# that done callbacks for the 2nd future aren't called until next tick
|
||||
await asyncio.sleep(0)
|
||||
# wait for done callbacks
|
||||
await asyncio.wait_for(
|
||||
futures.event.wait(),
|
||||
timeout=(max(0, end_time - loop.time()) if end_time else None),
|
||||
)
|
||||
# cancel waiter task
|
||||
for fut in futures:
|
||||
fut.cancel()
|
||||
# panic on failure or timeout
|
||||
_panic_or_proceed(
|
||||
done_futures.union(f for f, t in futures.items() if t is not None),
|
||||
futures.done.union(f for f, t in futures.items() if t is not None),
|
||||
timeout_exc_cls=asyncio.TimeoutError,
|
||||
panic=reraise,
|
||||
)
|
||||
@@ -460,11 +519,8 @@ class PregelRunner:
|
||||
def commit(
|
||||
self,
|
||||
task: PregelExecutableTask,
|
||||
fut: Union[None, concurrent.futures.Future[Any], asyncio.Future[Any]],
|
||||
exception: Optional[BaseException] = None,
|
||||
exception: Optional[BaseException],
|
||||
) -> None:
|
||||
if fut is not None:
|
||||
exception = _exception(fut)
|
||||
if isinstance(exception, asyncio.CancelledError):
|
||||
# for cancelled tasks, also save error in task,
|
||||
# so loop can finish super-step
|
||||
@@ -495,7 +551,7 @@ class PregelRunner:
|
||||
|
||||
|
||||
def _should_stop_others(
|
||||
done: Union[set[concurrent.futures.Future[Any]], set[asyncio.Future[Any]]],
|
||||
done: set[F],
|
||||
) -> bool:
|
||||
"""Check if any task failed, if so, cancel all other tasks.
|
||||
GraphInterrupts are not considered failures."""
|
||||
|
||||
@@ -112,13 +112,16 @@ def _chain_future(source: AnyFuture, destination: AnyFuture) -> None:
|
||||
source.add_done_callback(_call_set_state)
|
||||
|
||||
|
||||
def chain_future(source: AnyFuture, destination: concurrent.futures.Future) -> None:
|
||||
def chain_future(source: AnyFuture, destination: AnyFuture) -> None:
|
||||
# adapted from asyncio.run_coroutine_threadsafe
|
||||
try:
|
||||
_chain_future(source, destination)
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
raise
|
||||
except BaseException as exc:
|
||||
if destination.set_running_or_notify_cancel():
|
||||
if isinstance(destination, concurrent.futures.Future):
|
||||
if destination.set_running_or_notify_cancel():
|
||||
destination.set_exception(exc)
|
||||
else:
|
||||
destination.set_exception(exc)
|
||||
raise
|
||||
|
||||
@@ -34,7 +34,12 @@ from langchain_core.runnables.utils import Input
|
||||
from langchain_core.tracers._streaming import _StreamingCallbackHandler
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
from langgraph.constants import CONF, CONFIG_KEY_STORE, CONFIG_KEY_STREAM_WRITER
|
||||
from langgraph.constants import (
|
||||
CONF,
|
||||
CONFIG_KEY_END,
|
||||
CONFIG_KEY_STORE,
|
||||
CONFIG_KEY_STREAM_WRITER,
|
||||
)
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import StreamWriter
|
||||
from langgraph.utils.config import (
|
||||
@@ -58,6 +63,10 @@ class StrEnum(str, enum.Enum):
|
||||
"""A string enum."""
|
||||
|
||||
|
||||
# Special type to denote any type is accepted
|
||||
ANY_TYPE = object()
|
||||
|
||||
|
||||
ASYNCIO_ACCEPTS_CONTEXT = sys.version_info >= (3, 11)
|
||||
|
||||
KWARGS_CONFIG_KEYS: tuple[tuple[str, tuple[Any, ...], str, Any], ...] = (
|
||||
@@ -73,6 +82,12 @@ KWARGS_CONFIG_KEYS: tuple[tuple[str, tuple[Any, ...], str, Any], ...] = (
|
||||
CONFIG_KEY_STORE,
|
||||
inspect.Parameter.empty,
|
||||
),
|
||||
(
|
||||
sys.intern("previous"),
|
||||
(ANY_TYPE,),
|
||||
CONFIG_KEY_END,
|
||||
inspect.Parameter.empty,
|
||||
),
|
||||
)
|
||||
"""List of kwargs that can be passed to functions, and their corresponding
|
||||
config keys, default values and type annotations.
|
||||
@@ -135,9 +150,12 @@ class RunnableCallable(Runnable):
|
||||
self.func_accepts: dict[str, bool] = {}
|
||||
for kw, typ, _, _ in KWARGS_CONFIG_KEYS:
|
||||
p = params.get(kw)
|
||||
self.func_accepts[kw] = (
|
||||
p is not None and p.annotation in typ and p.kind in VALID_KINDS
|
||||
)
|
||||
if typ == (ANY_TYPE,):
|
||||
self.func_accepts[kw] = p is not None and p.kind in VALID_KINDS
|
||||
else:
|
||||
self.func_accepts[kw] = (
|
||||
p is not None and p.annotation in typ and p.kind in VALID_KINDS
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
repr_args = {
|
||||
@@ -162,16 +180,20 @@ class RunnableCallable(Runnable):
|
||||
if self.func_accepts_config:
|
||||
kwargs["config"] = config
|
||||
_conf = config[CONF]
|
||||
for kw, _, ck, defv in KWARGS_CONFIG_KEYS:
|
||||
for kw, _, config_key, default_value in KWARGS_CONFIG_KEYS:
|
||||
if not self.func_accepts[kw]:
|
||||
continue
|
||||
|
||||
if defv is inspect.Parameter.empty and kw not in kwargs and ck not in _conf:
|
||||
if (
|
||||
default_value is inspect.Parameter.empty
|
||||
and kw not in kwargs
|
||||
and config_key not in _conf
|
||||
):
|
||||
raise ValueError(
|
||||
f"Missing required config key '{ck}' for '{self.name}'."
|
||||
f"Missing required config key '{config_key}' for '{self.name}'."
|
||||
)
|
||||
elif kwargs.get(kw) is None:
|
||||
kwargs[kw] = _conf.get(ck, defv)
|
||||
kwargs[kw] = _conf.get(config_key, default_value)
|
||||
|
||||
context = copy_context()
|
||||
if self.trace:
|
||||
@@ -210,16 +232,20 @@ class RunnableCallable(Runnable):
|
||||
if self.func_accepts_config:
|
||||
kwargs["config"] = config
|
||||
_conf = config[CONF]
|
||||
for kw, _, ck, defv in KWARGS_CONFIG_KEYS:
|
||||
for kw, _, config_key, default_value in KWARGS_CONFIG_KEYS:
|
||||
if not self.func_accepts[kw]:
|
||||
continue
|
||||
|
||||
if defv is inspect.Parameter.empty and kw not in kwargs and ck not in _conf:
|
||||
if (
|
||||
default_value is inspect.Parameter.empty
|
||||
and kw not in kwargs
|
||||
and config_key not in _conf
|
||||
):
|
||||
raise ValueError(
|
||||
f"Missing required config key '{ck}' for '{self.name}'."
|
||||
f"Missing required config key '{config_key}' for '{self.name}'."
|
||||
)
|
||||
elif kwargs.get(kw) is None:
|
||||
kwargs[kw] = _conf.get(ck, defv)
|
||||
kwargs[kw] = _conf.get(config_key, default_value)
|
||||
context = copy_context()
|
||||
if self.trace:
|
||||
callback_manager = get_async_callback_manager_for_config(config, self.tags)
|
||||
|
||||
Generated
+3
-3
@@ -1324,14 +1324,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.25"
|
||||
version = "0.3.30"
|
||||
description = "Building applications with LLMs through composability"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "langchain_core-0.3.25-py3-none-any.whl", hash = "sha256:e10581c6c74ba16bdc6fdf16b00cced2aa447cc4024ed19746a1232918edde38"},
|
||||
{file = "langchain_core-0.3.25.tar.gz", hash = "sha256:fdb8df41e5cdd928c0c2551ebbde1cea770ee3c64598395367ad77ddf9acbae7"},
|
||||
{file = "langchain_core-0.3.30-py3-none-any.whl", hash = "sha256:0a4c4e02fac5968b67fbb0142c00c2b976c97e45fce62c7ac9eb1636a6926493"},
|
||||
{file = "langchain_core-0.3.30.tar.gz", hash = "sha256:0f1281b4416977df43baf366633ad18e96c5dcaaeae6fcb8a799f9889c853243"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.2.62"
|
||||
version = "0.2.64"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2088,3 +2088,82 @@ def test_inspect_react() -> None:
|
||||
model = FakeToolCallingModel(tool_calls=[])
|
||||
agent = create_react_agent(model, [])
|
||||
inspect.getclosurevars(agent.nodes["agent"].bound.func)
|
||||
|
||||
|
||||
def test_react_with_subgraph_tools() -> None:
|
||||
class State(TypedDict):
|
||||
a: int
|
||||
b: int
|
||||
|
||||
class Output(TypedDict):
|
||||
result: int
|
||||
|
||||
# Define the subgraphs
|
||||
def add(state):
|
||||
return {"result": state["a"] + state["b"]}
|
||||
|
||||
add_subgraph = (
|
||||
StateGraph(State, output=Output).add_node(add).add_edge(START, "add").compile()
|
||||
)
|
||||
|
||||
def multiply(state):
|
||||
return {"result": state["a"] * state["b"]}
|
||||
|
||||
multiply_subgraph = (
|
||||
StateGraph(State, output=Output)
|
||||
.add_node(multiply)
|
||||
.add_edge(START, "multiply")
|
||||
.compile()
|
||||
)
|
||||
|
||||
multiply_subgraph.invoke({"a": 2, "b": 3})
|
||||
|
||||
# Add subgraphs as tools
|
||||
|
||||
def addition(a: int, b: int):
|
||||
"""Add two numbers"""
|
||||
return add_subgraph.invoke({"a": a, "b": b})["result"]
|
||||
|
||||
def multiplication(a: int, b: int):
|
||||
"""Multiply two numbers"""
|
||||
return multiply_subgraph.invoke({"a": a, "b": b})["result"]
|
||||
|
||||
model = FakeToolCallingModel(
|
||||
tool_calls=[
|
||||
[
|
||||
{"args": {"a": 2, "b": 3}, "id": "1", "name": "addition"},
|
||||
{"args": {"a": 2, "b": 3}, "id": "2", "name": "multiplication"},
|
||||
],
|
||||
[],
|
||||
]
|
||||
)
|
||||
checkpointer = MemorySaver()
|
||||
tool_node = ToolNode([addition, multiplication], handle_tool_errors=False)
|
||||
agent = create_react_agent(model, tool_node, checkpointer=checkpointer)
|
||||
result = agent.invoke(
|
||||
{"messages": [HumanMessage(content="What's 2 + 3 and 2 * 3?")]},
|
||||
config={"configurable": {"thread_id": "1"}},
|
||||
)
|
||||
assert result["messages"] == [
|
||||
_AnyIdHumanMessage(content="What's 2 + 3 and 2 * 3?"),
|
||||
AIMessage(
|
||||
content="What's 2 + 3 and 2 * 3?",
|
||||
id="0",
|
||||
tool_calls=[
|
||||
ToolCall(name="addition", args={"a": 2, "b": 3}, id="1"),
|
||||
ToolCall(name="multiplication", args={"a": 2, "b": 3}, id="2"),
|
||||
],
|
||||
),
|
||||
ToolMessage(
|
||||
content="5", name="addition", tool_call_id="1", id=result["messages"][2].id
|
||||
),
|
||||
ToolMessage(
|
||||
content="6",
|
||||
name="multiplication",
|
||||
tool_call_id="2",
|
||||
id=result["messages"][3].id,
|
||||
),
|
||||
AIMessage(
|
||||
content="What's 2 + 3 and 2 * 3?-What's 2 + 3 and 2 * 3?-5-6", id="1"
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1436,6 +1436,9 @@ def test_imp_task(request: pytest.FixtureRequest, checkpointer_name: str) -> Non
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
mapper_calls = 0
|
||||
|
||||
class Config:
|
||||
model: str
|
||||
|
||||
@task()
|
||||
def mapper(input: int) -> str:
|
||||
nonlocal mapper_calls
|
||||
@@ -1443,13 +1446,57 @@ def test_imp_task(request: pytest.FixtureRequest, checkpointer_name: str) -> Non
|
||||
time.sleep(input / 100)
|
||||
return str(input) * 2
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
@entrypoint(checkpointer=checkpointer, config_schema=Config)
|
||||
def graph(input: list[int]) -> list[str]:
|
||||
futures = [mapper(i) for i in input]
|
||||
mapped = [f.result() for f in futures]
|
||||
answer = interrupt("question")
|
||||
return [m + answer for m in mapped]
|
||||
|
||||
assert graph.get_input_jsonschema() == {
|
||||
"type": "array",
|
||||
"items": {"type": "integer"},
|
||||
"title": "LangGraphInput",
|
||||
}
|
||||
assert graph.get_output_jsonschema() == {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"title": "LangGraphOutput",
|
||||
}
|
||||
assert graph.get_config_jsonschema() == {
|
||||
"$defs": {
|
||||
"Configurable": {
|
||||
"properties": {
|
||||
"model": {"default": None, "title": "Model", "type": "string"},
|
||||
"checkpoint_id": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"default": None,
|
||||
"description": "Pass to fetch a past checkpoint. If None, fetches the latest checkpoint.",
|
||||
"title": "Checkpoint ID",
|
||||
},
|
||||
"checkpoint_ns": {
|
||||
"default": "",
|
||||
"description": 'Checkpoint namespace. Denotes the path to the subgraph node the checkpoint originates from, separated by `|` character, e.g. `"child|grandchild"`. Defaults to "" (root graph).',
|
||||
"title": "Checkpoint NS",
|
||||
"type": "string",
|
||||
},
|
||||
"thread_id": {
|
||||
"default": "",
|
||||
"title": "Thread ID",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"title": "Configurable",
|
||||
"type": "object",
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"configurable": {"$ref": "#/$defs/Configurable", "default": None}
|
||||
},
|
||||
"title": "LangGraphConfig",
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
assert [*graph.stream([0, 1], thread1)] == [
|
||||
{"mapper": "00"},
|
||||
@@ -1474,9 +1521,77 @@ def test_imp_task(request: pytest.FixtureRequest, checkpointer_name: str) -> Non
|
||||
assert mapper_calls == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_imp_nested(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
|
||||
def mynode(input: list[str]) -> list[str]:
|
||||
return [it + "a" for it in input]
|
||||
|
||||
builder = StateGraph(list[str])
|
||||
builder.add_node(mynode)
|
||||
builder.add_edge(START, "mynode")
|
||||
add_a = builder.compile()
|
||||
|
||||
@task
|
||||
def submapper(input: int) -> str:
|
||||
return str(input)
|
||||
|
||||
@task()
|
||||
def mapper(input: int) -> str:
|
||||
time.sleep(input / 100)
|
||||
return submapper(input).result() * 2
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def graph(input: list[int]) -> list[str]:
|
||||
futures = [mapper(i) for i in input]
|
||||
mapped = [f.result() for f in futures]
|
||||
answer = interrupt("question")
|
||||
final = [m + answer for m in mapped]
|
||||
return add_a.invoke(final)
|
||||
|
||||
assert graph.get_input_jsonschema() == {
|
||||
"type": "array",
|
||||
"items": {"type": "integer"},
|
||||
"title": "LangGraphInput",
|
||||
}
|
||||
assert graph.get_output_jsonschema() == {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"title": "LangGraphOutput",
|
||||
}
|
||||
|
||||
assert graph.get_graph().draw_mermaid() == snapshot
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
assert [*graph.stream([0, 1], thread1)] == [
|
||||
{"submapper": "0"},
|
||||
{"mapper": "00"},
|
||||
{"submapper": "1"},
|
||||
{"mapper": "11"},
|
||||
{
|
||||
"__interrupt__": (
|
||||
Interrupt(
|
||||
value="question",
|
||||
resumable=True,
|
||||
ns=[AnyStr("graph:")],
|
||||
when="during",
|
||||
),
|
||||
)
|
||||
},
|
||||
]
|
||||
|
||||
assert graph.invoke(Command(resume="answer"), thread1) == [
|
||||
"00answera",
|
||||
"11answera",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_imp_stream_order(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
|
||||
@@ -1499,6 +1614,8 @@ def test_imp_stream_order(
|
||||
fut_baz = baz(fut_bar.result())
|
||||
return fut_baz.result()
|
||||
|
||||
assert graph.get_graph().draw_mermaid() == snapshot
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
assert [c for c in graph.stream({"a": "0"}, thread1)] == [
|
||||
{
|
||||
@@ -4902,6 +5019,73 @@ def test_interrupt_loop(request: pytest.FixtureRequest, checkpointer_name: str):
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_interrupt_functional(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
checkpointer: BaseCheckpointSaver = request.getfixturevalue(
|
||||
f"checkpointer_{checkpointer_name}"
|
||||
)
|
||||
|
||||
@task
|
||||
def foo(state: dict) -> dict:
|
||||
return {"a": state["a"] + "foo"}
|
||||
|
||||
@task
|
||||
def bar(state: dict) -> dict:
|
||||
return {"a": state["a"] + "bar", "b": state["b"]}
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def graph(inputs: dict) -> dict:
|
||||
fut_foo = foo(inputs)
|
||||
value = interrupt("Provide value for bar:")
|
||||
bar_input = {**fut_foo.result(), "b": value}
|
||||
fut_bar = bar(bar_input)
|
||||
return fut_bar.result()
|
||||
|
||||
assert graph.get_graph().draw_mermaid() == snapshot
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
# First run, interrupted at bar
|
||||
graph.invoke({"a": ""}, config)
|
||||
# Resume with an answer
|
||||
res = graph.invoke(Command(resume="bar"), config)
|
||||
assert res == {"a": "foobar", "b": "bar"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_interrupt_task_functional(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
checkpointer: BaseCheckpointSaver = request.getfixturevalue(
|
||||
f"checkpointer_{checkpointer_name}"
|
||||
)
|
||||
|
||||
@task
|
||||
def foo(state: dict) -> dict:
|
||||
return {"a": state["a"] + "foo"}
|
||||
|
||||
@task
|
||||
def bar(state: dict) -> dict:
|
||||
value = interrupt("Provide value for bar:")
|
||||
return {"a": state["a"] + value}
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def graph(inputs: dict) -> dict:
|
||||
fut_foo = foo(inputs)
|
||||
fut_bar = bar(fut_foo.result())
|
||||
return fut_bar.result()
|
||||
|
||||
assert graph.get_graph().draw_mermaid() == snapshot
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
# First run, interrupted at bar
|
||||
graph.invoke({"a": ""}, config)
|
||||
# Resume with an answer
|
||||
res = graph.invoke(Command(resume="bar"), config)
|
||||
assert res == {"a": "foobar"}
|
||||
|
||||
|
||||
def test_root_mixed_return() -> None:
|
||||
def my_node(state: list[str]):
|
||||
return [Command(update=["a"]), ["b"]]
|
||||
@@ -5322,7 +5506,9 @@ def test_multiple_updates() -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_falsy_return_from_task(request: pytest.FixtureRequest, checkpointer_name: str):
|
||||
def test_falsy_return_from_task(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, snapshot: SnapshotAssertion
|
||||
):
|
||||
"""Test with a falsy return from a task."""
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
|
||||
@@ -5336,6 +5522,8 @@ def test_falsy_return_from_task(request: pytest.FixtureRequest, checkpointer_nam
|
||||
falsy_task().result()
|
||||
interrupt("test")
|
||||
|
||||
assert graph.get_graph().draw_mermaid() == snapshot
|
||||
|
||||
configurable = {"configurable": {"thread_id": str(uuid.uuid4())}}
|
||||
graph.invoke({"a": 5}, configurable)
|
||||
graph.invoke(Command(resume="123"), configurable)
|
||||
@@ -5343,7 +5531,7 @@ def test_falsy_return_from_task(request: pytest.FixtureRequest, checkpointer_nam
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_multiple_interrupts_imperative(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, snapshot: SnapshotAssertion
|
||||
):
|
||||
"""Test multiple interrupts with an imperative API."""
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
@@ -5368,6 +5556,8 @@ def test_multiple_interrupts_imperative(
|
||||
|
||||
return {"values": values}
|
||||
|
||||
assert graph.get_graph().draw_mermaid() == snapshot
|
||||
|
||||
configurable = {"configurable": {"thread_id": str(uuid.uuid4())}}
|
||||
graph.invoke({}, configurable)
|
||||
graph.invoke(Command(resume="a"), configurable)
|
||||
@@ -5526,3 +5716,425 @@ def test_sync_streaming_with_functional_api() -> None:
|
||||
delta = arrival_times[1] - arrival_times[0]
|
||||
# Delta cannot be less than 10 ms if it is streaming as results are generated.
|
||||
assert delta > time_delay
|
||||
|
||||
|
||||
def test_entrypoint_without_checkpointer() -> None:
|
||||
"""Test no checkpointer."""
|
||||
states = []
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
# Test without previous
|
||||
@entrypoint()
|
||||
def foo(inputs: Any) -> Any:
|
||||
states.append(inputs)
|
||||
return inputs
|
||||
|
||||
assert foo.invoke({"a": "1"}, config) == {"a": "1"}
|
||||
|
||||
@entrypoint()
|
||||
def foo(inputs: Any, *, previous: Any) -> Any:
|
||||
states.append(previous)
|
||||
return {"previous": previous, "current": inputs}
|
||||
|
||||
assert foo.invoke({"a": "1"}, config) == {"current": {"a": "1"}, "previous": None}
|
||||
assert foo.invoke({"a": "1"}, config) == {"current": {"a": "1"}, "previous": None}
|
||||
|
||||
|
||||
async def test_async_entrypoint_without_checkpointer() -> None:
|
||||
"""Test no checkpointer."""
|
||||
states = []
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
# Test without previous
|
||||
@entrypoint()
|
||||
async def foo(inputs: Any) -> Any:
|
||||
states.append(inputs)
|
||||
return inputs
|
||||
|
||||
assert (await foo.ainvoke({"a": "1"}, config)) == {"a": "1"}
|
||||
|
||||
@entrypoint()
|
||||
async def foo(inputs: Any, *, previous: Any) -> Any:
|
||||
states.append(previous)
|
||||
return {"previous": previous, "current": inputs}
|
||||
|
||||
assert (await foo.ainvoke({"a": "1"}, config)) == {
|
||||
"current": {"a": "1"},
|
||||
"previous": None,
|
||||
}
|
||||
assert (await foo.ainvoke({"a": "1"}, config)) == {
|
||||
"current": {"a": "1"},
|
||||
"previous": None,
|
||||
}
|
||||
|
||||
|
||||
def test_entrypoint_stateful() -> None:
|
||||
"""Test stateful entrypoint invoke."""
|
||||
|
||||
# Test invoke
|
||||
states = []
|
||||
|
||||
@entrypoint(checkpointer=MemorySaver())
|
||||
def foo(inputs, *, previous: Any) -> Any:
|
||||
states.append(previous)
|
||||
return {"previous": previous, "current": inputs}
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
assert foo.invoke({"a": "1"}, config) == {"current": {"a": "1"}, "previous": None}
|
||||
assert foo.invoke({"a": "2"}, config) == {
|
||||
"current": {"a": "2"},
|
||||
"previous": {"current": {"a": "1"}, "previous": None},
|
||||
}
|
||||
assert foo.invoke({"a": "3"}, config) == {
|
||||
"current": {"a": "3"},
|
||||
"previous": {
|
||||
"current": {"a": "2"},
|
||||
"previous": {"current": {"a": "1"}, "previous": None},
|
||||
},
|
||||
}
|
||||
assert states == [
|
||||
None,
|
||||
{"current": {"a": "1"}, "previous": None},
|
||||
{"current": {"a": "2"}, "previous": {"current": {"a": "1"}, "previous": None}},
|
||||
]
|
||||
|
||||
# Test stream
|
||||
@entrypoint(checkpointer=MemorySaver())
|
||||
def foo(inputs, *, previous: Any) -> Any:
|
||||
return {"previous": previous, "current": inputs}
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
items = [item for item in foo.stream({"a": "1"}, config)]
|
||||
assert items == [{"foo": {"current": {"a": "1"}, "previous": None}}]
|
||||
|
||||
|
||||
def test_entrypoint_from_sync_generator() -> None:
|
||||
"""@entrypoint does not support sync generators."""
|
||||
previous_return_values = []
|
||||
|
||||
@entrypoint(checkpointer=MemorySaver())
|
||||
def foo(inputs, previous=None) -> Any:
|
||||
previous_return_values.append(previous)
|
||||
yield "a"
|
||||
yield "b"
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
assert foo.invoke({"a": "1"}, config) == ["a", "b"]
|
||||
assert previous_return_values == [None]
|
||||
assert foo.invoke({"a": "2"}, config) == ["a", "b"]
|
||||
assert previous_return_values == [None, ["a", "b"]]
|
||||
|
||||
|
||||
def test_entrypoint_request_stream_writer() -> None:
|
||||
"""Test using a stream writer with an entrypoint."""
|
||||
|
||||
@entrypoint(checkpointer=MemorySaver())
|
||||
def foo(inputs, writer: StreamWriter) -> Any:
|
||||
writer("a")
|
||||
yield "b"
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
# Different invocations
|
||||
# Are any of these confusing or unexpected?
|
||||
assert list(foo.invoke({}, config)) == ["b"]
|
||||
assert list(foo.stream({}, config)) == ["a", "b"]
|
||||
|
||||
# Stream modes
|
||||
assert list(foo.stream({}, config, stream_mode=["updates"])) == [
|
||||
("updates", {"foo": ["b"]})
|
||||
]
|
||||
assert list(foo.stream({}, config, stream_mode=["values"])) == [("values", ["b"])]
|
||||
assert list(foo.stream({}, config, stream_mode=["custom"])) == [
|
||||
(
|
||||
"custom",
|
||||
"a",
|
||||
),
|
||||
(
|
||||
"custom",
|
||||
"b",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def test_entrypoint_from_async_generator() -> None:
|
||||
"""@entrypoint does not support sync generators."""
|
||||
# Test invoke
|
||||
previous_return_values = []
|
||||
|
||||
# In this version reducers do not work
|
||||
@entrypoint(checkpointer=MemorySaver())
|
||||
async def foo(inputs, previous=None) -> Any:
|
||||
previous_return_values.append(previous)
|
||||
yield "a"
|
||||
yield "b"
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
assert list(await foo.ainvoke({"a": "1"}, config)) == ["a", "b"]
|
||||
assert previous_return_values == [None]
|
||||
assert list(foo.invoke({"a": "2"}, config)) == ["a", "b"]
|
||||
assert previous_return_values == [None, ["a", "b"]]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_multiple_subgraphs(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
) -> None:
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
|
||||
class State(TypedDict):
|
||||
a: int
|
||||
b: int
|
||||
|
||||
class Output(TypedDict):
|
||||
result: int
|
||||
|
||||
# Define the subgraphs
|
||||
def add(state):
|
||||
return {"result": state["a"] + state["b"]}
|
||||
|
||||
add_subgraph = (
|
||||
StateGraph(State, output=Output).add_node(add).add_edge(START, "add").compile()
|
||||
)
|
||||
|
||||
def multiply(state):
|
||||
return {"result": state["a"] * state["b"]}
|
||||
|
||||
multiply_subgraph = (
|
||||
StateGraph(State, output=Output)
|
||||
.add_node(multiply)
|
||||
.add_edge(START, "multiply")
|
||||
.compile()
|
||||
)
|
||||
|
||||
# Test calling the same subgraph multiple times
|
||||
def call_same_subgraph(state):
|
||||
result = add_subgraph.invoke(state)
|
||||
another_result = add_subgraph.invoke({"a": result["result"], "b": 10})
|
||||
return another_result
|
||||
|
||||
parent_call_same_subgraph = (
|
||||
StateGraph(State, output=Output)
|
||||
.add_node(call_same_subgraph)
|
||||
.add_edge(START, "call_same_subgraph")
|
||||
.compile(checkpointer=checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
assert parent_call_same_subgraph.invoke({"a": 2, "b": 3}, config) == {"result": 15}
|
||||
|
||||
# Test calling multiple subgraphs
|
||||
class Output(TypedDict):
|
||||
add_result: int
|
||||
multiply_result: int
|
||||
|
||||
def call_multiple_subgraphs(state):
|
||||
add_result = add_subgraph.invoke(state)
|
||||
multiply_result = multiply_subgraph.invoke(state)
|
||||
return {
|
||||
"add_result": add_result["result"],
|
||||
"multiply_result": multiply_result["result"],
|
||||
}
|
||||
|
||||
parent_call_multiple_subgraphs = (
|
||||
StateGraph(State, output=Output)
|
||||
.add_node(call_multiple_subgraphs)
|
||||
.add_edge(START, "call_multiple_subgraphs")
|
||||
.compile(checkpointer=checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": "2"}}
|
||||
assert parent_call_multiple_subgraphs.invoke({"a": 2, "b": 3}, config) == {
|
||||
"add_result": 5,
|
||||
"multiply_result": 6,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_multiple_subgraphs_functional(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
) -> None:
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
|
||||
# Define addition subgraph
|
||||
@entrypoint()
|
||||
def add(inputs):
|
||||
a, b = inputs
|
||||
return a + b
|
||||
|
||||
# Define multiplication subgraph using tasks
|
||||
@task
|
||||
def multiply_task(a, b):
|
||||
return a * b
|
||||
|
||||
@entrypoint()
|
||||
def multiply(inputs):
|
||||
return multiply_task(*inputs).result()
|
||||
|
||||
# Test calling the same subgraph multiple times
|
||||
@task
|
||||
def call_same_subgraph(a, b):
|
||||
result = add.invoke([a, b])
|
||||
another_result = add.invoke([result, 10])
|
||||
return another_result
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def parent_call_same_subgraph(inputs):
|
||||
return call_same_subgraph(*inputs).result()
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
assert parent_call_same_subgraph.invoke([2, 3], config) == 15
|
||||
|
||||
# Test calling multiple subgraphs
|
||||
@task
|
||||
def call_multiple_subgraphs(a, b):
|
||||
add_result = add.invoke([a, b])
|
||||
multiply_result = multiply.invoke([a, b])
|
||||
return [add_result, multiply_result]
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def parent_call_multiple_subgraphs(inputs):
|
||||
return call_multiple_subgraphs(*inputs).result()
|
||||
|
||||
config = {"configurable": {"thread_id": "2"}}
|
||||
assert parent_call_multiple_subgraphs.invoke([2, 3], config) == [5, 6]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_multiple_subgraphs_mixed(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
) -> None:
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
|
||||
class State(TypedDict):
|
||||
a: int
|
||||
b: int
|
||||
|
||||
class Output(TypedDict):
|
||||
result: int
|
||||
|
||||
# Define the subgraphs
|
||||
def add(state):
|
||||
return {"result": state["a"] + state["b"]}
|
||||
|
||||
add_subgraph = (
|
||||
StateGraph(State, output=Output).add_node(add).add_edge(START, "add").compile()
|
||||
)
|
||||
|
||||
def multiply(state):
|
||||
return {"result": state["a"] * state["b"]}
|
||||
|
||||
multiply_subgraph = (
|
||||
StateGraph(State, output=Output)
|
||||
.add_node(multiply)
|
||||
.add_edge(START, "multiply")
|
||||
.compile()
|
||||
)
|
||||
|
||||
# Test calling the same subgraph multiple times
|
||||
@task
|
||||
def call_same_subgraph(a, b):
|
||||
result = add_subgraph.invoke({"a": a, "b": b})["result"]
|
||||
another_result = add_subgraph.invoke({"a": result, "b": 10})["result"]
|
||||
return another_result
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def parent_call_same_subgraph(inputs):
|
||||
return call_same_subgraph(*inputs).result()
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
assert parent_call_same_subgraph.invoke([2, 3], config) == 15
|
||||
|
||||
# Test calling multiple subgraphs
|
||||
@task
|
||||
def call_multiple_subgraphs(a, b):
|
||||
add_result = add_subgraph.invoke({"a": a, "b": b})["result"]
|
||||
multiply_result = multiply_subgraph.invoke({"a": a, "b": b})["result"]
|
||||
return [add_result, multiply_result]
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def parent_call_multiple_subgraphs(inputs):
|
||||
return call_multiple_subgraphs(*inputs).result()
|
||||
|
||||
config = {"configurable": {"thread_id": "2"}}
|
||||
assert parent_call_multiple_subgraphs.invoke([2, 3], config) == [5, 6]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_multiple_subgraphs_mixed_checkpointer(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
) -> None:
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
|
||||
class SubgraphState(TypedDict):
|
||||
sub_counter: Annotated[int, operator.add]
|
||||
|
||||
def subgraph_node(state):
|
||||
return {"sub_counter": 2}
|
||||
|
||||
sub_graph_1 = (
|
||||
StateGraph(SubgraphState)
|
||||
.add_node(subgraph_node)
|
||||
.add_edge(START, "subgraph_node")
|
||||
.compile(checkpointer=True)
|
||||
)
|
||||
|
||||
class OtherSubgraphState(TypedDict):
|
||||
other_sub_counter: Annotated[int, operator.add]
|
||||
|
||||
def other_subgraph_node(state):
|
||||
return {"other_sub_counter": 3}
|
||||
|
||||
sub_graph_2 = (
|
||||
StateGraph(OtherSubgraphState)
|
||||
.add_node(other_subgraph_node)
|
||||
.add_edge(START, "other_subgraph_node")
|
||||
.compile()
|
||||
)
|
||||
|
||||
class ParentState(TypedDict):
|
||||
parent_counter: int
|
||||
|
||||
def parent_node(state):
|
||||
result = sub_graph_1.invoke({"sub_counter": state["parent_counter"]})
|
||||
other_result = sub_graph_2.invoke({"other_sub_counter": result["sub_counter"]})
|
||||
return {"parent_counter": other_result["other_sub_counter"]}
|
||||
|
||||
parent_graph = (
|
||||
StateGraph(ParentState)
|
||||
.add_node(parent_node)
|
||||
.add_edge(START, "parent_node")
|
||||
.compile(checkpointer=checkpointer)
|
||||
)
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
assert parent_graph.invoke({"parent_counter": 0}, config) == {"parent_counter": 5}
|
||||
assert parent_graph.invoke({"parent_counter": 0}, config) == {"parent_counter": 7}
|
||||
config = {"configurable": {"thread_id": "2"}}
|
||||
assert [
|
||||
c
|
||||
for c in parent_graph.stream(
|
||||
{"parent_counter": 0}, config, subgraphs=True, stream_mode="updates"
|
||||
)
|
||||
] == [
|
||||
(("parent_node",), {"subgraph_node": {"sub_counter": 2}}),
|
||||
(
|
||||
(AnyStr("parent_node:"), "1"),
|
||||
{"other_subgraph_node": {"other_sub_counter": 3}},
|
||||
),
|
||||
((), {"parent_node": {"parent_counter": 5}}),
|
||||
]
|
||||
assert [
|
||||
c
|
||||
for c in parent_graph.stream(
|
||||
{"parent_counter": 0}, config, subgraphs=True, stream_mode="updates"
|
||||
)
|
||||
] == [
|
||||
(("parent_node",), {"subgraph_node": {"sub_counter": 2}}),
|
||||
(
|
||||
(AnyStr("parent_node:"), "1"),
|
||||
{"other_subgraph_node": {"other_sub_counter": 3}},
|
||||
),
|
||||
((), {"parent_node": {"parent_counter": 7}}),
|
||||
]
|
||||
|
||||
@@ -1132,7 +1132,8 @@ async def test_node_not_cancelled_on_other_node_interrupted(
|
||||
assert awhiles == 1
|
||||
|
||||
|
||||
async def test_step_timeout_on_stream_hang() -> None:
|
||||
@pytest.mark.parametrize("stream_hang_s", [0.3, 0.6])
|
||||
async def test_step_timeout_on_stream_hang(stream_hang_s: float) -> None:
|
||||
inner_task_cancelled = False
|
||||
|
||||
async def awhile(input: Any) -> None:
|
||||
@@ -2534,6 +2535,7 @@ async def test_imp_task_cancel(checkpointer_name: str) -> None:
|
||||
assert mapper_cancels == 2
|
||||
|
||||
|
||||
@pytest.mark.skip("TODO: re-enable")
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_imp_sync_from_async(checkpointer_name: str) -> None:
|
||||
@@ -6281,6 +6283,63 @@ async def test_interrupt_loop(checkpointer_name: str):
|
||||
]
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_interrupt_functional(checkpointer_name: str) -> None:
|
||||
@task
|
||||
async def foo(state: dict) -> dict:
|
||||
return {"a": state["a"] + "foo"}
|
||||
|
||||
@task
|
||||
async def bar(state: dict) -> dict:
|
||||
return {"a": state["a"] + "bar", "b": state["b"]}
|
||||
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
async def graph(inputs: dict) -> dict:
|
||||
foo_result = await foo(inputs)
|
||||
value = interrupt("Provide value for bar:")
|
||||
bar_input = {**foo_result, "b": value}
|
||||
bar_result = await bar(bar_input)
|
||||
return bar_result
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
# First run, interrupted at bar
|
||||
await graph.ainvoke({"a": ""}, config)
|
||||
# Resume with an answer
|
||||
res = await graph.ainvoke(Command(resume="bar"), config)
|
||||
assert res == {"a": "foobar", "b": "bar"}
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_interrupt_task_functional(checkpointer_name: str) -> None:
|
||||
@task
|
||||
async def foo(state: dict) -> dict:
|
||||
return {"a": state["a"] + "foo"}
|
||||
|
||||
@task
|
||||
async def bar(state: dict) -> dict:
|
||||
value = interrupt("Provide value for bar:")
|
||||
return {"a": state["a"] + value}
|
||||
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
async def graph(inputs: dict) -> dict:
|
||||
foo_result = await foo(inputs)
|
||||
bar_result = await bar(foo_result)
|
||||
return bar_result
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
# First run, interrupted at bar
|
||||
await graph.ainvoke({"a": ""}, config)
|
||||
# Resume with an answer
|
||||
res = await graph.ainvoke(Command(resume="bar"), config)
|
||||
assert res == {"a": "foobar"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_command_with_static_breakpoints(checkpointer_name: str) -> None:
|
||||
"""Test that we can use Command to resume and update with static breakpoints."""
|
||||
@@ -6871,3 +6930,281 @@ async def test_async_streaming_with_functional_api() -> None:
|
||||
delta = arrival_times[1] - arrival_times[0]
|
||||
# Delta cannot be less than 10 ms if it is streaming as results are generated.
|
||||
assert delta > time_delay
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_multiple_subgraphs(checkpointer_name: str) -> None:
|
||||
class State(TypedDict):
|
||||
a: int
|
||||
b: int
|
||||
|
||||
class Output(TypedDict):
|
||||
result: int
|
||||
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
# Define the subgraphs
|
||||
async def add(state):
|
||||
return {"result": state["a"] + state["b"]}
|
||||
|
||||
add_subgraph = (
|
||||
StateGraph(State, output=Output)
|
||||
.add_node(add)
|
||||
.add_edge(START, "add")
|
||||
.compile()
|
||||
)
|
||||
|
||||
async def multiply(state):
|
||||
return {"result": state["a"] * state["b"]}
|
||||
|
||||
multiply_subgraph = (
|
||||
StateGraph(State, output=Output)
|
||||
.add_node(multiply)
|
||||
.add_edge(START, "multiply")
|
||||
.compile()
|
||||
)
|
||||
|
||||
# Test calling the same subgraph multiple times
|
||||
async def call_same_subgraph(state):
|
||||
result = await add_subgraph.ainvoke(state)
|
||||
another_result = await add_subgraph.ainvoke(
|
||||
{"a": result["result"], "b": 10}
|
||||
)
|
||||
return another_result
|
||||
|
||||
parent_call_same_subgraph = (
|
||||
StateGraph(State, output=Output)
|
||||
.add_node(call_same_subgraph)
|
||||
.add_edge(START, "call_same_subgraph")
|
||||
.compile(checkpointer=checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
assert await parent_call_same_subgraph.ainvoke({"a": 2, "b": 3}, config) == {
|
||||
"result": 15
|
||||
}
|
||||
|
||||
# Test calling multiple subgraphs
|
||||
class Output(TypedDict):
|
||||
add_result: int
|
||||
multiply_result: int
|
||||
|
||||
async def call_multiple_subgraphs(state):
|
||||
add_result = await add_subgraph.ainvoke(state)
|
||||
multiply_result = await multiply_subgraph.ainvoke(state)
|
||||
return {
|
||||
"add_result": add_result["result"],
|
||||
"multiply_result": multiply_result["result"],
|
||||
}
|
||||
|
||||
parent_call_multiple_subgraphs = (
|
||||
StateGraph(State, output=Output)
|
||||
.add_node(call_multiple_subgraphs)
|
||||
.add_edge(START, "call_multiple_subgraphs")
|
||||
.compile(checkpointer=checkpointer)
|
||||
)
|
||||
config = {"configurable": {"thread_id": "2"}}
|
||||
assert await parent_call_multiple_subgraphs.ainvoke(
|
||||
{"a": 2, "b": 3}, config
|
||||
) == {
|
||||
"add_result": 5,
|
||||
"multiply_result": 6,
|
||||
}
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_multiple_subgraphs_functional(checkpointer_name: str) -> None:
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
# Define addition subgraph
|
||||
@entrypoint()
|
||||
async def add(inputs):
|
||||
a, b = inputs
|
||||
return a + b
|
||||
|
||||
# Define multiplication subgraph using tasks
|
||||
@task
|
||||
async def multiply_task(a, b):
|
||||
return a * b
|
||||
|
||||
@entrypoint()
|
||||
async def multiply(inputs):
|
||||
return await multiply_task(*inputs)
|
||||
|
||||
# Test calling the same subgraph multiple times
|
||||
@task
|
||||
async def call_same_subgraph(a, b):
|
||||
result = await add.ainvoke([a, b])
|
||||
another_result = await add.ainvoke([result, 10])
|
||||
return another_result
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
async def parent_call_same_subgraph(inputs):
|
||||
return await call_same_subgraph(*inputs)
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
assert await parent_call_same_subgraph.ainvoke([2, 3], config) == 15
|
||||
|
||||
# Test calling multiple subgraphs
|
||||
@task
|
||||
async def call_multiple_subgraphs(a, b):
|
||||
add_result = await add.ainvoke([a, b])
|
||||
multiply_result = await multiply.ainvoke([a, b])
|
||||
return [add_result, multiply_result]
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
async def parent_call_multiple_subgraphs(inputs):
|
||||
return await call_multiple_subgraphs(*inputs)
|
||||
|
||||
config = {"configurable": {"thread_id": "2"}}
|
||||
assert await parent_call_multiple_subgraphs.ainvoke([2, 3], config) == [5, 6]
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_multiple_subgraphs_mixed(checkpointer_name: str) -> None:
|
||||
class State(TypedDict):
|
||||
a: int
|
||||
b: int
|
||||
|
||||
class Output(TypedDict):
|
||||
result: int
|
||||
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
# Define the subgraphs
|
||||
async def add(state):
|
||||
return {"result": state["a"] + state["b"]}
|
||||
|
||||
add_subgraph = (
|
||||
StateGraph(State, output=Output)
|
||||
.add_node(add)
|
||||
.add_edge(START, "add")
|
||||
.compile()
|
||||
)
|
||||
|
||||
async def multiply(state):
|
||||
return {"result": state["a"] * state["b"]}
|
||||
|
||||
multiply_subgraph = (
|
||||
StateGraph(State, output=Output)
|
||||
.add_node(multiply)
|
||||
.add_edge(START, "multiply")
|
||||
.compile()
|
||||
)
|
||||
|
||||
# Test calling the same subgraph multiple times
|
||||
@task
|
||||
async def call_same_subgraph(a, b):
|
||||
result = (await add_subgraph.ainvoke({"a": a, "b": b}))["result"]
|
||||
another_result = (await add_subgraph.ainvoke({"a": result, "b": 10}))[
|
||||
"result"
|
||||
]
|
||||
return another_result
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
async def parent_call_same_subgraph(inputs):
|
||||
return await call_same_subgraph(*inputs)
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
assert await parent_call_same_subgraph.ainvoke([2, 3], config) == 15
|
||||
|
||||
# Test calling multiple subgraphs
|
||||
@task
|
||||
async def call_multiple_subgraphs(a, b):
|
||||
add_result = (await add_subgraph.ainvoke({"a": a, "b": b}))["result"]
|
||||
multiply_result = (await multiply_subgraph.ainvoke({"a": a, "b": b}))[
|
||||
"result"
|
||||
]
|
||||
return [add_result, multiply_result]
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
async def parent_call_multiple_subgraphs(inputs):
|
||||
return await call_multiple_subgraphs(*inputs)
|
||||
|
||||
config = {"configurable": {"thread_id": "2"}}
|
||||
assert await parent_call_multiple_subgraphs.ainvoke([2, 3], config) == [5, 6]
|
||||
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_multiple_subgraphs_mixed_checkpointer(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
) -> None:
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
|
||||
class SubgraphState(TypedDict):
|
||||
sub_counter: Annotated[int, operator.add]
|
||||
|
||||
async def subgraph_node(state):
|
||||
return {"sub_counter": 2}
|
||||
|
||||
sub_graph_1 = (
|
||||
StateGraph(SubgraphState)
|
||||
.add_node(subgraph_node)
|
||||
.add_edge(START, "subgraph_node")
|
||||
.compile(checkpointer=True)
|
||||
)
|
||||
|
||||
class OtherSubgraphState(TypedDict):
|
||||
other_sub_counter: Annotated[int, operator.add]
|
||||
|
||||
async def other_subgraph_node(state):
|
||||
return {"other_sub_counter": 3}
|
||||
|
||||
sub_graph_2 = (
|
||||
StateGraph(OtherSubgraphState)
|
||||
.add_node(other_subgraph_node)
|
||||
.add_edge(START, "other_subgraph_node")
|
||||
.compile()
|
||||
)
|
||||
|
||||
class ParentState(TypedDict):
|
||||
parent_counter: int
|
||||
|
||||
async def parent_node(state):
|
||||
result = await sub_graph_1.ainvoke({"sub_counter": state["parent_counter"]})
|
||||
other_result = await sub_graph_2.ainvoke(
|
||||
{"other_sub_counter": result["sub_counter"]}
|
||||
)
|
||||
return {"parent_counter": other_result["other_sub_counter"]}
|
||||
|
||||
parent_graph = (
|
||||
StateGraph(ParentState)
|
||||
.add_node(parent_node)
|
||||
.add_edge(START, "parent_node")
|
||||
.compile(checkpointer=checkpointer)
|
||||
)
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
assert await parent_graph.ainvoke({"parent_counter": 0}, config) == {
|
||||
"parent_counter": 5
|
||||
}
|
||||
assert await parent_graph.ainvoke({"parent_counter": 0}, config) == {
|
||||
"parent_counter": 7
|
||||
}
|
||||
config = {"configurable": {"thread_id": "2"}}
|
||||
assert [
|
||||
c
|
||||
async for c in parent_graph.astream(
|
||||
{"parent_counter": 0}, config, subgraphs=True, stream_mode="updates"
|
||||
)
|
||||
] == [
|
||||
(("parent_node",), {"subgraph_node": {"sub_counter": 2}}),
|
||||
(
|
||||
(AnyStr("parent_node:"), "1"),
|
||||
{"other_subgraph_node": {"other_sub_counter": 3}},
|
||||
),
|
||||
((), {"parent_node": {"parent_counter": 5}}),
|
||||
]
|
||||
assert [
|
||||
c
|
||||
async for c in parent_graph.astream(
|
||||
{"parent_counter": 0}, config, subgraphs=True, stream_mode="updates"
|
||||
)
|
||||
] == [
|
||||
(("parent_node",), {"subgraph_node": {"sub_counter": 2}}),
|
||||
(
|
||||
(AnyStr("parent_node:"), "1"),
|
||||
{"other_subgraph_node": {"other_sub_counter": 3}},
|
||||
),
|
||||
((), {"parent_node": {"parent_counter": 7}}),
|
||||
]
|
||||
|
||||
@@ -195,6 +195,7 @@ async def test_subgraph_w_interrupt(
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": False,
|
||||
"__pregel_previous": None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_task_id": history[0].tasks[0].id,
|
||||
"__pregel_scratchpad": {
|
||||
@@ -267,6 +268,7 @@ async def test_subgraph_w_interrupt(
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": False,
|
||||
"__pregel_previous": None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_task_id": history[0].tasks[0].id,
|
||||
"__pregel_scratchpad": {
|
||||
@@ -369,6 +371,7 @@ async def test_subgraph_w_interrupt(
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": False,
|
||||
"__pregel_previous": None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_task_id": history[0].tasks[0].id,
|
||||
"__pregel_scratchpad": {
|
||||
@@ -481,6 +484,7 @@ async def test_subgraph_w_interrupt(
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": True,
|
||||
"__pregel_previous": None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_task_id": history[1].tasks[0].id,
|
||||
"__pregel_scratchpad": {
|
||||
@@ -548,6 +552,7 @@ async def test_subgraph_w_interrupt(
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": True,
|
||||
"__pregel_previous": None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_task_id": history[1].tasks[0].id,
|
||||
"__pregel_scratchpad": {
|
||||
@@ -671,6 +676,7 @@ async def test_subgraph_w_interrupt(
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": True,
|
||||
"__pregel_previous": None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_task_id": history[1].tasks[0].id,
|
||||
"__pregel_scratchpad": {
|
||||
|
||||
@@ -194,6 +194,7 @@ def test_subgraph_w_interrupt(
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": False,
|
||||
"__pregel_previous": None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_task_id": history[0].tasks[0].id,
|
||||
"__pregel_scratchpad": {
|
||||
@@ -267,6 +268,7 @@ def test_subgraph_w_interrupt(
|
||||
"__pregel_store": None,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": False,
|
||||
"__pregel_previous": None,
|
||||
"__pregel_task_id": history[0].tasks[0].id,
|
||||
"__pregel_scratchpad": {
|
||||
"subgraph_counter": AnyInt(),
|
||||
@@ -370,6 +372,7 @@ def test_subgraph_w_interrupt(
|
||||
"__pregel_store": None,
|
||||
"__pregel_resuming": False,
|
||||
"__pregel_task_id": history[0].tasks[0].id,
|
||||
"__pregel_previous": None,
|
||||
"__pregel_scratchpad": {
|
||||
"subgraph_counter": AnyInt(),
|
||||
"call_counter": 0,
|
||||
@@ -480,6 +483,7 @@ def test_subgraph_w_interrupt(
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_store": None,
|
||||
"__pregel_resuming": True,
|
||||
"__pregel_previous": None,
|
||||
"__pregel_task_id": history[1].tasks[0].id,
|
||||
"__pregel_scratchpad": {
|
||||
"subgraph_counter": AnyInt(),
|
||||
@@ -547,6 +551,7 @@ def test_subgraph_w_interrupt(
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_store": None,
|
||||
"__pregel_resuming": True,
|
||||
"__pregel_previous": None,
|
||||
"__pregel_task_id": history[1].tasks[0].id,
|
||||
"__pregel_scratchpad": {
|
||||
"subgraph_counter": AnyInt(),
|
||||
@@ -669,6 +674,7 @@ def test_subgraph_w_interrupt(
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": True,
|
||||
"__pregel_previous": None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_task_id": history[1].tasks[0].id,
|
||||
"__pregel_scratchpad": {
|
||||
|
||||
Reference in New Issue
Block a user