mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-28 18:59:42 +02:00
Compare commits
86
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bcd48f497 | ||
|
|
ed8c91c39a | ||
|
|
67aeba6210 | ||
|
|
6ca126f94d | ||
|
|
2f49ebfd53 | ||
|
|
d9c781cc7d | ||
|
|
66af72acb3 | ||
|
|
a44d822cd3 | ||
|
|
28d884b37a | ||
|
|
d7a0da8f06 | ||
|
|
2d9347ec33 | ||
|
|
5e9bea5134 | ||
|
|
8951d162f0 | ||
|
|
d90126b34e | ||
|
|
adc4569daf | ||
|
|
6d70559618 | ||
|
|
70afd2dc2c | ||
|
|
14e7215090 | ||
|
|
460edde3a7 | ||
|
|
34d591dedf | ||
|
|
746b5f0730 | ||
|
|
64448404de | ||
|
|
9637f3b1be | ||
|
|
c7fe85585b | ||
|
|
c6971c6a58 | ||
|
|
2327f8619f | ||
|
|
4eb124e83d | ||
|
|
84e9ebda91 | ||
|
|
48e8576fcf | ||
|
|
6263c2f710 | ||
|
|
17bc55028e | ||
|
|
cd8443f5ce | ||
|
|
7e3f31f97c | ||
|
|
3db9bf325c | ||
|
|
97337355a2 | ||
|
|
d6c12133ad | ||
|
|
d67abdd112 | ||
|
|
8dec77e129 | ||
|
|
b81c21f311 | ||
|
|
cd6b086374 | ||
|
|
5929b0b08d | ||
|
|
9f33b4e529 | ||
|
|
f0fb8d9187 | ||
|
|
0ff913c50b | ||
|
|
67075bfb7f | ||
|
|
37b2956758 | ||
|
|
f42fc971e3 | ||
|
|
6bcab08f55 | ||
|
|
8fe7c8101f | ||
|
|
6bca615e6a | ||
|
|
0147790937 | ||
|
|
364c572f0f | ||
|
|
8af4b63c69 | ||
|
|
941b4b1287 | ||
|
|
24f04fba37 | ||
|
|
05f21a384b | ||
|
|
29e9ee2d7b | ||
|
|
d6b4ee348f | ||
|
|
9e89b68596 | ||
|
|
1251eeaa48 | ||
|
|
85e73653f9 | ||
|
|
a8b70cb23c | ||
|
|
173627a94c | ||
|
|
edd7d608cd | ||
|
|
8977a35060 | ||
|
|
0a6e5a18bb | ||
|
|
abc6323c6c | ||
|
|
d5a1bb05f5 | ||
|
|
8506c6655b | ||
|
|
44af4d8e3e | ||
|
|
8e6c0be48a | ||
|
|
35523f4081 | ||
|
|
9a7b1fa12a | ||
|
|
71bf2f9e85 | ||
|
|
b03c647677 | ||
|
|
5d49188d3e | ||
|
|
deeb2d6e92 | ||
|
|
0db67d4196 | ||
|
|
56c9c210c3 | ||
|
|
3398715258 | ||
|
|
cba7d21732 | ||
|
|
b6ea73ff24 | ||
|
|
1a477e57ff | ||
|
|
f94eeabebd | ||
|
|
9e22a75423 | ||
|
|
e0da491fe6 |
@@ -88,10 +88,11 @@ def has_blocklisted_command(code: str, metadata: dict) -> bool:
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_mermaid_retries(code: str) -> str:
|
||||
def remove_mermaid(code: str) -> str:
|
||||
return code.replace(
|
||||
"draw_mermaid_png()",
|
||||
"draw_mermaid_png(max_retries=10, retry_delay=2.0)"
|
||||
"display(Image(graph.get_graph().draw_mermaid_png()))",
|
||||
# replace with a dummy statement
|
||||
"print()"
|
||||
)
|
||||
|
||||
|
||||
@@ -188,12 +189,12 @@ def add_vcr_to_notebook(
|
||||
return notebook
|
||||
|
||||
|
||||
def add_mermaid_retries_to_notebook(notebook: nbformat.NotebookNode) -> nbformat.NotebookNode:
|
||||
def remove_mermaid_from_notebook(notebook: nbformat.NotebookNode) -> nbformat.NotebookNode:
|
||||
for cell in notebook.cells:
|
||||
if cell.cell_type != "code":
|
||||
continue
|
||||
|
||||
cell.source = add_mermaid_retries(cell.source)
|
||||
cell.source = remove_mermaid(cell.source)
|
||||
return notebook
|
||||
|
||||
|
||||
@@ -218,7 +219,7 @@ def process_notebooks(should_comment_install_cells: bool) -> None:
|
||||
notebook, cassette_prefix=cassette_prefix
|
||||
)
|
||||
|
||||
notebook = add_mermaid_retries_to_notebook(notebook)
|
||||
notebook = remove_mermaid_from_notebook(notebook)
|
||||
|
||||
if notebook_path in NOTEBOOKS_NO_EXECUTION:
|
||||
# Add a cell at the beginning to indicate that this notebook should not be executed
|
||||
|
||||
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 @@
|
||||
eNqFVMtOHEcUjaWs/BVXvcFY3fMKzMBsEmwT4mDACkSJZUejmu473eWpripXVQ+0EYuQx35+IRCIkPNYeedNVlnkC/w1udVDwyjGsjSL6fu+55y6x+cTNJYreesVlw4Nix192OnxucEXBVr341mOLlPJ6eOd3b2TwvA3mXPa9ptNpnnDsQkXZSNWedMiM3F2OlRJ+fbW7cOA3IMxlkEfAjcRZeT2Hiz31na/Gq/jveLFzpOVg2R5c9Phwe5Sa2u83QtCCKijqTK+yZhbsMBgQaoEF4BLeMRkumGYzj71kTk7GBi0hXCW4jtkmfUfJKhd5kuwZMJkjImP5jIWRYKDROWMS5/x9Dsy48GN5jqaSbuPhqwjJizOOQzbH8SK4JLuBi/PWYq2dhydZ8gSwvjni68tmmgtpazpa10SqDK6BNk2O41P6PfnWhzT/NG6jFXCZTr9PX3JdQgJjgRzeDZzT0/uNu9e3FdSYsXW9GKMqCMm+AT/uD+bK3qEMnXZ9KTT7b6qbXulxulfTGvBY+Yzm8+tkr8Rjpo4xx/OrGOusMen1B3//ec8R2tpl192NuslfnpP2w+0ONtFQzqb/ipTLg9OH9Au0zefGx5CuwdfMgmdVmcZ2t1+u9Nf7sLG1t7/9zhttzpLlbi+pykNYfP2448Oa8F8SC/BSAmh9geFHlR4e4kHfVkIEQY1y7OvmjwSQnClr6eHgeNOIDW6qgobQhE6poQINrj7ohjC4yo1DAojKLJ+JoIy4oz0FTHeSLnLimGDq8qc+kJNUpJn1Tb9hAInKJpU5EpfwTZtZP9+DQ/ndgrB70kLGwRXasJaiBJmooJRIWevGO7YUsagDDD/ZxH2M/QJGcKIG+tAK8t9JBNUKS1yagjcVgFeCqRqJhO4o/QsSJSL4cyJNF7yvnQGz/z4I54+o8fnF6HtiTGo68DMWxg2FAiaGUY3htRF8xZxRsNSCZcZktyAJ4sN2OU5F8yAU7CNbl+Z8bchlKoAliR+HouXcFAAgwpVKKzv6IeloIF3w+yQ9QlcGytDbLYaq51eq9XqdYns+TfttXAUXrP+UDqjkqJC1Te54qEPa3APSdQSDelvo+DUJ4ItTHiRvyOFvDJX1/KzWAuVNvlc4cip6EoVEYuGl3VtlPqyUXtptDrEVqfXXmq/K5A+bF8rgrYeFlz4EwJDoeKxBTXyiJnr0RuwzgjsChmDmsRO1TzytXwq3RBVuS5c9ZRJE6gb8ISAp4PE5RzqGs1ImRysxpiPeAyO2bENoeZTGxXTMfHzcEn1QsjZmL7muVhZXV5Z7a7eRMXsMVZHauB4TuHtRrd79B/qa3Sq
|
||||
@@ -1 +1 @@
|
||||
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
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
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
|
||||
@@ -1 +1 @@
|
||||
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
|
||||
@@ -1 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
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
|
||||
@@ -1 +1 @@
|
||||
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
|
||||
@@ -1 +1 @@
|
||||
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
|
||||
@@ -29,7 +29,9 @@ agent = create_react_agent(
|
||||
)
|
||||
|
||||
# Run the agent
|
||||
agent.invoke({"messages": "what is the weather in sf"})
|
||||
agent.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]}
|
||||
)
|
||||
```
|
||||
|
||||
1. Define a tool for the agent to use. Tools can be defined as vanilla Python functions. For more advanced tool usage and customization, check the [tools](./tools.md) page.
|
||||
@@ -85,7 +87,7 @@ agent = create_react_agent(
|
||||
)
|
||||
|
||||
agent.invoke(
|
||||
{"messages": "what is the weather in sf"},
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]}
|
||||
)
|
||||
```
|
||||
|
||||
@@ -113,7 +115,7 @@ agent = create_react_agent(
|
||||
)
|
||||
|
||||
agent.invoke(
|
||||
{"messages": "what is the weather in sf"},
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
# highlight-next-line
|
||||
config={"configurable": {"user_name": "John Smith"}}
|
||||
)
|
||||
@@ -150,12 +152,12 @@ agent = create_react_agent(
|
||||
# highlight-next-line
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
sf_response = agent.invoke(
|
||||
{"messages": "what is the weather in sf"},
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
# highlight-next-line
|
||||
config # (2)!
|
||||
)
|
||||
ny_response = agent.invoke(
|
||||
{"messages": "what about new york?"},
|
||||
{"messages": [{"role": "user", "content": "what about new york?"}]},
|
||||
# highlight-next-line
|
||||
config
|
||||
)
|
||||
@@ -189,7 +191,9 @@ agent = create_react_agent(
|
||||
response_format=WeatherResponse # (1)!
|
||||
)
|
||||
|
||||
response = agent.invoke({"messages": "what is the weather in sf"})
|
||||
response = agent.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]}
|
||||
)
|
||||
|
||||
# highlight-next-line
|
||||
response["structured_response"]
|
||||
|
||||
@@ -36,7 +36,7 @@ for this purpose:
|
||||
|
||||
```python
|
||||
agent.invoke(
|
||||
{"messages": "hi!"},
|
||||
{"messages": [{"role": "user", "content": "hi!"}]},
|
||||
# highlight-next-line
|
||||
config={"configurable": {"user_id": "user_123"}}
|
||||
)
|
||||
@@ -183,7 +183,7 @@ Tools can access context through special parameter **annotations**.
|
||||
)
|
||||
|
||||
agent.invoke(
|
||||
{"messages": "look up user information"},
|
||||
{"messages": [{"role": "user", "content": "look up user information"}]},
|
||||
# highlight-next-line
|
||||
config={"configurable": {"user_id": "user_123"}}
|
||||
)
|
||||
@@ -278,7 +278,7 @@ agent = create_react_agent(
|
||||
)
|
||||
|
||||
agent.invoke(
|
||||
{"messages": "greet the user"},
|
||||
{"messages": [{"role": "user", "content": "greet the user"}]},
|
||||
# highlight-next-line
|
||||
config={"configurable": {"user_id": "user_123"}}
|
||||
)
|
||||
|
||||
@@ -70,7 +70,7 @@ config = {
|
||||
}
|
||||
|
||||
for chunk in agent.stream(
|
||||
{"messages": "book a stay at McKittrick hotel"},
|
||||
{"messages": [{"role": "user", "content": "book a stay at McKittrick hotel"}]},
|
||||
# highlight-next-line
|
||||
config
|
||||
):
|
||||
@@ -194,7 +194,7 @@ config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
# Run the agent
|
||||
for chunk in agent.stream(
|
||||
{"messages": "book a stay at McKittrick hotel"},
|
||||
{"messages": [{"role": "user", "content": "book a stay at McKittrick hotel"}]},
|
||||
# highlight-next-line
|
||||
config
|
||||
):
|
||||
|
||||
@@ -40,8 +40,12 @@ async with MultiServerMCPClient(
|
||||
# highlight-next-line
|
||||
client.get_tools()
|
||||
)
|
||||
math_response = await agent.ainvoke({"messages": "what's (3 + 5) x 12?"})
|
||||
weather_response = await agent.ainvoke({"messages": "what is the weather in nyc?"})
|
||||
math_response = await agent.ainvoke(
|
||||
{"messages": [{"role": "user", "content": "what's (3 + 5) x 12?"}]}
|
||||
)
|
||||
weather_response = await agent.ainvoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in nyc?"}]}
|
||||
)
|
||||
```
|
||||
|
||||
## Custom MCP servers
|
||||
|
||||
@@ -59,14 +59,14 @@ config = {
|
||||
}
|
||||
|
||||
sf_response = agent.invoke(
|
||||
{"messages": "what is the weather in sf"},
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
# highlight-next-line
|
||||
config
|
||||
)
|
||||
|
||||
# Continue the conversation using the same thread_id
|
||||
ny_response = agent.invoke(
|
||||
{"messages": "what about new york?"},
|
||||
{"messages": [{"role": "user", "content": "what about new york?"}]},
|
||||
# highlight-next-line
|
||||
config # (4)!
|
||||
)
|
||||
@@ -188,13 +188,13 @@ agent = create_react_agent(
|
||||
|
||||
# Run the agent
|
||||
agent.invoke(
|
||||
{"messages": "look up user information"},
|
||||
{"messages": [{"role": "user", "content": "look up user information"}]},
|
||||
# highlight-next-line
|
||||
config={"configurable": {"user_id": "user_123"}}
|
||||
)
|
||||
```
|
||||
|
||||
1. The `InMemoryStore` is a store that stores data in memory. In a production setting, you would typically use a database or other persistent storage. Please review the [store documentation](../reference/stores.md) for more options. If you're deploying with **LangGraph Platform**, the platform will provide a production-ready store for you.
|
||||
1. The `InMemoryStore` is a store that stores data in memory. In a production setting, you would typically use a database or other persistent storage. Please review the [store documentation](../reference/store.md) for more options. If you're deploying with **LangGraph Platform**, the platform will provide a production-ready store for you.
|
||||
2. For this example, we write some sample data to the store using the `put` method. Please see the [BaseStore.put][langgraph.store.base.BaseStore.put] API reference for more details.
|
||||
3. The first argument is the namespace. This is used to group related data together. In this case, we are using the `users` namespace to group user data.
|
||||
4. A key within the namespace. This example uses a user ID for the key.
|
||||
@@ -206,7 +206,7 @@ agent.invoke(
|
||||
### Writing
|
||||
|
||||
```python title="Example of a tool that updates user information"
|
||||
from typing import TypedDict
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.config import get_store
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
@@ -236,7 +236,7 @@ agent = create_react_agent(
|
||||
|
||||
# Run the agent
|
||||
agent.invoke(
|
||||
{"messages": "My name is John Smith"},
|
||||
{"messages": [{"role": "user", "content": "My name is John Smith"}]},
|
||||
# highlight-next-line
|
||||
config={"configurable": {"user_id": "user_123"}} # (6)!
|
||||
)
|
||||
@@ -245,7 +245,7 @@ agent.invoke(
|
||||
store.get(("users",), "user_123").value
|
||||
```
|
||||
|
||||
1. The `InMemoryStore` is a store that stores data in memory. In a production setting, you would typically use a database or other persistent storage. Please review the [store documentation](../reference/stores.md) for more options. If you're deploying with **LangGraph Platform**, the platform will provide a production-ready store for you.
|
||||
1. The `InMemoryStore` is a store that stores data in memory. In a production setting, you would typically use a database or other persistent storage. Please review the [store documentation](../reference/store.md) for more options. If you're deploying with **LangGraph Platform**, the platform will provide a production-ready store for you.
|
||||
2. The `UserInfo` class is a `TypedDict` that defines the structure of the user information. The LLM will use this to format the response according to the schema.
|
||||
3. The `save_user_info` function is a tool that allows an agent to update user information. This could be useful for a chat application where the user wants to update their profile information.
|
||||
4. The `get_store` function is used to access the store. You can call it from anywhere in your code, including tools and prompts. This function returns the store that was passed to the agent when it was created.
|
||||
|
||||
@@ -53,12 +53,22 @@ hotel_assistant = create_react_agent(
|
||||
supervisor = create_supervisor(
|
||||
agents=[flight_assistant, hotel_assistant],
|
||||
model=ChatOpenAI(model="gpt-4o"),
|
||||
prompt="You manage a hotel booking assistant and a flight booking assistant. Assign work to them."
|
||||
prompt=(
|
||||
"You manage a hotel booking assistant and a"
|
||||
"flight booking assistant. Assign work to them."
|
||||
)
|
||||
).compile()
|
||||
|
||||
for chunk in supervisor.stream({
|
||||
"messages": "book a flight from BOS to JFK and a stay at McKittrick Hotel"
|
||||
}):
|
||||
for chunk in supervisor.stream(
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "book a flight from BOS to JFK and a stay at McKittrick Hotel"
|
||||
}
|
||||
]
|
||||
}
|
||||
):
|
||||
print(chunk)
|
||||
print("\n")
|
||||
```
|
||||
@@ -110,9 +120,16 @@ swarm = create_swarm(
|
||||
default_active_agent="flight_assistant"
|
||||
).compile()
|
||||
|
||||
for chunk in supervisor.stream({
|
||||
"messages": "book a flight from BOS to JFK and a stay at McKittrick Hotel"
|
||||
}):
|
||||
for chunk in swarm.stream(
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "book a flight from BOS to JFK and a stay at McKittrick Hotel"
|
||||
}
|
||||
]
|
||||
}
|
||||
):
|
||||
print(chunk)
|
||||
print("\n")
|
||||
```
|
||||
@@ -253,9 +270,16 @@ multi_agent_graph = (
|
||||
)
|
||||
|
||||
# Run the multi-agent graph
|
||||
for chunk in multi_agent_graph.stream({
|
||||
"messages": "book a flight from BOS to JFK and a stay at McKittrick Hotel"
|
||||
}):
|
||||
for chunk in multi_agent_graph.stream(
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "book a flight from BOS to JFK and a stay at McKittrick Hotel"
|
||||
}
|
||||
]
|
||||
}
|
||||
):
|
||||
print(chunk)
|
||||
print("\n")
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Running agents
|
||||
|
||||
|
||||
Agents support both synchronous and asynchronous execution using either `.invoke()` / `await .invoke()` for full responses, or `.stream()` / `.astream()` for **incremental** [streaming](#streaming) output. This section explains how to provide input, interpret output, enable streaming, and control execution limits.
|
||||
Agents support both synchronous and asynchronous execution using either `.invoke()` / `await .invoke()` for full responses, or `.stream()` / `.astream()` for **incremental** [streaming](streaming.md) output. This section explains how to provide input, interpret output, enable streaming, and control execution limits.
|
||||
|
||||
|
||||
## Basic usage
|
||||
@@ -18,7 +18,7 @@ Agents can be executed in two primary modes:
|
||||
agent = create_react_agent(...)
|
||||
|
||||
# highlight-next-line
|
||||
response = agent.invoke({"messages": "what is the weather in sf"})
|
||||
response = agent.invoke({"messages": [{"role": "user", "content": "what is the weather in sf"}]})
|
||||
```
|
||||
|
||||
=== "Async invocation"
|
||||
@@ -27,7 +27,7 @@ Agents can be executed in two primary modes:
|
||||
|
||||
agent = create_react_agent(...)
|
||||
# highlight-next-line
|
||||
response = await agent.ainvoke({"messages": "what is the weather in sf"})
|
||||
response = await agent.ainvoke({"messages": [{"role": "user", "content": "what is the weather in sf"}]})
|
||||
```
|
||||
|
||||
## Inputs and outputs
|
||||
@@ -82,7 +82,7 @@ Streaming is available in both sync and async modes:
|
||||
|
||||
```python
|
||||
for chunk in agent.stream(
|
||||
{"messages": "what is the weather in sf"},
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
stream_mode="updates"
|
||||
):
|
||||
print(chunk)
|
||||
@@ -92,7 +92,7 @@ Streaming is available in both sync and async modes:
|
||||
|
||||
```python
|
||||
async for chunk in agent.astream(
|
||||
{"messages": "what is the weather in sf"},
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
stream_mode="updates"
|
||||
):
|
||||
print(chunk)
|
||||
@@ -122,7 +122,7 @@ To control agent execution and avoid infinite loops, set a recursion limit. This
|
||||
|
||||
try:
|
||||
response = agent.invoke(
|
||||
{"messages": "what's the weather in sf"},
|
||||
{"messages": [{"role": "user", "content": "what's the weather in sf"}]},
|
||||
# highlight-next-line
|
||||
{"recursion_limit": recursion_limit},
|
||||
)
|
||||
@@ -148,7 +148,7 @@ To control agent execution and avoid infinite loops, set a recursion limit. This
|
||||
|
||||
try:
|
||||
response = agent_with_recursion_limit.invoke(
|
||||
{"messages": "what's the weather in sf"},
|
||||
{"messages": [{"role": "user", "content": "what's the weather in sf"}]},
|
||||
)
|
||||
except GraphRecursionError:
|
||||
print("Agent stopped due to max iterations.")
|
||||
|
||||
@@ -35,7 +35,7 @@ For example, if you have an agent that calls a tool once, you should see the fol
|
||||
)
|
||||
# highlight-next-line
|
||||
for chunk in agent.stream(
|
||||
{"messages": "what is the weather in sf"},
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
# highlight-next-line
|
||||
stream_mode="updates"
|
||||
):
|
||||
@@ -52,7 +52,7 @@ For example, if you have an agent that calls a tool once, you should see the fol
|
||||
)
|
||||
# highlight-next-line
|
||||
async for chunk in agent.astream(
|
||||
{"messages": "what is the weather in sf"},
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
# highlight-next-line
|
||||
stream_mode="updates"
|
||||
):
|
||||
@@ -73,7 +73,7 @@ To stream tokens as they are produced by the LLM, use `stream_mode="messages"`:
|
||||
)
|
||||
# highlight-next-line
|
||||
for token, metadata in agent.stream(
|
||||
{"messages": "what is the weather in sf"},
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
# highlight-next-line
|
||||
stream_mode="messages"
|
||||
):
|
||||
@@ -91,7 +91,7 @@ To stream tokens as they are produced by the LLM, use `stream_mode="messages"`:
|
||||
)
|
||||
# highlight-next-line
|
||||
async for token, metadata in agent.astream(
|
||||
{"messages": "what is the weather in sf"},
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
# highlight-next-line
|
||||
stream_mode="messages"
|
||||
):
|
||||
@@ -125,7 +125,7 @@ To stream updates from tools as they are executed, you can use [get_stream_write
|
||||
)
|
||||
|
||||
for chunk in agent.stream(
|
||||
{"messages": "what is the weather in sf"},
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
# highlight-next-line
|
||||
stream_mode="custom"
|
||||
):
|
||||
@@ -154,7 +154,7 @@ To stream updates from tools as they are executed, you can use [get_stream_write
|
||||
)
|
||||
|
||||
async for chunk in agent.astream(
|
||||
{"messages": "what is the weather in sf"},
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
# highlight-next-line
|
||||
stream_mode="custom"
|
||||
):
|
||||
@@ -178,7 +178,7 @@ You can specify multiple streaming modes by passing stream mode as a list: `stre
|
||||
)
|
||||
|
||||
for stream_mode, chunk in agent.stream(
|
||||
{"messages": "what is the weather in sf"},
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
# highlight-next-line
|
||||
stream_mode=["updates", "messages", "custom"]
|
||||
):
|
||||
@@ -195,7 +195,7 @@ You can specify multiple streaming modes by passing stream mode as a list: `stre
|
||||
)
|
||||
|
||||
async for stream_mode, chunk in agent.astream(
|
||||
{"messages": "what is the weather in sf"},
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
# highlight-next-line
|
||||
stream_mode=["updates", "messages", "custom"]
|
||||
):
|
||||
|
||||
@@ -116,7 +116,9 @@ agent = create_react_agent(
|
||||
tools=tools
|
||||
)
|
||||
|
||||
agent.invoke({"messages": "what's 3 + 5 and 4 * 7? make both calculations in parallel"})
|
||||
agent.invoke(
|
||||
{"messages": [{"role": "user", "content": "what's 3 + 5 and 4 * 7?"}]}
|
||||
)
|
||||
```
|
||||
|
||||
## Return tool results directly
|
||||
@@ -137,7 +139,9 @@ agent = create_react_agent(
|
||||
tools=[add]
|
||||
)
|
||||
|
||||
agent.invoke({"messages": "what's 3 + 5?"})
|
||||
agent.invoke(
|
||||
{"messages": [{"role": "user", "content": "what's 3 + 5?"}]}
|
||||
)
|
||||
```
|
||||
|
||||
## Force tool use
|
||||
@@ -161,7 +165,9 @@ agent = create_react_agent(
|
||||
tools=tools
|
||||
)
|
||||
|
||||
agent.invoke({"messages": "Hi, I am Bob"})
|
||||
agent.invoke(
|
||||
{"messages": [{"role": "user", "content": "Hi, I am Bob"}]}
|
||||
)
|
||||
```
|
||||
|
||||
!!! Warning "Avoid infinite loops"
|
||||
@@ -191,7 +197,9 @@ By default, the agent will catch all exceptions raised during tool calls and wil
|
||||
model="anthropic:claude-3-7-sonnet-latest",
|
||||
tools=[multiply]
|
||||
)
|
||||
agent.invoke({"messages": "what's 42 x 7?"})
|
||||
agent.invoke(
|
||||
{"messages": [{"role": "user", "content": "what's 42 x 7?"}]}
|
||||
)
|
||||
```
|
||||
|
||||
=== "Disable error handling"
|
||||
@@ -215,7 +223,9 @@ By default, the agent will catch all exceptions raised during tool calls and wil
|
||||
model="anthropic:claude-3-7-sonnet-latest",
|
||||
tools=tool_node
|
||||
)
|
||||
agent_no_error_handling.invoke({"messages": "what's 42 x 7?"})
|
||||
agent_no_error_handling.invoke(
|
||||
{"messages": [{"role": "user", "content": "what's 42 x 7?"}]}
|
||||
)
|
||||
```
|
||||
|
||||
1. This disables error handling (enabled by default). See all available strategies in the [API reference][langgraph.prebuilt.tool_node.ToolNode].
|
||||
@@ -243,7 +253,9 @@ By default, the agent will catch all exceptions raised during tool calls and wil
|
||||
model="anthropic:claude-3-7-sonnet-latest",
|
||||
tools=tool_node
|
||||
)
|
||||
agent_custom_error_handling.invoke({"messages": "what's 42 x 7?"})
|
||||
agent_custom_error_handling.invoke(
|
||||
{"messages": [{"role": "user", "content": "what's 42 x 7?"}]}
|
||||
)
|
||||
```
|
||||
|
||||
1. This provides a custom message to send to the LLM in case of an exception. See all available strategies in the [API reference][langgraph.prebuilt.tool_node.ToolNode].
|
||||
|
||||
@@ -40,6 +40,14 @@ Example `package.json` file:
|
||||
}
|
||||
```
|
||||
|
||||
When deploying your app, the dependencies will be installed using the package manager of your choice, provided they adhere to the compatible version ranges listed below:
|
||||
|
||||
```
|
||||
"@langchain/core": "^0.3.42",
|
||||
"@langchain/langgraph": "^0.2.57",
|
||||
"@langchain/langgraph-checkpoint": "~0.0.16",
|
||||
```
|
||||
|
||||
Example file directory:
|
||||
|
||||
```bash
|
||||
@@ -82,14 +90,10 @@ import { ChatOpenAI } from "@langchain/openai";
|
||||
import { MessagesAnnotation, StateGraph } from "@langchain/langgraph";
|
||||
import { ToolNode } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
const tools = [
|
||||
new TavilySearchResults({ maxResults: 3, }),
|
||||
];
|
||||
const tools = [new TavilySearchResults({ maxResults: 3 })];
|
||||
|
||||
// Define the function that calls the model
|
||||
async function callModel(
|
||||
state: typeof MessagesAnnotation.State,
|
||||
) {
|
||||
async function callModel(state: typeof MessagesAnnotation.State) {
|
||||
/**
|
||||
* Call the LLM powering our agent.
|
||||
* Feel free to customize the prompt, model, and other logic!
|
||||
@@ -101,9 +105,9 @@ async function callModel(
|
||||
const response = await model.invoke([
|
||||
{
|
||||
role: "system",
|
||||
content: `You are a helpful assistant. The current date is ${new Date().getTime()}.`
|
||||
content: `You are a helpful assistant. The current date is ${new Date().getTime()}.`,
|
||||
},
|
||||
...state.messages
|
||||
...state.messages,
|
||||
]);
|
||||
|
||||
// MessagesAnnotation supports returning a single message or array of messages
|
||||
@@ -141,10 +145,7 @@ const workflow = new StateGraph(MessagesAnnotation)
|
||||
routeModelOutput,
|
||||
// List of the possible destinations the conditional edge can route to.
|
||||
// Required for conditional edges to properly render the graph in Studio
|
||||
[
|
||||
"tools",
|
||||
"__end__"
|
||||
],
|
||||
["tools", "__end__"]
|
||||
)
|
||||
// This means that after `tools` is called, `callModel` node is called next.
|
||||
.addEdge("tools", "callModel");
|
||||
@@ -155,6 +156,7 @@ export const graph = workflow.compile();
|
||||
```
|
||||
|
||||
!!! info "Assign `CompiledGraph` to Variable"
|
||||
|
||||
The build process for LangGraph Cloud requires that the `CompiledGraph` object be assigned to a variable at the top-level of a JavaScript module (alternatively, you can provide [a function that creates a graph](./graph_rebuild.md)).
|
||||
|
||||
Example file directory:
|
||||
@@ -193,6 +195,7 @@ Example `langgraph.json` file:
|
||||
Note that the variable name of the `CompiledGraph` appears at the end of the value of each subkey in the top-level `graphs` key (i.e. `:<variable_name>`).
|
||||
|
||||
!!! info "Configuration Location"
|
||||
|
||||
The LangGraph API configuration file must be placed in a directory that is at the same level or higher than the TypeScript files that contain compiled graphs and associated dependencies.
|
||||
|
||||
## Next
|
||||
|
||||
@@ -22,7 +22,7 @@ To support this, LangGraph Studio, in combination with LangSmith, allows you to
|
||||
|
||||
First navigate to the LangSmith trace. Here you should see a button to "Run in Studio".
|
||||
|
||||
{width=1200}
|
||||
{width=1200}
|
||||
|
||||
This will prompt you to enter the url that your locally running agent is accessible at. Once provided, select "Clone thread locally". If you have multiple graphs in your agent, you will also be prompted to select a graph to clone this thread under.
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Configurable Headers
|
||||
|
||||
LangGraph allows runtime configuration to modify agent behavior and permissions dynamically. When using the [LangGraph Platform](../quick_start.md), you can pass this configuration in the request body (`config`) or specific request headers. This enables adjustments based on user identity or other request data (see the [configuration how-to](../../how-tos/configuration.ipynb) for more details on how to access within your graph).
|
||||
|
||||
For privacy, control which headers are passed to the runtime configuration via the `http.configurable_headers` section in your `langgraph.json` file.
|
||||
|
||||
Here's how to customize the included and excluded headers:
|
||||
|
||||
```json
|
||||
{
|
||||
"http": {
|
||||
"configurable_headers": {
|
||||
"include": ["x-user-id", "x-organization-id", "my-prefix-*"],
|
||||
"exclude": ["authorization", "x-api-key"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `include` and `exclude` lists accept exact header names or patterns using `*` to match any number of characters. For your security, no other regex patterns are supported.
|
||||
|
||||
If you'd like to opt-out of configurable headers, you can simply set a wildcard pattern in the `exclude` list:
|
||||
|
||||
```json
|
||||
{
|
||||
"http": {
|
||||
"configurable_headers": {
|
||||
"exclude": ["*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This will exclude all headers from being added to your run's configuration.
|
||||
@@ -1,12 +1,16 @@
|
||||
# How to Add Breakpoints
|
||||
# How to add static breakpoints
|
||||
|
||||
When creating LangGraph agents, it is often nice to add a human-in-the-loop component.
|
||||
This can be helpful when giving them access to tools.
|
||||
Often in these situations you may want to manually approve an action before taking.
|
||||
!!! tip "Prerequisites"
|
||||
|
||||
This can be in several ways, but the primary supported way is to add an "interrupt" before a node is executed.
|
||||
This interrupts execution at that node.
|
||||
You can then resume from that spot to continue.
|
||||
This guide assumes familiarity with the following concepts:
|
||||
|
||||
* [Breakpoints](../../concepts/breakpoints.md)
|
||||
* [LangGraph Glossary](../../concepts/low_level.md)
|
||||
|
||||
|
||||
Human-in-the-loop (HIL) interactions are crucial for [agentic systems](../../concepts/agentic_concepts.md#human-in-the-loop). [Breakpoints](../../concepts/low_level.md#breakpoints) are a common HIL interaction pattern, allowing the graph to stop at specific steps and seek human approval before proceeding (e.g., for sensitive actions).
|
||||
|
||||
Breakpoints are built on top of LangGraph [checkpoints](../../concepts/low_level.md#persistence), which save the graph's state after each node execution. Checkpoints are saved in [threads](../../concepts/low_level.md#threads) that preserve graph state and can be accessed after a graph has finished execution. This allows for graph execution to pause at specific points, await human approval, and then resume execution from the last checkpoint.
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
# Review Tool Calls
|
||||
# How to review tool calls
|
||||
|
||||
Human-in-the-loop (HIL) interactions are crucial for [agentic systems](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#human-in-the-loop). A common pattern is to add some human in the loop step after certain tool calls. These tool calls often lead to either a function call or saving of some information. Examples include:
|
||||
!!! tip "Prerequisites"
|
||||
|
||||
This guide assumes familiarity with the following concepts:
|
||||
|
||||
* [Tool calling](https://python.langchain.com/docs/concepts/tool_calling/)
|
||||
* [Human-in-the-loop](../../concepts/human_in_the_loop.md)
|
||||
* [LangGraph Glossary](../../concepts/low_level.md)
|
||||
|
||||
Human-in-the-loop (HIL) interactions are crucial for [agentic systems](../../concepts/agentic_concepts.md). A common pattern is to add some human in the loop step after certain tool calls. These tool calls often lead to either a function call or saving of some information. Examples include:
|
||||
|
||||
- A tool call to execute SQL, which will then be run by the tool
|
||||
- A tool call to generate a summary, which will then be saved to the State of the graph
|
||||
@@ -11,13 +19,46 @@ There are typically a few different interactions you may want to do here:
|
||||
|
||||
1. Approve the tool call and continue
|
||||
2. Modify the tool call manually and then continue
|
||||
3. Give natural language feedback, and then pass that back to the agent instead of continuing
|
||||
3. Give natural language feedback, and then pass that back to the agent
|
||||
|
||||
We can implement this in LangGraph using a [breakpoint](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/breakpoints/): breakpoints allow us to interrupt graph execution before a specific step. At this breakpoint, we can manually update the graph state taking one of the three options above
|
||||
|
||||
We can implement these in LangGraph using the [`interrupt()`][langgraph.types.interrupt] function. `interrupt` allows us to stop graph execution to collect input from a user and continue execution with collected input:
|
||||
|
||||
|
||||
```python
|
||||
def human_review_node(state) -> Command[Literal["call_llm", "run_tool"]]:
|
||||
# this is the value we'll be providing via Command(resume=<human_review>)
|
||||
human_review = interrupt(
|
||||
{
|
||||
"question": "Is this correct?",
|
||||
# Surface tool calls for review
|
||||
"tool_call": tool_call
|
||||
}
|
||||
)
|
||||
|
||||
review_action, review_data = human_review
|
||||
|
||||
# Approve the tool call and continue
|
||||
if review_action == "continue":
|
||||
return Command(goto="run_tool")
|
||||
|
||||
# Modify the tool call manually and then continue
|
||||
elif review_action == "update":
|
||||
...
|
||||
updated_msg = get_updated_msg(review_data)
|
||||
return Command(goto="run_tool", update={"messages": [updated_message]})
|
||||
|
||||
# Give natural language feedback, and then pass that back to the agent
|
||||
elif review_action == "feedback":
|
||||
...
|
||||
feedback_msg = get_feedback_msg(review_data)
|
||||
return Command(goto="call_llm", update={"messages": [feedback_msg]})
|
||||
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
We are not going to show the full code for the graph we are hosting, but you can see it [here](../../how-tos/human_in_the_loop/review-tool-calls.ipynb#simple-usage) if you want to. Once this graph is hosted, we are ready to invoke it and wait for user input.
|
||||
We are not going to show the full code for the graph we are hosting, but you can see it [here](../../how-tos/human_in_the_loop/review-tool-calls.ipynb). Once this graph is hosted, we are ready to invoke it and wait for user input.
|
||||
|
||||
### SDK initialization
|
||||
|
||||
@@ -54,122 +95,9 @@ First, we need to setup our client so that we can communicate with our hosted gr
|
||||
--data '{}'
|
||||
```
|
||||
|
||||
## Example with no review
|
||||
|
||||
Let's look at an example when no review is required (because no tools are called)
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
input = { 'messages':[{ "role":"user", "content":"hi!" }] }
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistant_id,
|
||||
input=input,
|
||||
stream_mode="updates",
|
||||
interrupt_before=["action"],
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const input = { "messages": [{ "role": "user", "content": "hi!" }] };
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: input,
|
||||
streamMode: "updates",
|
||||
interruptBefore: ["action"],
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"hi!\"}]},
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
],
|
||||
\"interrupt_before\": [\"action\"]
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "", $0)
|
||||
event_type = $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'messages': [{'content': 'hi!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '39c51f14-2d5c-4690-883a-d940854b1845', 'example': False}]}
|
||||
{'messages': [{'content': 'hi!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '39c51f14-2d5c-4690-883a-d940854b1845', 'example': False}, {'content': [{'text': "Hello! Welcome. How can I assist you today? Is there anything specific you'd like to know or any information you're looking for?", 'type': 'text', 'index': 0}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-d65e07fb-43ff-4d98-ab6b-6316191b9c8b', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 355, 'output_tokens': 31, 'total_tokens': 386}}]}
|
||||
|
||||
|
||||
If we check the state, we can see that it is finished
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
state = await client.threads.get_state(thread["thread_id"])
|
||||
|
||||
print(state['next'])
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const state = await client.threads.getState(thread["thread_id"]);
|
||||
|
||||
console.log(state.next);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state | jq -c '.next'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
[]
|
||||
|
||||
## Example of approving tool
|
||||
|
||||
Let's now look at what it looks like to approve a tool call. Note that we don't need to pass an interrupt to our streaming calls because the graph (defined [here](../../how-tos/human_in_the_loop/review-tool-calls.ipynb#simple-usage)) was already compiled with an interrupt before the `human_review_node`.
|
||||
First, let's run the agent with an input that requires tool calls with approval:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -180,6 +108,7 @@ Let's now look at what it looks like to approve a tool call. Note that we don't
|
||||
thread["thread_id"],
|
||||
assistant_id,
|
||||
input=input,
|
||||
stream_mode="updates",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
@@ -195,6 +124,7 @@ Let's now look at what it looks like to approve a tool call. Note that we don't
|
||||
assistantId,
|
||||
{
|
||||
input: input,
|
||||
streamMode: "updates"
|
||||
}
|
||||
);
|
||||
|
||||
@@ -213,75 +143,32 @@ Let's now look at what it looks like to approve a tool call. Note that we don't
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]}
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "", $0)
|
||||
event_type = $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]},
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
]
|
||||
}"
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '54e19d6e-89fa-44fb-b92c-12e7dd4ddf08', 'example': False}]}
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '54e19d6e-89fa-44fb-b92c-12e7dd4ddf08', 'example': False}, {'content': [{'text': "Certainly! I can help you check the weather in San Francisco. To get this information, I'll use the weather search function. Let me do that for you right away.", 'type': 'text', 'index': 0}, {'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-45a6b6c3-ac69-42a4-8957-d982203d6392', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 90, 'total_tokens': 450}}]}
|
||||
{'call_llm': {'messages': [{'content': [{'text': "I'll help you check the weather in San Francisco.", 'type': 'text'}, {'id': 'toolu_01142G3woscA8JjFTLdqymtn', 'input': {'city': 'San Francisco'}, 'name': 'weather_search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {'id': 'msg_01Tdfufy4nZYXMbVZvgyNbhc', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 379, 'output_tokens': 66}, 'model_name': 'claude-3-5-sonnet-20241022'}, 'type': 'ai', 'name': None, 'id': 'run-a33434b2-f5ca-40c6-98e2-6288d349d4ce-0', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01142G3woscA8JjFTLdqymtn', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 379, 'output_tokens': 66, 'total_tokens': 445, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}]}}
|
||||
{'__interrupt__': [{'value': {'question': 'Is this correct?', 'tool_call': {'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01142G3woscA8JjFTLdqymtn', 'type': 'tool_call'}}, 'resumable': True, 'ns': ['human_review_node:9caf42cf-1371-7213-a331-e6fe5d026be8'], 'when': 'during'}]}
|
||||
|
||||
|
||||
If we now check, we can see that it is waiting on human review:
|
||||
To approve the tool call, we need to let `human_review_node` know what value to use for the `human_review` variable we defined inside the node. We can provide this value by invoking the graph with a `Command(resume=<human_review>)` input. Since we're approving the tool call, we'll provide `resume` value of `{"action": "continue"}` to navigate to `run_tool` node:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
state = await client.threads.get_state(thread["thread_id"])
|
||||
# highlight-next-line
|
||||
from langgraph_sdk.schema import Command
|
||||
|
||||
print(state['next'])
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const state = await client.threads.getState(thread["thread_id"]);
|
||||
|
||||
console.log(state.next);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url <DELPOYMENT_URL>/threads/<THREAD_ID>/state | jq -c '.next'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
['human_review_node']
|
||||
|
||||
To approve the tool call, we can just continue the thread with no edits. To do this, we just create a new run with no inputs.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistant_id,
|
||||
input=None,
|
||||
stream_mode="values",
|
||||
# highlight-next-line
|
||||
command=Command(resume={"action": "continue"}),
|
||||
stream_mode="updates",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
@@ -294,8 +181,9 @@ To approve the tool call, we can just continue the thread with no edits. To do t
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: null,
|
||||
streamMode: "values",
|
||||
// highlight-next-line
|
||||
command: { resume: { "action": "continue" } },
|
||||
streamMode: "updates"
|
||||
}
|
||||
);
|
||||
|
||||
@@ -313,34 +201,21 @@ To approve the tool call, we can just continue the thread with no edits. To do t
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\"
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "", $0)
|
||||
event_type = $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
\"assistant_id\": \"agent\",
|
||||
\"command\": {
|
||||
\"resume\": { \"action\": \"continue\"}
|
||||
},
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
]
|
||||
}"
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '54e19d6e-89fa-44fb-b92c-12e7dd4ddf08', 'example': False}, {'content': [{'text': "Certainly! I can help you check the weather in San Francisco. To get this information, I'll use the weather search function. Let me do that for you right away.", 'type': 'text', 'index': 0}, {'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-45a6b6c3-ac69-42a4-8957-d982203d6392', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 90, 'total_tokens': 450}}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '826cd0f2-9cc6-46f0-b7df-daa6a05d13d2', 'tool_call_id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'artifact': None, 'status': 'success'}]}
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '54e19d6e-89fa-44fb-b92c-12e7dd4ddf08', 'example': False}, {'content': [{'text': "Certainly! I can help you check the weather in San Francisco. To get this information, I'll use the weather search function. Let me do that for you right away.", 'type': 'text', 'index': 0}, {'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-45a6b6c3-ac69-42a4-8957-d982203d6392', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 90, 'total_tokens': 450}}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '826cd0f2-9cc6-46f0-b7df-daa6a05d13d2', 'tool_call_id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'artifact': None, 'status': 'success'}, {'content': [{'text': "\n\nGreat news! The weather in San Francisco is sunny today. It's a beautiful day in the city by the bay. Is there anything else you'd like to know about the weather or any other information I can help you with?", 'type': 'text', 'index': 0}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-5d5fd0f1-a939-447e-801a-9aaa812322d3', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 464, 'output_tokens': 50, 'total_tokens': 514}}]}
|
||||
{'human_review_node': None}
|
||||
{'run_tool': {'messages': [{'role': 'tool', 'name': 'weather_search', 'content': 'Sunny!', 'tool_call_id': 'toolu_01142G3woscA8JjFTLdqymtn'}]}}
|
||||
{'call_llm': {'messages': [{'content': "According to the search, it's sunny in San Francisco right now!", 'additional_kwargs': {}, 'response_metadata': {'id': 'msg_01JJE9AtT4a9Lob91RRiW9rU', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 458, 'output_tokens': 18}, 'model_name': 'claude-3-5-sonnet-20241022'}, 'type': 'ai', 'name': None, 'id': 'run-5e8d80b5-c46a-4aad-af37-b01f8bb15963-0', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 458, 'output_tokens': 18, 'total_tokens': 476, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}]}}
|
||||
|
||||
## Edit Tool Call
|
||||
|
||||
@@ -355,7 +230,7 @@ Let's now say we want to edit the tool call. E.g. change some of the parameters
|
||||
thread["thread_id"],
|
||||
assistant_id,
|
||||
input=input,
|
||||
stream_mode="values",
|
||||
stream_mode="updates",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
@@ -371,7 +246,7 @@ Let's now say we want to edit the tool call. E.g. change some of the parameters
|
||||
assistantId,
|
||||
{
|
||||
input: input,
|
||||
streamMode: "values",
|
||||
streamMode: "updates",
|
||||
}
|
||||
);
|
||||
|
||||
@@ -390,84 +265,35 @@ Let's now say we want to edit the tool call. E.g. change some of the parameters
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]}
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "", $0)
|
||||
event_type = $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]},
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
]
|
||||
}"
|
||||
```
|
||||
|
||||
Output:
|
||||
To do this, we will use `Command` with a different resume value of `{"action": "update", "data": <tool call args>}`. This will do the following:
|
||||
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'cec11391-84da-464b-bd2a-bd4f0d93b9ee', 'example': False}]}
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'cec11391-84da-464b-bd2a-bd4f0d93b9ee', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01SunSpDurNfcnXppWLPrtjC', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-6326da9f-6061-4e12-8586-482e32ab4cab', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01SunSpDurNfcnXppWLPrtjC', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}]}
|
||||
|
||||
|
||||
To do this, we first need to update the state. We can do this by passing a message in with the **same** id of the message we want to overwrite. This will have the effect of **replacing** that old message. Note that this is only possible because of the **reducer** we are using that replaces messages with the same ID - read more about that [here](https://langchain-ai.github.io/langgraph/concepts/low_level/#working-with-messages-in-graph-state).
|
||||
* combine existing tool call with user-provided tool call arguments and update the existing AI message with the new tool call
|
||||
* navigate to `run_tool` node with the updated AI message and continue execution
|
||||
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
# To get the ID of the message we want to replace, we need to fetch the current state and find it there.
|
||||
state = await client.threads.get_state(thread['thread_id'])
|
||||
print("Current State:")
|
||||
print(state['values'])
|
||||
print("\nCurrent Tool Call ID:")
|
||||
current_content = state['values']['messages'][-1]['content']
|
||||
current_id = state['values']['messages'][-1]['id']
|
||||
tool_call_id = state['values']['messages'][-1]['tool_calls'][0]['id']
|
||||
print(tool_call_id)
|
||||
# highlight-next-line
|
||||
from langgraph_sdk.schema import Command
|
||||
|
||||
# We now need to construct a replacement tool call.
|
||||
# We will change the argument to be `San Francisco, USA`
|
||||
# Note that we could change any number of arguments or tool names - it just has to be a valid one
|
||||
new_message = {
|
||||
"role": "assistant",
|
||||
"content": current_content,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": tool_call_id,
|
||||
"name": "weather_search",
|
||||
"args": {"city": "San Francisco, USA"}
|
||||
}
|
||||
],
|
||||
# This is important - this needs to be the same as the message you replacing!
|
||||
# Otherwise, it will show up as a separate message
|
||||
"id": current_id
|
||||
}
|
||||
await client.threads.update_state(
|
||||
# This is the config which represents this thread
|
||||
thread['thread_id'],
|
||||
# This is the updated value we want to push
|
||||
{"messages": [new_message]},
|
||||
# We push this update acting as our human_review_node
|
||||
as_node="human_review_node"
|
||||
)
|
||||
|
||||
print("\nResuming Execution")
|
||||
# Let's now continue executing from here
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistant_id,
|
||||
input=None,
|
||||
# highlight-next-line
|
||||
command=Command(
|
||||
# highlight-next-line
|
||||
resume={"action": "update", "data": {"city": "San Francisco, USA"}}
|
||||
# highlight-next-line
|
||||
),
|
||||
stream_mode="updates",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
@@ -476,51 +302,21 @@ To do this, we first need to update the state. We can do this by passing a messa
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const state = await client.threads.getState(thread.thread_id);
|
||||
console.log("Current State:");
|
||||
console.log(state.values);
|
||||
|
||||
console.log("\nCurrent Tool Call ID:");
|
||||
const lastMessage = state.values.messages[state.values.messages.length - 1];
|
||||
const currentContent = lastMessage.content;
|
||||
const currentId = lastMessage.id;
|
||||
const toolCallId = lastMessage.tool_calls[0].id;
|
||||
console.log(toolCallId);
|
||||
|
||||
// Construct a replacement tool call
|
||||
const newMessage = {
|
||||
role: "assistant",
|
||||
content: currentContent,
|
||||
tool_calls: [
|
||||
{
|
||||
id: toolCallId,
|
||||
name: "weather_search",
|
||||
args: { city: "San Francisco, USA" }
|
||||
}
|
||||
],
|
||||
// Ensure the ID is the same as the message you're replacing
|
||||
id: currentId
|
||||
};
|
||||
|
||||
await client.threads.updateState(
|
||||
thread.thread_id, // Thread ID
|
||||
{
|
||||
values: { "messages": [newMessage] }, // Updated message
|
||||
asNode: "human_review_node"
|
||||
} // Acting as human_review_node
|
||||
);
|
||||
|
||||
console.log("\nResuming Execution");
|
||||
// Continue executing from here
|
||||
const streamResponseResumed = client.runs.stream(
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: null,
|
||||
// highlight-next-line
|
||||
command: {
|
||||
// highlight-next-line
|
||||
resume: { "action": "update", "data": { "city": "San Francisco, USA" } }
|
||||
// highlight-next-line
|
||||
},
|
||||
streamMode: "updates"
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponseResumed) {
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
@@ -531,76 +327,37 @@ To do this, we first need to update the state. We can do this by passing a messa
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"values\": { \"messages\": [$(curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state |
|
||||
jq -c '{
|
||||
role: "assistant",
|
||||
content: .values.messages[-1].content,
|
||||
tool_calls: [
|
||||
{
|
||||
id: .values.messages[-1].tool_calls[0].id,
|
||||
name: "weather_search",
|
||||
args: { city: "San Francisco, USA" }
|
||||
}
|
||||
],
|
||||
id: .values.messages[-1].id
|
||||
}')
|
||||
]},
|
||||
\"as_node\": \"human_review_node\"
|
||||
}" && echo "Resuming Execution" && curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"assistant_id": "agent"
|
||||
}' | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "", $0)
|
||||
event_type = $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"command\": {
|
||||
\"resume\": { \"action\": \"update\", \"data\": { \"city\": \"San Francisco, USA\" } }
|
||||
},
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
]
|
||||
}"
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
Current State:
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '8713d1fa-9b26-4eab-b768-dafdaac70590', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-ede13f26-daf5-4d8f-817a-7611075bbcf1', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}]}
|
||||
|
||||
Current Tool Call ID:
|
||||
toolu_01VzagzsUGZsNMwW1wHkcw7h
|
||||
|
||||
Resuming Execution
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '8713d1fa-9b26-4eab-b768-dafdaac70590', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ede13f26-daf5-4d8f-817a-7611075bbcf1', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '7fc7d463-66bf-4555-9929-6af483de169b', 'tool_call_id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'artifact': None, 'status': 'success'}]}
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '8713d1fa-9b26-4eab-b768-dafdaac70590', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ede13f26-daf5-4d8f-817a-7611075bbcf1', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '7fc7d463-66bf-4555-9929-6af483de169b', 'tool_call_id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'artifact': None, 'status': 'success'}, {'content': [{'text': "\n\nBased on the search result, the weather in San Francisco is sunny! It's a beautiful day in the city by the bay. Is there anything else you'd like to know about the weather or any other information I can help you with?", 'type': 'text', 'index': 0}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-d90ce97a-39f9-4330-985e-67c5f351a0c5', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 455, 'output_tokens': 52, 'total_tokens': 507}}]}
|
||||
{'human_review_node': {'messages': [{'role': 'ai', 'content': [{'text': "I'll help you check the weather in San Francisco.", 'type': 'text'}, {'id': 'toolu_016L4EDPcaQRzzZxiB4Wq2wa', 'input': {'city': 'San Francisco'}, 'name': 'weather_search', 'type': 'tool_use'}], 'tool_calls': [{'id': 'toolu_016L4EDPcaQRzzZxiB4Wq2wa', 'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}}], 'id': 'run-b07f0c35-4e93-43a5-9b48-363767ada3ca-0'}]}}
|
||||
{'run_tool': {'messages': [{'role': 'tool', 'name': 'weather_search', 'content': 'Sunny!', 'tool_call_id': 'toolu_016L4EDPcaQRzzZxiB4Wq2wa'}]}}
|
||||
{'call_llm': {'messages': [{'content': "According to the search, it's sunny in San Francisco right now!", 'additional_kwargs': {}, 'response_metadata': {'id': 'msg_01De5HurjNUMwMUpfRtMLbX1', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 460, 'output_tokens': 18}, 'model_name': 'claude-3-5-sonnet-20241022'}, 'type': 'ai', 'name': None, 'id': 'run-85e2aaaa-6f61-4fa0-b594-b6e57129d7e7-0', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 460, 'output_tokens': 18, 'total_tokens': 478, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}]}}
|
||||
|
||||
## Give feedback to a tool call
|
||||
|
||||
Sometimes, you may not want to execute a tool call, but you also may not want to ask the user to manually modify the tool call. In that case it may be better to get natural language feedback from the user. You can then insert these feedback as a mock **RESULT** of the tool call.
|
||||
Sometimes, you may not want to execute a tool call, but you also may not want to ask the user to manually modify the tool call. In that case it may be better to get natural language feedback from the user. You can then insert this feedback as a mock **RESULT** of the tool call.
|
||||
|
||||
There are multiple ways to do this:
|
||||
|
||||
You could add a new message to the state (representing the "result" of a tool call)
|
||||
You could add TWO new messages to the state - one representing an "error" from the tool call, other HumanMessage representing the feedback
|
||||
Both are similar in that they involve adding messages to the state. The main difference lies in the logic AFTER the `human_node` and how it handles different types of messages.
|
||||
1. You could add a new message to the state (representing the "result" of a tool call)
|
||||
2. You could add TWO new messages to the state - one representing an "error" from the tool call, other HumanMessage representing the feedback
|
||||
|
||||
For this example we will just add a single tool call representing the feedback. Let's see this in action!
|
||||
Both are similar in that they involve adding messages to the state. The main difference lies in the logic AFTER the `human_review_node` and how it handles different types of messages.
|
||||
|
||||
For this example we will just add a single tool call representing the feedback (see `human_review_node` implementation). Let's see this in action!
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -611,6 +368,7 @@ For this example we will just add a single tool call representing the feedback.
|
||||
thread["thread_id"],
|
||||
assistant_id,
|
||||
input=input,
|
||||
stream_mode="updates",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
@@ -626,6 +384,7 @@ For this example we will just add a single tool call representing the feedback.
|
||||
assistantId,
|
||||
{
|
||||
input: input,
|
||||
streamMode: "updates"
|
||||
}
|
||||
);
|
||||
|
||||
@@ -644,74 +403,35 @@ For this example we will just add a single tool call representing the feedback.
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]}
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "", $0)
|
||||
event_type = $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]},
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
]
|
||||
}"
|
||||
```
|
||||
|
||||
Output:
|
||||
To do this, we will use `Command` with a different resume value of `{"action": "feedback", "data": <feedback string>}`. This will do the following:
|
||||
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c80f13d0-674d-4233-b6a0-3940509d3cf3', 'example': False}]}
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c80f13d0-674d-4233-b6a0-3940509d3cf3', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_016XyTdFA8NuPWeLyZPSzoM3', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-4911ac27-3d7c-4edf-a3ca-c2908e3922eb', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_016XyTdFA8NuPWeLyZPSzoM3', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}]}
|
||||
|
||||
To do this, we first need to update the state. We can do this by passing a message in with the same **tool call id** of the tool call we want to respond to. Note that this is a **different*** ID from above
|
||||
* create a new tool message that combines existing tool call from LLM with the with user-provided feedback as content
|
||||
* navigate to `call_llm` node with the updated tool message and continue execution
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
# To get the ID of the message we want to replace, we need to fetch the current state and find it there.
|
||||
state = await client.threads.get_state(thread['thread_id'])
|
||||
print("Current State:")
|
||||
print(state['values'])
|
||||
print("\nCurrent Tool Call ID:")
|
||||
tool_call_id = state['values']['messages'][-1]['tool_calls'][0]['id']
|
||||
print(tool_call_id)
|
||||
# highlight-next-line
|
||||
from langgraph_sdk.schema import Command
|
||||
|
||||
# We now need to construct a replacement tool call.
|
||||
# We will change the argument to be `San Francisco, USA`
|
||||
# Note that we could change any number of arguments or tool names - it just has to be a valid one
|
||||
new_message = {
|
||||
"role": "tool",
|
||||
# This is our natural language feedback
|
||||
"content": "User requested changes: pass in the country as well",
|
||||
"name": "weather_search",
|
||||
"tool_call_id": tool_call_id
|
||||
}
|
||||
await client.threads.update_state(
|
||||
# This is the config which represents this thread
|
||||
thread['thread_id'],
|
||||
# This is the updated value we want to push
|
||||
{"messages": [new_message]},
|
||||
# We push this update acting as our human_review_node
|
||||
as_node="human_review_node"
|
||||
)
|
||||
|
||||
print("\nResuming execution")
|
||||
# Let's now continue executing from here
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistant_id,
|
||||
input=None,
|
||||
stream_mode="values",
|
||||
# highlight-next-line
|
||||
command=Command(
|
||||
resume={
|
||||
"action": "feedback",
|
||||
"data": "User requested changes: use <city, country> format for location"
|
||||
}
|
||||
),
|
||||
stream_mode="updates",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
@@ -720,133 +440,22 @@ To do this, we first need to update the state. We can do this by passing a messa
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const state = await client.threads.getState(thread.thread_id);
|
||||
console.log("Current State:");
|
||||
console.log(state.values);
|
||||
|
||||
console.log("\nCurrent Tool Call ID:");
|
||||
const lastMessage = state.values.messages[state.values.messages.length - 1];
|
||||
const toolCallId = lastMessage.tool_calls[0].id;
|
||||
console.log(toolCallId);
|
||||
|
||||
// Construct a replacement tool call
|
||||
const newMessage = {
|
||||
role: "tool",
|
||||
content: "User requested changes: pass in the country as well",
|
||||
name: "weather_search",
|
||||
tool_call_id: toolCallId,
|
||||
};
|
||||
|
||||
await client.threads.updateState(
|
||||
thread.thread_id, // Thread ID
|
||||
{
|
||||
values: { "messages": [newMessage] }, // Updated message
|
||||
asNode: "human_review_node"
|
||||
} // Acting as human_review_node
|
||||
);
|
||||
|
||||
console.log("\nResuming Execution");
|
||||
// Continue executing from here
|
||||
const streamResponseEdited = client.runs.stream(
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: null,
|
||||
streamMode: "values",
|
||||
interruptBefore: ["action"],
|
||||
// highlight-next-line
|
||||
command: {
|
||||
resume: {
|
||||
"action": "feedback",
|
||||
"data": "User requested changes: use <city, country> format for location"
|
||||
}
|
||||
},
|
||||
streamMode: "updates"
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponseEdited) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"values\": { \"messages\": [$(curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state |
|
||||
jq -c '{
|
||||
role: "tool",
|
||||
content: "User requested changes: pass in the country as well",
|
||||
name: "get_weather",
|
||||
tool_call_id: .values.messages[-1].id.tool_calls[0].id
|
||||
}')
|
||||
]},
|
||||
\"as_node\": \"human_review_node\"
|
||||
}" && echo "Resuming Execution" && curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"assistant_id": "agent"
|
||||
}' | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "", $0)
|
||||
event_type = $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
|
||||
Output:
|
||||
|
||||
Current State:
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '3b2bbc38-d11b-49eb-80c0-c24a40dab5a8', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-c5a50900-abf5-4885-9cdb-da2bf0d892ac', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}]}
|
||||
|
||||
Current Tool Call ID:
|
||||
toolu_01NNw18j57GEGPZvsa9f1wvX
|
||||
|
||||
Resuming execution
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '3b2bbc38-d11b-49eb-80c0-c24a40dab5a8', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-c5a50900-abf5-4885-9cdb-da2bf0d892ac', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}, {'content': 'User requested changes: pass in the country as well', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '787288be-213c-4fd3-8503-4a009bdb1b00', 'tool_call_id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'artifact': None, 'status': 'success'}, {'content': [{'text': '\n\nI apologize for the oversight. It seems the function requires additional information. Let me try again with a more specific request.', 'type': 'text', 'index': 0}, {'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco, USA"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-5c355a56-cfe3-4046-b49f-f5b09fc397ef', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 461, 'output_tokens': 83, 'total_tokens': 544}}]}
|
||||
|
||||
We can see that we now get to another breakpoint - because it went back to the model and got an entirely new prediction of what to call. Let's now approve this one and continue
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistant_id,
|
||||
input=None,
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const streamResponseResumed = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: null,
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponseResumed) {
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
@@ -860,31 +469,81 @@ We can see that we now get to another breakpoint - because it went back to the m
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\"
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "", $0)
|
||||
event_type = $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
\"assistant_id\": \"agent\",
|
||||
\"command\": {
|
||||
\"resume\": { \"action\": \"feedback\", \"data\": \"User requested changes: use <city, country> format for location\" }
|
||||
},
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
]
|
||||
}"
|
||||
```
|
||||
|
||||
|
||||
Output:
|
||||
|
||||
{'human_review_node': {'messages': [{'role': 'tool', 'content': 'User requested changes: use <city, country> format for location', 'name': 'weather_search', 'tool_call_id': 'toolu_01RkPHCjpfoUvPAktaq4Cqhm'}]}}
|
||||
{'call_llm': {'messages': [{'content': [{'text': 'Let me try that again with the correct format:', 'type': 'text'}, {'id': 'toolu_01Rdrag6cVufHZG26BwVaiE7', 'input': {'city': 'San Francisco, USA'}, 'name': 'weather_search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {'id': 'msg_01EBan969yY5f6iGk6sPgKcj', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 469, 'output_tokens': 68}, 'model_name': 'claude-3-5-sonnet-20241022'}, 'type': 'ai', 'name': None, 'id': 'run-64bbc255-d126-4db0-8ae5-3197cf29bed1-0', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01Rdrag6cVufHZG26BwVaiE7', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 469, 'output_tokens': 68, 'total_tokens': 537, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}]}}
|
||||
{'__interrupt__': [{'value': {'question': 'Is this correct?', 'tool_call': {'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01Rdrag6cVufHZG26BwVaiE7', 'type': 'tool_call'}}, 'resumable': True, 'ns': ['human_review_node:e9856878-e28c-5dd1-d353-4d83aa1a3a2b'], 'when': 'during'}]}
|
||||
|
||||
We can see that we now get to another interrupt - because it went back to the model and got an entirely new prediction of what to call. Let's now approve this one and continue.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
# highlight-next-line
|
||||
from langgraph_sdk.schema import Command
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistant_id,
|
||||
# highlight-next-line
|
||||
command=Command(resume={"action": "continue"}),
|
||||
stream_mode="updates",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
// highlight-next-line
|
||||
command: { resume: { "action": "continue" } },
|
||||
streamMode: "updates"
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"command\": {
|
||||
\"resume\": { \"action\": \"continue\"}
|
||||
},
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
]
|
||||
}"
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '3b2bbc38-d11b-49eb-80c0-c24a40dab5a8', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-c5a50900-abf5-4885-9cdb-da2bf0d892ac', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}, {'content': 'User requested changes: pass in the country as well', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '787288be-213c-4fd3-8503-4a009bdb1b00', 'tool_call_id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'artifact': None, 'status': 'success'}, {'content': [{'text': '\n\nI apologize for the oversight. It seems the function requires additional information. Let me try again with a more specific request.', 'type': 'text', 'index': 0}, {'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco, USA"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-5c355a56-cfe3-4046-b49f-f5b09fc397ef', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 461, 'output_tokens': 83, 'total_tokens': 544}}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '3b857482-bca2-4a73-a9ab-1f35a3e43e5f', 'tool_call_id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'artifact': None, 'status': 'success'}]}
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '3b2bbc38-d11b-49eb-80c0-c24a40dab5a8', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-c5a50900-abf5-4885-9cdb-da2bf0d892ac', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}, {'content': 'User requested changes: pass in the country as well', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '787288be-213c-4fd3-8503-4a009bdb1b00', 'tool_call_id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'artifact': None, 'status': 'success'}, {'content': [{'text': '\n\nI apologize for the oversight. It seems the function requires additional information. Let me try again with a more specific request.', 'type': 'text', 'index': 0}, {'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco, USA"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-5c355a56-cfe3-4046-b49f-f5b09fc397ef', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 461, 'output_tokens': 83, 'total_tokens': 544}}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '3b857482-bca2-4a73-a9ab-1f35a3e43e5f', 'tool_call_id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'artifact': None, 'status': 'success'}, {'content': [{'text': "\n\nGreat news! The weather in San Francisco is sunny today. Is there anything else you'd like to know about the weather or any other information I can help you with?", 'type': 'text', 'index': 0}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-6a857bb1-f65b-4b86-93d6-c025e003c777', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 557, 'output_tokens': 38, 'total_tokens': 595}}]}
|
||||
{'human_review_node': None}
|
||||
{'run_tool': {'messages': [{'role': 'tool', 'name': 'weather_search', 'content': 'Sunny!', 'tool_call_id': 'toolu_01Rdrag6cVufHZG26BwVaiE7'}]}}
|
||||
{'call_llm': {'messages': [{'content': 'The weather in San Francisco is sunny!', 'additional_kwargs': {}, 'response_metadata': {'id': 'msg_013WTDHhbg8WiYLiQ9n2CaTk', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 550, 'output_tokens': 12}, 'model_name': 'claude-3-5-sonnet-20241022'}, 'type': 'ai', 'name': None, 'id': 'run-b6c815f0-989a-47cf-b150-33e3bbc4eab7-0', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 550, 'output_tokens': 12, 'total_tokens': 562, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}]}}
|
||||
@@ -1,16 +1,16 @@
|
||||
# How to Wait for User Input
|
||||
# How to wait for user input using `interrupt`
|
||||
|
||||
One of the main human-in-the-loop interaction patterns is waiting for human input. A key use case involves asking the user clarifying questions. One way to accomplish this is simply go to the `END` node and exit the graph. Then, any user response comes back in as fresh invocation of the graph. This is basically just creating a chatbot architecture.
|
||||
!!! tip "Prerequisites"
|
||||
|
||||
The issue with this is it is tough to resume back in a particular point in the graph. Often times the agent is halfway through some process, and just needs a bit of a user input. Although it is possible to design your graph in such a way where you have a `conditional_entry_point` to route user messages back to the right place, that is not super scalable (as it essentially involves having a routing function that can end up almost anywhere).
|
||||
This guide assumes familiarity with the following concepts:
|
||||
|
||||
A separate way to do this is to have a node explicitly for getting user input. This is easy to implement in a notebook setting - you just put an `input()` call in the node. But that isn't exactly production ready.
|
||||
* [Human-in-the-loop](../../concepts/human_in_the_loop.md)
|
||||
* [LangGraph Glossary](../../concepts/low_level.md)
|
||||
|
||||
|
||||
Luckily, LangGraph makes it possible to do similar things in a production way. The basic idea is:
|
||||
**Human-in-the-loop (HIL)** interactions are crucial for [agentic systems](../../concepts/agentic_concepts.md#human-in-the-loop). Waiting for human input is a common HIL interaction pattern, allowing the agent to ask the user clarifying questions and await input before proceeding.
|
||||
|
||||
- Set up a node that represents human input. This can have specific incoming/outgoing edges (as you desire). There shouldn't actually be any logic inside this node.
|
||||
- Add a breakpoint before the node. This will stop the graph before this node executes (which is good, because there's no real logic in it anyways)
|
||||
- Use `.update_state` to update the state of the graph. Pass in whatever human response you get. The key here is to use the `as_node` parameter to apply this update **as if you were that node**. This will have the effect of making it so that when you resume execution next it resumes as if that node just acted, and not from the beginning.
|
||||
We can implement this in LangGraph using the [`interrupt()`][langgraph.types.interrupt] function. `interrupt` allows us to stop graph execution to collect input from a user and continue execution with collected input.
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -54,7 +54,7 @@ First, we need to setup our client so that we can communicate with our hosted gr
|
||||
|
||||
### Initial invocation
|
||||
|
||||
Now, let's invoke our graph by interrupting before `ask_human` node:
|
||||
Now, let's invoke our graph.
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -73,7 +73,6 @@ Now, let's invoke our graph by interrupting before `ask_human` node:
|
||||
assistant_id,
|
||||
input=input,
|
||||
stream_mode="updates",
|
||||
interrupt_before=["ask_human"],
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
@@ -95,7 +94,6 @@ Now, let's invoke our graph by interrupting before `ask_human` node:
|
||||
{
|
||||
input: input,
|
||||
streamMode: "updates",
|
||||
interruptBefore: ["ask_human"]
|
||||
}
|
||||
);
|
||||
|
||||
@@ -115,117 +113,34 @@ Now, let's invoke our graph by interrupting before `ask_human` node:
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"Ask the user where they are, then look up the weather there\"}]},
|
||||
\"interrupt_before\": [\"ask_human\"],
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
]
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "", $0)
|
||||
event_type = $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
}"
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'agent': {'messages': [{'content': [{'text': "Certainly! I'll use the AskHuman function to ask the user about their location, and then I'll use the search function to look up the weather for that location. Let's start by asking the user where they are.", 'type': 'text'}, {'id': 'toolu_01RFahzYPvnPWTb2USk2RdKR', 'input': {'question': 'Where are you currently located?'}, 'name': 'AskHuman', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-a8422215-71d3-4093-afb4-9db141c94ddb', 'example': False, 'tool_calls': [{'name': 'AskHuman', 'args': {'question': 'Where are you currently located?'}, 'id': 'toolu_01RFahzYPvnPWTb2USk2RdKR'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
|
||||
{'agent': {'messages': [{'content': [{'text': "I'll help you ask the user about their location and then search for weather information.", 'type': 'text'}, {'id': 'toolu_012JeNEvyePZFWK39d52Wdwi', 'input': {'question': 'Where are you located?'}, 'name': 'AskHuman', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {'id': 'msg_01UBEdS6UvuFMetdokNsykVG', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 438, 'output_tokens': 76}, 'model_name': 'claude-3-5-sonnet-20241022'}, 'type': 'ai', 'name': None, 'id': 'run-1b1210d8-39e0-4607-9f0e-0ea932d28d5c-0', 'example': False, 'tool_calls': [{'name': 'AskHuman', 'args': {'question': 'Where are you located?'}, 'id': 'toolu_012JeNEvyePZFWK39d52Wdwi', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 438, 'output_tokens': 76, 'total_tokens': 514, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}]}}
|
||||
{'__interrupt__': [{'value': 'Where are you located?', 'resumable': True, 'ns': ['ask_human:2d41f894-f297-211e-9bfe-1d162ecba54a'], 'when': 'during'}]}
|
||||
|
||||
You can see that our graph got interrupted inside the `ask_human` node, which is now waiting for a `location` to be provided.
|
||||
|
||||
### Adding user input to state
|
||||
|
||||
We now want to update this thread with a response from the user. We then can kick off another run.
|
||||
|
||||
Because we are treating this as a tool call, we will need to update the state as if it is a response from a tool call. In order to do this, we will need to check the state to get the ID of the tool call.
|
||||
### Providing human input
|
||||
|
||||
We can provide human input (`location`) by invoking the graph with a `Command(resume="<location>")`:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
state = await client.threads.get_state(thread['thread_id'])
|
||||
tool_call_id = state['values']['messages'][-1]['tool_calls'][0]['id']
|
||||
# highlight-next-line
|
||||
from langgraph_sdk.schema import Command
|
||||
|
||||
# We now create the tool call with the id and the response we want
|
||||
tool_message = [{"tool_call_id": tool_call_id, "type": "tool", "content": "san francisco"}]
|
||||
|
||||
await client.threads.update_state(thread['thread_id'], {"messages": tool_message}, as_node="ask_human")
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const state = await client.threads.getState(thread["thread_id"]);
|
||||
const toolCallId = state.values.messages[state.values.messages.length - 1].tool_calls[0].id;
|
||||
|
||||
// We now create the tool call with the id and the response we want
|
||||
const toolMessage = [
|
||||
{
|
||||
tool_call_id: toolCallId,
|
||||
type: "tool",
|
||||
content: "san francisco"
|
||||
}
|
||||
];
|
||||
|
||||
await client.threads.updateState(
|
||||
thread["thread_id"],
|
||||
{ values: { messages: toolMessage } },
|
||||
{ asNode: "ask_human" }
|
||||
);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
|
||||
| jq -r '.values.messages[-1].tool_calls[0].id' \
|
||||
| sh -c '
|
||||
TOOL_CALL_ID="$1"
|
||||
|
||||
# Construct the JSON payload
|
||||
JSON_PAYLOAD=$(printf "{\"messages\": [{\"tool_call_id\": \"%s\", \"type\": \"tool\", \"content\": \"san francisco\"}], \"as_node\": \"ask_human\"}" "$TOOL_CALL_ID")
|
||||
|
||||
# Send the updated state
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
|
||||
--header "Content-Type: application/json" \
|
||||
--data "${JSON_PAYLOAD}"
|
||||
' _
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'configurable': {'thread_id': 'a9f322ae-4ed1-41ec-942b-38cb3d342c3a',
|
||||
'checkpoint_ns': '',
|
||||
'checkpoint_id': '1ef58e97-a623-63dd-8002-39a9a9b20be3'}}
|
||||
|
||||
|
||||
### Invoking after receiving human input
|
||||
|
||||
We can now tell the agent to continue. We can just pass in None as the input to the graph, since no additional input is needed:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistant_id,
|
||||
input=None,
|
||||
# highlight-next-line
|
||||
command=Command(resume="san francisco"),
|
||||
stream_mode="updates",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
@@ -238,7 +153,8 @@ We can now tell the agent to continue. We can just pass in None as the input to
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: null,
|
||||
// highlight-next-line
|
||||
command: { resume: "san francisco" },
|
||||
streamMode: "updates"
|
||||
}
|
||||
);
|
||||
@@ -253,40 +169,23 @@ We can now tell the agent to continue. We can just pass in None as the input to
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"command\": {
|
||||
\"resume\": \"san francisco\"
|
||||
},
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
]
|
||||
}"| \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "", $0)
|
||||
event_type = $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
}"
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'agent': {'messages': [{'content': [{'text': "Thank you for letting me know that you're in San Francisco. Now, I'll use the search function to look up the weather in San Francisco.", 'type': 'text'}, {'id': 'toolu_01K57ofmgG2wyJ8tYJjbq5k7', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-241baed7-db5e-44ce-ac3c-56431705c22b', 'example': False, 'tool_calls': [{'name': 'search', 'args': {'query': 'current weather in San Francisco'}, 'id': 'toolu_01K57ofmgG2wyJ8tYJjbq5k7'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
|
||||
{'action': {'messages': [{'content': '["I looked up: current weather in San Francisco. Result: It\'s sunny in San Francisco, but you better look out if you\'re a Gemini 😈."]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': '8b699b95-8546-4557-8e66-14ea71a15ed8', 'tool_call_id': 'toolu_01K57ofmgG2wyJ8tYJjbq5k7'}]}}
|
||||
{'agent': {'messages': [{'content': "Based on the search results, I can provide you with information about the current weather in San Francisco:\n\nThe weather in San Francisco is currently sunny. It's a beautiful day in the city! \n\nHowever, I should note that the search result included an unusual comment about Gemini zodiac signs. This appears to be either a joke or potentially irrelevant information added by the search engine. For accurate and detailed weather information, you might want to check a reliable weather service or app for San Francisco.\n\nIs there anything else you'd like to know about the weather or San Francisco?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-b4d7309f-f849-46aa-b6ef-475bcabd2be9', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
|
||||
|
||||
{'ask_human': {'messages': [{'tool_call_id': 'toolu_012JeNEvyePZFWK39d52Wdwi', 'type': 'tool', 'content': 'san francisco'}]}}
|
||||
{'agent': {'messages': [{'content': [{'text': 'Let me search for the weather in San Francisco.', 'type': 'text'}, {'id': 'toolu_019f9Y7ST6rNeDQkDjFCHk6C', 'input': {'query': 'current weather in san francisco'}, 'name': 'search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {'id': 'msg_0152YFm7DtnzfZQuiMUzaSsw', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 527, 'output_tokens': 67}, 'model_name': 'claude-3-5-sonnet-20241022'}, 'type': 'ai', 'name': None, 'id': 'run-f509b5b2-eb30-4200-a8da-fa79ed68812a-0', 'example': False, 'tool_calls': [{'name': 'search', 'args': {'query': 'current weather in san francisco'}, 'id': 'toolu_019f9Y7ST6rNeDQkDjFCHk6C', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 527, 'output_tokens': 67, 'total_tokens': 594, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}]}}
|
||||
{'action': {'messages': [{'content': "I looked up: current weather in san francisco. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': 'cbd0f623-cc12-48a2-8c18-3cbb943e46e0', 'tool_call_id': 'toolu_019f9Y7ST6rNeDQkDjFCHk6C', 'artifact': None, 'status': 'success'}]}}
|
||||
{'agent': {'messages': [{'content': "Based on the search results, it's currently sunny in San Francisco. Would you like any specific details about the weather forecast?", 'additional_kwargs': {}, 'response_metadata': {'id': 'msg_01FhzXj72CehBYkJGX69vsBc', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 639, 'output_tokens': 29}, 'model_name': 'claude-3-5-sonnet-20241022'}, 'type': 'ai', 'name': None, 'id': 'run-f48e818e-dd88-415e-9a0b-4a958498b553-0', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 639, 'output_tokens': 29, 'total_tokens': 668, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}]}}
|
||||
@@ -119,11 +119,11 @@ With this set up, running your graph and viewing in LangGraph Studio will result
|
||||
|
||||
**Note the configuration icon in the top right corner of the `call_model` node**:
|
||||
|
||||
{width=1200}
|
||||
{width=1200}
|
||||
|
||||
Clicking this icon will open a modal where you can edit the configuration for all of the fields associated with the `call_model` node. From here, you can save your changes and apply them to the graph. Note that these values reflect the currently active assistant, and saving will update the assistant with the new values.
|
||||
|
||||
{width=1200}
|
||||
{width=1200}
|
||||
|
||||
### Playground
|
||||
|
||||
@@ -133,7 +133,7 @@ LangGraph Studio also supports prompt engineering through an integration with th
|
||||
2. Within the thread log, any nodes that have made an LLM call will have a "View LLM Runs" button. Clicking this will open a popover with the LLM runs for that node.
|
||||
3. Select the LLM run you want to edit. This will open the LangSmith Playground with the selected LLM run.
|
||||
|
||||
{width=1200}
|
||||
{width=1200}
|
||||
|
||||
From here you can edit the prompt, test different model configurations and re-run just this LLM call without having to re-run the entire graph. When you are happy with your changes, you can copy the updated prompt back into your graph.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# How to stream events
|
||||
|
||||
!!! info "Prerequisites"
|
||||
* [Streaming](../../concepts/streaming.md#streaming-llm-tokens-and-events-astream_events)
|
||||
* [Streaming](../../concepts/streaming.md#streaming-graph-outputs-stream-and-astream)
|
||||
|
||||
This guide covers how to stream events from your graph (`stream_mode="events"`). Depending on the use case and user experience of your LangGraph application, your application may process event types differently.
|
||||
|
||||
|
||||
@@ -476,7 +476,7 @@ The `useStream()` hook provides several callback options to help you respond to
|
||||
- `onError`: Called when an error occurs.
|
||||
- `onFinish`: Called when the stream is finished.
|
||||
- `onUpdateEvent`: Called when an update event is received.
|
||||
- `onCustomEvent`: Called when a custom event is received. See [Custom events](../../concepts/streaming.md#custom) to learn how to stream custom events.
|
||||
- `onCustomEvent`: Called when a custom event is received. See [Custom events](../../how-tos/streaming.ipynb#custom) to learn how to stream custom events.
|
||||
- `onMetadataEvent`: Called when a metadata event is received, which contains the Run ID and Thread ID.
|
||||
|
||||
## Learn More
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"info": {
|
||||
"title": "LangGraph Control Plane API (Beta)",
|
||||
"version": "0.0.1",
|
||||
"description": "The LangGraph Control Plane API is used to programmatically create and manage LangGraph Server deployments. For example, the APIs can be orchestrated to create custom CI/CD workflows.\n\n### Beta\nThis API is currently in beta and may change or break without notice. This API documentation may not be up-to-date with actual API functionality.\n### Host\nhttps://api.host.langchain.com/\n\n### Authentication\nTo authenticate with the LangGraph Control Plane API, set the `X-Api-Key` header to a valid LangSmith API key for each request.\n\n### Versioning\nEach endpoint path is prefixed with a version (e.g. `v1`).\n\n### Quick Start\n\n1. Call `GET /{version}/projects` to retrieve the `Project` `id`. The `Project` `id` is needed in subsequent API calls.\n2. Call `POST /{version}/projects/{project_id}/revisions` to create a new `Revision` for the `Project`.\n3. Call `GET /{version}/projects/{project_id}/revisions` to get the latest `Revision` (first element in returned list). Get the `Revision` `id`.\n4. Poll for `Revision` `status` until `status` is `DEPLOYED` by calling `GET /{version}/projects/{project_id}/revisions/{revision_id}`."
|
||||
"description": "The LangGraph Control Plane API is used to programmatically create and manage LangGraph Server deployments. For example, the APIs can be orchestrated to create custom CI/CD workflows.\n\n### Beta\nThis API is currently in beta and may change or break without notice. This API documentation may not be up-to-date with actual API functionality.\n### Host\nhttps://api.host.langchain.com/\n\n### Authentication\nTo authenticate with the LangGraph Control Plane API, set the `X-Api-Key` header to a valid LangSmith API key for each request.\n\n### Versioning\nEach endpoint path is prefixed with a version (e.g. `v1`).\n\n### Quick Start\n\n1. Call `POST /{version}/projects` to create a new `Project`.\n2. Call `GET /{version}/projects` to retrieve the `Project` `id`. The `Project` `id` is needed in subsequent API calls.\n3. Call `POST /{version}/projects/{project_id}/revisions` to create a new `Revision` for the `Project`.\n4. Call `GET /{version}/projects/{project_id}/revisions` to get the latest `Revision` (first element in returned list). Get the `Revision` `id`.\n5. Poll for `Revision` `status` until `status` is `DEPLOYED` by calling `GET /{version}/projects/{project_id}/revisions/{revision_id}`."
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
@@ -22,6 +22,34 @@
|
||||
],
|
||||
"paths": {
|
||||
"/v1/projects": {
|
||||
"post": {
|
||||
"tags": ["Projects (v1)"],
|
||||
"summary": "Create Project",
|
||||
"description": "Create a new project.",
|
||||
"operationId": "create_project_projects_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateProjectRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"tags": ["Projects (v1)"],
|
||||
"summary": "List Projects",
|
||||
@@ -397,6 +425,83 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateProjectRequest":{
|
||||
"type": "object",
|
||||
"description": "Object for creating a new project.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the project.",
|
||||
"required": true
|
||||
},
|
||||
"lc_hosted": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the project is hosted on LangChain's cloud (i.e. Cloud SaaS deployment option). Set to `false` for Self-Hosted Data Plane and Self-Hosted Control Plane deployment options.",
|
||||
"default": true
|
||||
},
|
||||
"repo_url": {
|
||||
"type": ["string", "null"],
|
||||
"description": "URL of the GitHub repository to use for the project. Omit this field if creating a new project from a Docker image.",
|
||||
"default": "null"
|
||||
},
|
||||
"repo_path": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Path to `langgraph.json` configuration file. For example, `langgraph.json` or `src/langgraph.json`.\n\nIf this field is omitted or set to `null`, the previous revision's `repo_path` value is used. Set this field for deployments from a GitHub repository. Omit this field if creating a new revision from a Docker image.",
|
||||
"default": "null"
|
||||
},
|
||||
"repo_commit": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Git branch name of deployment.\n\nThis field only applies to deployments from a GitHub repository.",
|
||||
"default": "null"
|
||||
},
|
||||
"env_vars": {
|
||||
"type": "array",
|
||||
"description": "List of environment variables or secrets.\n\nIf this field is omitted or set to `null`, the previous revision's `env_vars` value is used.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EnvVar"
|
||||
},
|
||||
"default": "null"
|
||||
},
|
||||
"host_integration_id": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uuid",
|
||||
"description": "Do not use."
|
||||
},
|
||||
"deployment_type": {
|
||||
"type": "string",
|
||||
"description": "Development (`dev`) or Production (`prod`) type deployment.",
|
||||
"enum": [
|
||||
"dev",
|
||||
"prod"
|
||||
]
|
||||
},
|
||||
"shareable": {
|
||||
"type": ["boolean", "null"],
|
||||
"description": "Boolean flag to configure if a deployment is shareable through LangGraph Studio.\n\nIf this field is omitted or set to `null`, the previous revision's `shareable` value is used. This field does not apply to BYOC deployments.",
|
||||
"default": "null"
|
||||
},
|
||||
"platform": {
|
||||
"type": "object",
|
||||
"description": "Do not use.",
|
||||
"default": "null"
|
||||
},
|
||||
"image_path": {
|
||||
"type": ["string", "null"],
|
||||
"description": "URI of the Docker image to deploy.\n\nIf this field is omitted or set to `null`, the previous revision's `image_path` value is used. Set this field for BYOC deployments. Omit this field if creating a new revision from a GitHub repository.",
|
||||
"default": "null"
|
||||
},
|
||||
"build_on_push": {
|
||||
"type": "boolean",
|
||||
"description": "Boolean flag to indicate if a new revision is automatically created on push to GitHub branch (`repo_branch`).\n\nThis field does not apply for BYOC deployments.",
|
||||
"default": false
|
||||
},
|
||||
"container_spec": {
|
||||
"description": "If this field is omitted or set to `null`, the previous revision's `container_spec` value is used.",
|
||||
"$ref": "#/components/schemas/ContainerSpec",
|
||||
"default": "null"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateRevisionRequest": {
|
||||
"type": "object",
|
||||
"description": "Object for creating a new revision.",
|
||||
|
||||
@@ -49,7 +49,7 @@ The LangGraph CLI requires a JSON configuration file that follows this [schema](
|
||||
| <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. |
|
||||
| <span style="white-space: nowrap;">`checkpointer`</span> | Configuration for the checkpointer. Contains a `ttl` field which is an object with the following keys: <ul><li>`strategy`: How to handle expired checkpoints (e.g., `"delete"`).</li><li>`sweep_interval_minutes`: How often to check for expired checkpoints (integer).</li><li>`default_ttl`: Default time-to-live for checkpoints in **minutes** (integer). Defines how long checkpoints are kept before the specified strategy is applied.</li></ul> |
|
||||
| <span style="white-space: nowrap;">`http`</span> | HTTP server configuration with the following fields: <ul><li>`app`: Path to custom Starlette/FastAPI app (e.g., `"./src/agent/webapp.py:app"`). See [custom routes guide](../../how-tos/http/custom_routes.md).</li><li>`disable_assistants`: Disable `/assistants` routes</li><li>`disable_threads`: Disable `/threads` routes</li><li>`disable_runs`: Disable `/runs` routes</li><li>`disable_store`: Disable `/store` routes</li><li>`disable_meta`: Disable `/ok`, `/info`, `/metrics`, and `/docs` routes</li><li>`cors`: CORS configuration with fields for `allow_origins`, `allow_methods`, `allow_headers`, etc.</li></ul> |
|
||||
| <span style="white-space: nowrap;">`http`</span> | HTTP server configuration with the following fields: <ul><li>`app`: Path to custom Starlette/FastAPI app (e.g., `"./src/agent/webapp.py:app"`). See [custom routes guide](../../how-tos/http/custom_routes.md).</li><li>`disable_assistants`: Disable `/assistants` routes</li><li>`disable_threads`: Disable `/threads` routes</li><li>`disable_runs`: Disable `/runs` routes</li><li>`disable_store`: Disable `/store` routes</li><li>`disable_meta`: Disable `/ok`, `/info`, `/metrics`, and `/docs` routes</li><li>`cors`: CORS configuration with fields for `allow_origins`, `allow_methods`, `allow_headers`, etc.</li><li>`configurable_headers`: Define which request headers to exclude or include as a run's configurable values.</li></ul> |
|
||||
|
||||
=== "JS"
|
||||
|
||||
|
||||
@@ -97,3 +97,21 @@ Database Connectivity:
|
||||
Custom Redis instances are only available for [Self-Hosted Data Plane](../../concepts/langgraph_self_hosted_data_plane.md) and [Self-Hosted Control Plane](../../concepts/langgraph_self_hosted_control_plane.md) deployments.
|
||||
|
||||
Specify `REDIS_URI_CUSTOM` to use a custom Redis instance. The value of `REDIS_URI_CUSTOM` must be a valid [Redis connection URI](https://redis-py.readthedocs.io/en/stable/connections.html#redis.Redis.from_url).
|
||||
|
||||
## `REDIS_KEY_PREFIX`
|
||||
|
||||
!!! info "Available in API Server version 0.1.9+"
|
||||
This environment variable is supported in API Server version 0.1.9 and above.
|
||||
|
||||
Specify a prefix for Redis keys. This allows multiple LangGraph Server instances to share the same Redis instance by using different key prefixes.
|
||||
|
||||
Defaults to `''`.
|
||||
|
||||
## `REDIS_CLUSTER`
|
||||
|
||||
!!! info "Only Allowed in Self-Hosted Deployments"
|
||||
Redis Cluster mode is only available in Self-Hosted Deployment models, LangGraph Cloud SaaS will provision a redis instance for you by default.
|
||||
|
||||
Set `REDIS_CLUSTER` to `True` to enable Redis Cluster mode. When enabled, the system will connect to Redis using cluster mode. This is useful when connecting to a Redis Cluster deployment.
|
||||
|
||||
Defaults to `False`.
|
||||
|
||||
@@ -15,7 +15,7 @@ This guide shows a typical structure for a LangGraph application and shows how t
|
||||
|
||||
To deploy using the LangGraph Platform, the following information should be provided:
|
||||
|
||||
1. A [LangGraph API Configuration file](#configuration-file) (`langgraph.json`) that specifies the dependencies, graphs, environment variables to use for the application.
|
||||
1. A [LangGraph API Configuration file](#configuration-file-concepts) (`langgraph.json`) that specifies the dependencies, graphs, environment variables to use for the application.
|
||||
2. The [graphs](#graphs) that implement the logic of the application.
|
||||
3. A file that specifies [dependencies](#dependencies) required to run the application.
|
||||
4. [Environment variable](#environment-variables) that are required for the application to run.
|
||||
@@ -77,7 +77,7 @@ Below are examples of directory structures for Python and JavaScript application
|
||||
The directory structure of a LangGraph application can vary depending on the programming language and the package manager used.
|
||||
|
||||
|
||||
## Configuration File
|
||||
## Configuration File {#configuration-file-concepts}
|
||||
|
||||
The `langgraph.json` file is a JSON file that specifies the dependencies, graphs, environment variables, and other settings required to deploy a LangGraph application.
|
||||
|
||||
@@ -145,18 +145,18 @@ A LangGraph application may depend on other Python packages or JavaScript librar
|
||||
You will generally need to specify the following information for dependencies to be set up correctly:
|
||||
|
||||
1. A file in the directory that specifies the dependencies (e.g., `requirements.txt`, `pyproject.toml`, or `package.json`).
|
||||
2. A `dependencies` key in the [LangGraph configuration file](#configuration-file) that specifies the dependencies required to run the LangGraph application.
|
||||
3. Any additional binaries or system libraries can be specified using `dockerfile_lines` key in the [LangGraph configuration file](#configuration-file).
|
||||
2. A `dependencies` key in the [LangGraph configuration file](#configuration-file-concepts) that specifies the dependencies required to run the LangGraph application.
|
||||
3. Any additional binaries or system libraries can be specified using `dockerfile_lines` key in the [LangGraph configuration file](#configuration-file-concepts).
|
||||
|
||||
## Graphs
|
||||
|
||||
Use the `graphs` key in the [LangGraph configuration file](#configuration-file) to specify which graphs will be available in the deployed LangGraph application.
|
||||
Use the `graphs` key in the [LangGraph configuration file](#configuration-file-concepts) to specify which graphs will be available in the deployed LangGraph application.
|
||||
|
||||
You can specify one or more graphs in the configuration file. Each graph is identified by a name (which should be unique) and a path for either: (1) the compiled graph or (2) a function that makes a graph is defined.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
If you're working with a deployed LangGraph application locally, you can configure environment variables in the `env` key of the [LangGraph configuration file](#configuration-file).
|
||||
If you're working with a deployed LangGraph application locally, you can configure environment variables in the `env` key of the [LangGraph configuration file](#configuration-file-concepts).
|
||||
|
||||
For a production deployment, you will typically want to configure the environment variables in the deployment environment.
|
||||
|
||||
|
||||
@@ -409,39 +409,6 @@ The `Command` primitive provides several options to control and modify the graph
|
||||
|
||||
By leveraging `Command`, you can resume graph execution, handle user inputs, and dynamically adjust the graph's state.
|
||||
|
||||
## Using with `invoke` and `ainvoke`
|
||||
|
||||
When you use `stream` or `astream` to run the graph, you will receive an `Interrupt` event that let you know the `interrupt` was triggered.
|
||||
|
||||
`invoke` and `ainvoke` do not return the interrupt information. To access this information, you must use the [get_state](../reference/graphs.md#langgraph.graph.graph.CompiledGraph.get_state) method to retrieve the graph state after calling `invoke` or `ainvoke`.
|
||||
|
||||
```python
|
||||
# Run the graph up to the interrupt
|
||||
result = graph.invoke(inputs, thread_config)
|
||||
# Get the graph state to get interrupt information.
|
||||
state = graph.get_state(thread_config)
|
||||
# Print the state values
|
||||
print(state.values)
|
||||
# Print the pending tasks
|
||||
print(state.tasks)
|
||||
# Resume the graph with the user's input.
|
||||
graph.invoke(Command(resume={"age": "25"}), thread_config)
|
||||
```
|
||||
|
||||
```pycon
|
||||
{'foo': 'bar'} # State values
|
||||
(
|
||||
PregelTask(
|
||||
id='5d8ffc92-8011-0c9b-8b59-9d3545b7e553',
|
||||
name='node_foo',
|
||||
path=('__pregel_pull', 'node_foo'),
|
||||
error=None,
|
||||
interrupts=(Interrupt(value='value_in_interrupt', resumable=True, ns=['node_foo:5d8ffc92-8011-0c9b-8b59-9d3545b7e553'], when='during'),), state=None,
|
||||
result=None
|
||||
),
|
||||
) # Pending tasks. interrupts
|
||||
```
|
||||
|
||||
## How does resuming from an interrupt work?
|
||||
|
||||
!!! warning
|
||||
|
||||
@@ -20,7 +20,7 @@ The LangGraph Platform incorporates best practices for agent deployment, so you
|
||||
* **Double texting support**: Many times users might interact with your graph in unintended ways. For instance, a user may send one message and before the graph has finished running send a second message. We call this ["double texting"](double_texting.md) and have added four different ways to handle this.
|
||||
* **Optimized checkpointer**: LangGraph Platform comes with a built-in [checkpointer](./persistence.md#checkpoints) optimized for LangGraph applications.
|
||||
* **Human-in-the-loop endpoints**: We've exposed all endpoints needed to support [human-in-the-loop](human_in_the_loop.md) features.
|
||||
* **Memory**: In addition to thread-level persistence (covered above by [checkpointers]l(./persistence.md#checkpoints)), LangGraph Platform also comes with a built-in [memory store](persistence.md#memory-store).
|
||||
* **Memory**: In addition to thread-level persistence (covered above by [checkpointers](./persistence.md#checkpoints)), LangGraph Platform also comes with a built-in [memory store](persistence.md#memory-store).
|
||||
* **Cron jobs**: Built-in support for scheduling tasks, enabling you to automate regular actions like data clean-up or batch processing within your applications.
|
||||
* **Webhooks**: Allows your application to send real-time notifications and data updates to external systems, making it easy to integrate with third-party services and trigger actions based on specific events.
|
||||
* **Monitoring**: LangGraph Server integrates seamlessly with the [LangSmith](https://docs.smith.langchain.com/) monitoring platform, providing real-time insights into your application's performance and health.
|
||||
|
||||
@@ -7,19 +7,17 @@
|
||||
|
||||
## Versions
|
||||
|
||||
There are two versions of the self-hosted deployment: [Self-Hosted Enterprise](./deployment_options.md#self-hosted-enterprise) and [Self-Hosted Lite](./deployment_options.md#self-hosted-lite).
|
||||
There are two versions of the self-hosted deployment: [Self-Hosted Data Plane](./deployment_options.md#self-hosted-data-plane) and [Self-Hosted Control Plane](./deployment_options.md#self-hosted-control-plane).
|
||||
|
||||
### Self-Hosted Lite
|
||||
### Self-Hosted Data Plane
|
||||
|
||||
The Self-Hosted Lite version is a limited version of LangGraph Platform that you can run locally or in a self-hosted manner (up to 1 million nodes executed per year).
|
||||
The [Self-Hosted Data Plane](./langgraph_self_hosted_data_plane.md) deployment option is a "hybrid" model for deployment where we manage the [control plane](./langgraph_control_plane.md) in our cloud and you manage the [data plane](./langgraph_data_plane.md) in your cloud. This option provides a way to securely manage your data plane infrastructure, while offloading control plane management to us.
|
||||
|
||||
When using the Self-Hosted Lite version, you authenticate with a [LangSmith](https://smith.langchain.com/) API key.
|
||||
When using the Self-Hosted Data Plane version, you authenticate with a [LangSmith](https://smith.langchain.com/) API key.
|
||||
|
||||
### Self-Hosted Enterprise
|
||||
### Self-Hosted Control Plane
|
||||
|
||||
The Self-Hosted Enterprise version is the full version of LangGraph Platform.
|
||||
|
||||
To use the Self-Hosted Enterprise version, you must acquire a license key that you will need to pass in when running the Docker image. To acquire a license key, please email sales@langchain.dev.
|
||||
The [Self-Hosted Control Plane](./langgraph_self_hosted_control_plane.md) deployment option is a fully self-hosted model for deployment where you manage the [control plane](./langgraph_control_plane.md) and [data plane](./langgraph_data_plane.md) in your cloud. This option give you full control and responsibility of the control plane and data plane infrastructure.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ You will eventually need to pass in the following environment variables to the L
|
||||
|
||||
`<database_name_1>` and `database_name_2` are different databases within the same instance, but `<hostname_1>` is shared. **The same database cannot be used for separate deployments**.
|
||||
|
||||
- `LANGSMITH_API_KEY`: (If using [Self-Hosted Lite](../concepts/deployment_options.md#self-hosted-lite)) LangSmith API key. This will be used to authenticate ONCE at server start up.
|
||||
- `LANGSMITH_API_KEY`: (If using [Self-Hosted Data Plane](../concepts/deployment_options.md#self-hosted-data-plane)) LangSmith API key. This will be used to authenticate ONCE at server start up.
|
||||
- `LANGGRAPH_CLOUD_LICENSE_KEY`: (If using [Self-Hosted Enterprise](../concepts/deployment_options.md#self-hosted-enterprise)) LangGraph Platform license key. This will be used to authenticate ONCE at server start up.
|
||||
- `LANGCHAIN_ENDPOINT`: To send traces to a [self-hosted LangSmith](https://docs.smith.langchain.com/self_hosting) instance, set `LANGCHAIN_ENDPOINT` to the hostname of the self-hosted LangSmith instance.
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -248,8 +248,9 @@ LangGraph Platform supports multiple types of runs besides streaming runs.
|
||||
- [How to run multiple agents in the same thread](../cloud/how-tos/same-thread.md)
|
||||
- [How to create cron jobs](../cloud/how-tos/cron_jobs.md)
|
||||
- [How to create stateless runs](../cloud/how-tos/stateless_runs.md)
|
||||
- [How to use headers as runtime configuration](../cloud/how-tos/configurable_headers.md)
|
||||
|
||||
### Streaming
|
||||
### Streaming {#streaming_1}
|
||||
|
||||
Streaming the results of your LLM application is vital for ensuring a good user experience, especially when your graph may call multiple models and take a long time to fully complete a run. Read about how to stream values from your graph in these how to guides:
|
||||
|
||||
@@ -267,15 +268,21 @@ With LangGraph Platform you can integrate LangGraph agents into your React appli
|
||||
- [How to integrate LangGraph into your React application](../cloud/how-tos/use_stream_react.md)
|
||||
- [How to implement Generative User Interfaces with LangGraph](../cloud/how-tos/generative_ui_react.md)
|
||||
|
||||
### Human-in-the-loop
|
||||
### Human-in-the-loop {#human-in-the-loop-1}
|
||||
|
||||
When designing complex graphs, relying entirely on the LLM for decision-making can be risky, particularly when it involves tools that interact with files, APIs, or databases. These interactions may lead to unintended data access or modifications, depending on the use case. To mitigate these risks, LangGraph allows you to integrate human-in-the-loop behavior, ensuring your LLM applications operate as intended without undesirable outcomes.
|
||||
[Human-in-the-loop](../concepts/human_in_the_loop.md) functionality allows
|
||||
you to involve humans in the decision-making process of your graph. These how-to guides show how to implement human-in-the-loop workflows in your graph.
|
||||
|
||||
- [How to add a breakpoint](../cloud/how-tos/human_in_the_loop_breakpoint.md)
|
||||
- [How to wait for user input](../cloud/how-tos/human_in_the_loop_user_input.md)
|
||||
- [How to review tool calls](../cloud/how-tos/human_in_the_loop_review_tool_calls.md)
|
||||
- [How to add static breakpoints](../cloud/how-tos/human_in_the_loop_breakpoint.md)
|
||||
|
||||
### Time Travel
|
||||
|
||||
[Time travel](../concepts/time-travel.md) allows you to replay past actions in your LangGraph application to explore alternative paths and debug issues. These how-to guides show how to use time travel in your graph.
|
||||
|
||||
- [How to edit graph state](../cloud/how-tos/human_in_the_loop_edit_state.md)
|
||||
- [How to replay and branch from prior states](../cloud/how-tos/human_in_the_loop_time_travel.md)
|
||||
- [How to review tool calls](../cloud/how-tos/human_in_the_loop_review_tool_calls.md)
|
||||
|
||||
### Double-texting
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Using in `create_react_agent`\n",
|
||||
"## Using in `create_react_agent` {#using-in-create-react-agent}\n",
|
||||
"\n",
|
||||
"Add semantic search to your tool calling agent by injecting the store in the `prompt` function. You can also use the store in a tool to let your agent manually store or search for memories."
|
||||
]
|
||||
|
||||
@@ -139,7 +139,7 @@ langgraph dev --no-browser
|
||||
}
|
||||
```
|
||||
|
||||
Now let's try to chat with our bot. If we've implemented authentication correctly, we should only be able to access the bot if we provide a valid token in the request header. Users will still, however, be able to access each other's resources until we add [resource authorization handlers](../../concepts/auth.md#resource-authorization) in the next section of our tutorial.
|
||||
Now let's try to chat with our bot. If we've implemented authentication correctly, we should only be able to access the bot if we provide a valid token in the request header. Users will still, however, be able to access each other's resources until we add [resource authorization handlers](../../concepts/auth.md#resource-specific-handlers) in the next section of our tutorial.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
2. Resource Authorization (you are here) - Let users have private conversations
|
||||
3. [Production Auth](add_auth_server.md) - Add real user accounts and validate using OAuth2
|
||||
|
||||
In this tutorial, we will extend our chatbot to give each user their own private conversations. We'll add [resource-level access control](../../concepts/auth.md#resource-level-access-control) so users can only see their own threads.
|
||||
In this tutorial, we will extend our chatbot to give each user their own private conversations. We'll add [resource-level access control](../../concepts/auth.md#single-owner-resources) so users can only see their own threads.
|
||||
|
||||

|
||||
|
||||
@@ -35,7 +35,7 @@ langgraph dev --no-browser
|
||||
|
||||
Recall that in the last tutorial, the [`Auth`](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) object let us register an [authentication function](../../concepts/auth.md#authentication), which the LangGraph platform uses to validate the bearer tokens in incoming requests. Now we'll use it to register an **authorization** handler.
|
||||
|
||||
Authorization handlers are functions that run **after** authentication succeeds. These handlers can add [metadata](../../concepts/auth.md#resource-metadata) to resources (like who owns them) and filter what each user can see.
|
||||
Authorization handlers are functions that run **after** authentication succeeds. These handlers can add [metadata](../../concepts/auth.md#filter-operations) to resources (like who owns them) and filter what each user can see.
|
||||
|
||||
Let's update our `src/security/auth.py` and add one authorization handler that is run on every request:
|
||||
|
||||
@@ -211,7 +211,7 @@ This means:
|
||||
|
||||
## Adding scoped authorization handlers {#scoped-authorization}
|
||||
|
||||
The broad `@auth.on` handler matches on all [authorization events](../../concepts/auth.md#authorization-events). This is concise, but it means the contents of the `value` dict are not well-scoped, and we apply the same user-level access control to every resource. If we want to be more fine-grained, we can also control specific actions on resources.
|
||||
The broad `@auth.on` handler matches on all [authorization events](../../concepts/auth.md#supported-resources). This is concise, but it means the contents of the `value` dict are not well-scoped, and we apply the same user-level access control to every resource. If we want to be more fine-grained, we can also control specific actions on resources.
|
||||
|
||||
Update `src/security/auth.py` to add handlers for specific resource types:
|
||||
|
||||
@@ -290,7 +290,7 @@ Notice that instead of one global handler, we now have specific handlers for:
|
||||
2. Reading threads
|
||||
3. Accessing assistants
|
||||
|
||||
The first three of these match specific **actions** on each resource (see [resource actions](../../concepts/auth.md#resource-actions)), while the last one (`@auth.on.assistants`) matches _any_ action on the `assistants` resource. For each request, LangGraph will run the most specific handler that matches the resource and action being accessed. This means that the four handlers above will run rather than the broadly scoped "`@auth.on`" handler.
|
||||
The first three of these match specific **actions** on each resource (see [resource actions](../../concepts/auth.md#resource-specific-handlers)), while the last one (`@auth.on.assistants`) matches _any_ action on the `assistants` resource. For each request, LangGraph will run the most specific handler that matches the resource and action being accessed. This means that the four handlers above will run rather than the broadly scoped "`@auth.on`" handler.
|
||||
|
||||
Try adding the following test code to your test file:
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1627,7 +1627,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 83,
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
|
||||
+4
-41
@@ -104,23 +104,19 @@ nav:
|
||||
- How-to Guides:
|
||||
- how-tos/index.md
|
||||
- LangGraph:
|
||||
- LangGraph: how-tos#langgraph
|
||||
- Graph API Basics:
|
||||
- Graph API Basics: how-tos#graph-api-basics
|
||||
- how-tos/state-reducers.ipynb
|
||||
- how-tos/sequence.ipynb
|
||||
- how-tos/branching.ipynb
|
||||
- how-tos/recursion-limit.ipynb
|
||||
- how-tos/visualization.ipynb
|
||||
- Controllability:
|
||||
- Controllability: how-tos#controllability
|
||||
- how-tos/map-reduce.ipynb
|
||||
- how-tos/command.ipynb
|
||||
- how-tos/configuration.ipynb
|
||||
- how-tos/node-retries.ipynb
|
||||
- how-tos/return-when-recursion-limit-hits.ipynb
|
||||
- Persistence:
|
||||
- Persistence: how-tos#persistence
|
||||
- how-tos/persistence.ipynb
|
||||
- how-tos/subgraph-persistence.ipynb
|
||||
- how-tos/cross-thread-persistence.ipynb
|
||||
@@ -130,13 +126,11 @@ nav:
|
||||
- how-tos/persistence-functional.ipynb
|
||||
- how-tos/cross-thread-persistence-functional.ipynb
|
||||
- Memory:
|
||||
- Memory: how-tos#memory
|
||||
- how-tos/memory/manage-conversation-history.ipynb
|
||||
- how-tos/memory/delete-messages.ipynb
|
||||
- how-tos/memory/add-summary-conversation-history.ipynb
|
||||
- how-tos/memory/semantic-search.ipynb
|
||||
- Human-in-the-loop:
|
||||
- Human-in-the-loop: how-tos#human-in-the-loop
|
||||
- how-tos/human_in_the_loop/breakpoints.ipynb
|
||||
- how-tos/human_in_the_loop/dynamic_breakpoints.ipynb
|
||||
- how-tos/human_in_the_loop/edit-graph-state.ipynb
|
||||
@@ -146,7 +140,6 @@ nav:
|
||||
- how-tos/wait-user-input-functional.ipynb
|
||||
- how-tos/review-tool-calls-functional.ipynb
|
||||
- Streaming:
|
||||
- Streaming: how-tos#streaming
|
||||
- how-tos/streaming.ipynb
|
||||
- how-tos/streaming-tokens.ipynb
|
||||
- how-tos/streaming-specific-nodes.ipynb
|
||||
@@ -154,7 +147,6 @@ nav:
|
||||
- how-tos/streaming-subgraphs.ipynb
|
||||
- how-tos/disable-streaming.ipynb
|
||||
- Tool calling:
|
||||
- Tool calling: how-tos#tool-calling
|
||||
- how-tos/tool-calling.ipynb
|
||||
- how-tos/tool-calling-errors.ipynb
|
||||
- how-tos/pass-run-time-values-to-tools.ipynb
|
||||
@@ -162,31 +154,26 @@ nav:
|
||||
- how-tos/pass-config-to-tools.ipynb
|
||||
- how-tos/many-tools.ipynb
|
||||
- Subgraphs:
|
||||
- Subgraphs: how-tos#subgraphs
|
||||
- how-tos/subgraph.ipynb
|
||||
- how-tos/subgraphs-manage-state.ipynb
|
||||
- how-tos/subgraph-transform-state.ipynb
|
||||
- Multi-agent:
|
||||
- Multi-agent: how-tos#multi-agent
|
||||
- how-tos/agent-handoffs.ipynb
|
||||
- how-tos/multi-agent-network.ipynb
|
||||
- how-tos/multi-agent-multi-turn-convo.ipynb
|
||||
- how-tos/multi-agent-network-functional.ipynb
|
||||
- how-tos/multi-agent-multi-turn-convo-functional.ipynb
|
||||
- State Management:
|
||||
- State Management: how-tos#state-management
|
||||
- how-tos/state-model.ipynb
|
||||
- how-tos/input_output_schema.ipynb
|
||||
- how-tos/pass_private_state.ipynb
|
||||
- Other:
|
||||
- Other: how-tos#other
|
||||
- how-tos/async.ipynb
|
||||
- how-tos/react-agent-structured-output.ipynb
|
||||
- how-tos/run-id-langsmith.ipynb
|
||||
- how-tos/autogen-integration.ipynb
|
||||
- how-tos/autogen-integration-functional.ipynb
|
||||
- Prebuilt ReAct Agent:
|
||||
- Prebuilt ReAct Agent: how-tos#prebuilt-react-agent
|
||||
- how-tos/create-react-agent.ipynb
|
||||
- how-tos/create-react-agent-memory.ipynb
|
||||
- how-tos/create-react-agent-system-prompt.ipynb
|
||||
@@ -196,9 +183,7 @@ nav:
|
||||
- how-tos/react-agent-from-scratch.ipynb
|
||||
- how-tos/react-agent-from-scratch-functional.ipynb
|
||||
- LangGraph Platform:
|
||||
- LangGraph Platform: how-tos#langgraph-platform
|
||||
- Application Structure:
|
||||
- Application Structure: how-tos#application-structure
|
||||
- cloud/deployment/setup.md
|
||||
- cloud/deployment/setup_pyproject.md
|
||||
- cloud/deployment/setup_javascript.md
|
||||
@@ -208,7 +193,6 @@ nav:
|
||||
- cloud/deployment/graph_rebuild.md
|
||||
- how-tos/autogen-langgraph-platform.ipynb
|
||||
- Deployment:
|
||||
- Deployment: how-tos#deployment
|
||||
- cloud/deployment/cloud.md
|
||||
- cloud/deployment/self_hosted_data_plane.md
|
||||
- cloud/deployment/self_hosted_control_plane.md
|
||||
@@ -219,25 +203,21 @@ nav:
|
||||
- Data Management:
|
||||
- how-tos/ttl/configure_ttl.md
|
||||
- Authentication & Access Control:
|
||||
- Authentication & Access Control: how-tos#authentication-access-control
|
||||
- how-tos/auth/custom_auth.md
|
||||
- how-tos/auth/openapi_security.md
|
||||
- Assistants:
|
||||
- Assistants: how-tos#assistants
|
||||
- cloud/how-tos/configuration_cloud.md
|
||||
- cloud/how-tos/assistant_versioning.md
|
||||
- Threads:
|
||||
- Threads: how-tos#threads
|
||||
- cloud/how-tos/copy_threads.md
|
||||
- cloud/how-tos/check_thread_status.md
|
||||
- Runs:
|
||||
- Runs: how-tos#runs
|
||||
- cloud/how-tos/background_run.md
|
||||
- cloud/how-tos/same-thread.md
|
||||
- cloud/how-tos/cron_jobs.md
|
||||
- cloud/how-tos/stateless_runs.md
|
||||
- cloud/how-tos/configurable_headers.md
|
||||
- Streaming:
|
||||
- Streaming: how-tos#streaming_1
|
||||
- cloud/how-tos/stream_values.md
|
||||
- cloud/how-tos/stream_updates.md
|
||||
- cloud/how-tos/stream_messages.md
|
||||
@@ -247,14 +227,12 @@ nav:
|
||||
- cloud/how-tos/use_stream_react.md
|
||||
- cloud/how-tos/generative_ui_react.md
|
||||
- Human-in-the-loop:
|
||||
- Human-in-the-loop: how-tos#human-in-the-loop_1
|
||||
- cloud/how-tos/human_in_the_loop_breakpoint.md
|
||||
- cloud/how-tos/human_in_the_loop_user_input.md
|
||||
- cloud/how-tos/human_in_the_loop_edit_state.md
|
||||
- cloud/how-tos/human_in_the_loop_time_travel.md
|
||||
- cloud/how-tos/human_in_the_loop_review_tool_calls.md
|
||||
- Double-texting:
|
||||
- Double-texting: how-tos#double-texting
|
||||
- cloud/how-tos/interrupt_concurrent.md
|
||||
- cloud/how-tos/rollback_concurrent.md
|
||||
- cloud/how-tos/reject_concurrent.md
|
||||
@@ -264,12 +242,10 @@ nav:
|
||||
- Cron Jobs:
|
||||
- cloud/how-tos/cron_jobs.md
|
||||
- Modifying the API:
|
||||
- Modifying the API: how-tos#modifying-the-api
|
||||
- how-tos/http/custom_lifespan.md
|
||||
- how-tos/http/custom_middleware.md
|
||||
- how-tos/http/custom_routes.md
|
||||
- LangGraph Studio:
|
||||
- LangGraph Studio: how-tos#langgraph-studio
|
||||
- cloud/how-tos/test_deployment.md
|
||||
- cloud/how-tos/test_local_deployment.md
|
||||
- cloud/how-tos/invoke_studio.md
|
||||
@@ -281,7 +257,6 @@ nav:
|
||||
- Concepts:
|
||||
- concepts/index.md
|
||||
- LangGraph:
|
||||
- LangGraph: concepts#langgraph
|
||||
- concepts/high_level.md
|
||||
- concepts/low_level.md
|
||||
- concepts/agentic_concepts.md
|
||||
@@ -297,9 +272,7 @@ nav:
|
||||
- concepts/durable_execution.md
|
||||
- concepts/pregel.md
|
||||
- LangGraph Platform:
|
||||
- LangGraph Platform: concepts#langgraph-platform
|
||||
- High Level:
|
||||
- High Level: concepts#high-level
|
||||
- concepts/langgraph_platform.md
|
||||
- concepts/platform_architecture.md
|
||||
- concepts/scalability_and_resilience.md
|
||||
@@ -308,7 +281,6 @@ nav:
|
||||
- concepts/plans.md
|
||||
- concepts/template_applications.md
|
||||
- Components:
|
||||
- Components: concepts#components
|
||||
- concepts/langgraph_control_plane.md
|
||||
- concepts/langgraph_data_plane.md
|
||||
- concepts/langgraph_server.md
|
||||
@@ -317,13 +289,11 @@ nav:
|
||||
- concepts/sdk.md
|
||||
- how-tos/use-remote-graph.md
|
||||
- LangGraph Server:
|
||||
- LangGraph Server: concepts#langgraph-server
|
||||
- concepts/application_structure.md
|
||||
- concepts/assistants.md
|
||||
- concepts/double_texting.md
|
||||
- concepts/auth.md
|
||||
- Deployment Options:
|
||||
- Deployment Options: concepts#deployment-options
|
||||
- concepts/langgraph_cloud.md
|
||||
- concepts/langgraph_self_hosted_data_plane.md
|
||||
- concepts/langgraph_self_hosted_control_plane.md
|
||||
@@ -332,18 +302,15 @@ nav:
|
||||
- Tutorials:
|
||||
- tutorials/index.md
|
||||
- Quick Start:
|
||||
- Quick Start: tutorials#quick-start
|
||||
- tutorials/introduction.ipynb
|
||||
- tutorials/workflows/index.md
|
||||
- tutorials/langgraph-platform/local-server.md
|
||||
- cloud/quick_start.md
|
||||
- Chatbots:
|
||||
- Chatbots: tutorials#chatbots
|
||||
- tutorials/customer-support/customer-support.ipynb
|
||||
- tutorials/chatbots/information-gather-prompting.ipynb
|
||||
- tutorials/code_assistant/langgraph_code_assistant.ipynb
|
||||
- RAG:
|
||||
- RAG: tutorials#rag
|
||||
- tutorials/rag/langgraph_adaptive_rag.ipynb
|
||||
- tutorials/rag/langgraph_adaptive_rag_local.ipynb
|
||||
- tutorials/rag/langgraph_agentic_rag.ipynb
|
||||
@@ -353,37 +320,30 @@ nav:
|
||||
- tutorials/rag/langgraph_self_rag_local.ipynb
|
||||
- tutorials/sql-agent.ipynb
|
||||
- Agent Architectures:
|
||||
- Agent Architectures: tutorials#agent-architectures
|
||||
- Multi-Agent Systems:
|
||||
- Multi-Agent Systems: tutorials#multi-agent-systems
|
||||
- tutorials/multi_agent/multi-agent-collaboration.ipynb
|
||||
- tutorials/multi_agent/agent_supervisor.ipynb
|
||||
- tutorials/multi_agent/hierarchical_agent_teams.ipynb
|
||||
- Planning Agents:
|
||||
- Planning Agents: tutorials#planning-agents
|
||||
- tutorials/plan-and-execute/plan-and-execute.ipynb
|
||||
- tutorials/rewoo/rewoo.ipynb
|
||||
- tutorials/llm-compiler/LLMCompiler.ipynb
|
||||
- Reflection & Critique:
|
||||
- Reflection & Critique: tutorials#reflection-critique
|
||||
- tutorials/reflection/reflection.ipynb
|
||||
- tutorials/reflexion/reflexion.ipynb
|
||||
- tutorials/tot/tot.ipynb
|
||||
- tutorials/lats/lats.ipynb
|
||||
- tutorials/self-discover/self-discover.ipynb
|
||||
- Evaluation & Analysis:
|
||||
- Evaluation & Analysis: tutorials#evaluation
|
||||
- tutorials/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb
|
||||
- tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb
|
||||
- Experimental:
|
||||
- Experimental: tutorials#experimental
|
||||
- tutorials/storm/storm.ipynb
|
||||
- tutorials/tnt-llm/tnt-llm.ipynb
|
||||
- tutorials/web-navigation/web_voyager.ipynb
|
||||
- tutorials/usaco/usaco.ipynb
|
||||
- tutorials/extraction/retries.ipynb
|
||||
- LangGraph Platform:
|
||||
- LangGraph Platform: concepts#langgraph-platform
|
||||
- tutorials/auth/getting_started.md
|
||||
- tutorials/auth/resource_auth.md
|
||||
- tutorials/auth/add_auth_server.md
|
||||
@@ -533,6 +493,9 @@ extra:
|
||||
data: 0
|
||||
note: >-
|
||||
Thanks for your feedback! Please help us improve this page by adding to the discussion below.
|
||||
shared_analytics:
|
||||
provider: google
|
||||
property: G-47WX3HKKY2
|
||||
validation:
|
||||
# https://www.mkdocs.org/user-guide/configuration/
|
||||
# We are still raising for omitted files because they determine the breadcrumbs for pages.
|
||||
|
||||
Generated
+2073
-1622
File diff suppressed because it is too large
Load Diff
+12
-11
@@ -21,17 +21,17 @@ langgraph-checkpoint-sqlite = { path = "../libs/checkpoint-sqlite", develop = tr
|
||||
langgraph-checkpoint-postgres = { path = "../libs/checkpoint-postgres", develop = true }
|
||||
langgraph-sdk = {path = "../libs/sdk-py", develop = true}
|
||||
langchain-ollama = "^0.2.3"
|
||||
mkdocs = "^1.6.0"
|
||||
mkdocs-autorefs = ">=1.0.1,<1.1.0"
|
||||
mkdocstrings = "^0.25.1"
|
||||
mkdocstrings-python = "^1.10.4"
|
||||
mkdocs-minify-plugin = "^0.8.0"
|
||||
mkdocs-rss-plugin = "^1.13.1"
|
||||
mkdocs-git-committers-plugin-2 = "^2.3.0"
|
||||
mkdocs-material = {extras = ["imaging"], version = "^9.5.27"}
|
||||
markdown-callouts = "^0.4.0"
|
||||
markdown-include = "^0.8.1"
|
||||
mkdocs-exclude = "^1.0.2"
|
||||
mkdocs = "*"
|
||||
mkdocs-autorefs = "*"
|
||||
mkdocstrings = "*"
|
||||
mkdocstrings-python = "*"
|
||||
mkdocs-minify-plugin = "*"
|
||||
mkdocs-rss-plugin = "*"
|
||||
mkdocs-git-committers-plugin-2 = "*"
|
||||
mkdocs-material = {extras = ["imaging"], version = "*"}
|
||||
markdown-callouts = "*"
|
||||
markdown-include = "*"
|
||||
mkdocs-exclude = "*"
|
||||
psycopg = {extras = ["binary"], version = "^3.2.0"}
|
||||
psycopg-pool = "^3.2.0"
|
||||
pygments-ansi-color = ">=0.3"
|
||||
@@ -49,6 +49,7 @@ langchain-anthropic = "^0.3.8"
|
||||
langchain-nomic = "^0.1.3"
|
||||
langchain-fireworks = "^0.2.0"
|
||||
langchain-community = "^0.3.0"
|
||||
langchain-tavily = "^0.1.5"
|
||||
langchain-experimental = "^0.3.2"
|
||||
langchain-mistralai = "^0.2.6"
|
||||
langgraph-checkpoint-mongodb = "^0.1.0"
|
||||
|
||||
Generated
+44
-23
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
@@ -7,6 +7,7 @@ description = "Reusable constraint types to use with typing.Annotated"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
|
||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
||||
@@ -32,7 +33,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
|
||||
|
||||
[package.extras]
|
||||
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
|
||||
trio = ["trio (>=0.26.1)"]
|
||||
|
||||
[[package]]
|
||||
@@ -42,6 +43,7 @@ description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
|
||||
{file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
|
||||
@@ -54,7 +56,7 @@ description = "Foreign Function Interface for Python calling C code."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "platform_python_implementation == \"PyPy\""
|
||||
markers = "python_version < \"4.0\" and platform_python_implementation == \"PyPy\""
|
||||
files = [
|
||||
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
|
||||
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
|
||||
@@ -135,6 +137,7 @@ description = "The Real First Universal Charset Detector. Open, modern and activ
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"},
|
||||
@@ -245,7 +248,7 @@ files = [
|
||||
[package.extras]
|
||||
dev = ["Pygments", "build", "chardet", "pre-commit", "pytest", "pytest-cov", "pytest-dependency", "ruff", "tomli", "twine"]
|
||||
hard-encoding-detection = ["chardet"]
|
||||
toml = ["tomli"]
|
||||
toml = ["tomli ; python_version < \"3.11\""]
|
||||
types = ["chardet (>=5.1.0)", "mypy", "pytest", "pytest-cov", "pytest-dependency"]
|
||||
|
||||
[[package]]
|
||||
@@ -279,31 +282,33 @@ test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
version = "0.16.0"
|
||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
|
||||
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.7"
|
||||
version = "1.0.9"
|
||||
description = "A minimal low-level HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"},
|
||||
{file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"},
|
||||
{file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
|
||||
{file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
h11 = ">=0.13,<0.15"
|
||||
h11 = ">=0.16"
|
||||
|
||||
[package.extras]
|
||||
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||
@@ -318,6 +323,7 @@ description = "The next generation HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
|
||||
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
|
||||
@@ -330,7 +336,7 @@ httpcore = "==1.*"
|
||||
idna = "*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli", "brotlicffi"]
|
||||
brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
|
||||
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
@@ -370,6 +376,7 @@ description = "Apply JSON-Patches (RFC 6902)"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"},
|
||||
{file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"},
|
||||
@@ -385,6 +392,7 @@ description = "Identify specific nodes in a JSON document (RFC 6901)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"},
|
||||
{file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"},
|
||||
@@ -397,6 +405,7 @@ description = "Building applications with LLMs through composability"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "langchain_core-0.3.48-py3-none-any.whl", hash = "sha256:21e4fe84262b9c7ad8aefe7816439ede130893f8a64b8c965cd9695c2be91c73"},
|
||||
{file = "langchain_core-0.3.48.tar.gz", hash = "sha256:be4b2fe36d8a11fb4b6b13e0808b12aea9f25e345624ffafe1d606afb6059f21"},
|
||||
@@ -416,16 +425,16 @@ typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.21"
|
||||
version = "2.0.24"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "dev"]
|
||||
files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
langchain-core = ">=0.2.38,<0.4"
|
||||
langchain-core = {version = ">=0.2.38", markers = "python_version < \"4.0\""}
|
||||
ormsgpack = "^1.8.0"
|
||||
|
||||
[package.source]
|
||||
@@ -439,6 +448,7 @@ description = "Client library to connect to the LangSmith LLM Tracing and Evalua
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "langsmith-0.3.13-py3-none-any.whl", hash = "sha256:73aaf52bbc293b9415fff4f6dad68df40658081eb26c9cb2c7bd1ff57cedd695"},
|
||||
{file = "langsmith-0.3.13.tar.gz", hash = "sha256:14014058cff408772acb93344e03cb64174837292d5f1ae09b2c8c1d8df45e92"},
|
||||
@@ -662,6 +672,7 @@ files = [
|
||||
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
|
||||
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
|
||||
]
|
||||
markers = {main = "python_version < \"4.0\""}
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
@@ -697,8 +708,8 @@ typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""}
|
||||
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
|
||||
[package.extras]
|
||||
binary = ["psycopg-binary (==3.2.5)"]
|
||||
c = ["psycopg-c (==3.2.5)"]
|
||||
binary = ["psycopg-binary (==3.2.5) ; implementation_name != \"pypy\""]
|
||||
c = ["psycopg-c (==3.2.5) ; implementation_name != \"pypy\""]
|
||||
dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "isort-psycopg", "isort[colors] (>=6.0)", "mypy (>=1.14)", "pre-commit (>=4.0.1)", "types-setuptools (>=57.4)", "wheel (>=0.37)"]
|
||||
docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"]
|
||||
pool = ["psycopg-pool"]
|
||||
@@ -802,7 +813,7 @@ description = "C parser in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "platform_python_implementation == \"PyPy\""
|
||||
markers = "python_version < \"4.0\" and platform_python_implementation == \"PyPy\""
|
||||
files = [
|
||||
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
|
||||
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
|
||||
@@ -815,6 +826,7 @@ description = "Data validation using Python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"},
|
||||
{file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"},
|
||||
@@ -827,7 +839,7 @@ typing-extensions = ">=4.12.2"
|
||||
|
||||
[package.extras]
|
||||
email = ["email-validator (>=2.0.0)"]
|
||||
timezone = ["tzdata"]
|
||||
timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
@@ -836,6 +848,7 @@ description = "Core functionality for Pydantic validation and serialization"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"},
|
||||
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"},
|
||||
@@ -1009,6 +1022,7 @@ description = "Automatically rerun your tests on file modifications"
|
||||
optional = false
|
||||
python-versions = "<4.0.0,>=3.7.0"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "pytest_watcher-0.4.3-py3-none-any.whl", hash = "sha256:d59b1e1396f33a65ea4949b713d6884637755d641646960056a90b267c3460f9"},
|
||||
{file = "pytest_watcher-0.4.3.tar.gz", hash = "sha256:0cb0e4661648c8c0ff2b2d25efa5a8e421784b9e4c60fcecbf9b7c30b2d731b3"},
|
||||
@@ -1025,6 +1039,7 @@ description = "YAML parser and emitter for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
|
||||
{file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
|
||||
@@ -1088,6 +1103,7 @@ description = "Python HTTP for Humans."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
|
||||
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
|
||||
@@ -1110,6 +1126,7 @@ description = "A utility belt for advanced users of python-requests"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"},
|
||||
{file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"},
|
||||
@@ -1165,6 +1182,7 @@ description = "Retry code until it succeeds"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"},
|
||||
{file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"},
|
||||
@@ -1249,13 +1267,14 @@ description = "HTTP library with thread-safe connection pooling, file post, and
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"},
|
||||
{file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
|
||||
brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
|
||||
h2 = ["h2 (>=4,<5)"]
|
||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
@@ -1267,6 +1286,7 @@ description = "Filesystem events monitoring"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"},
|
||||
{file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"},
|
||||
@@ -1310,6 +1330,7 @@ description = "Zstandard bindings for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "zstandard-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9"},
|
||||
{file = "zstandard-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880"},
|
||||
@@ -1418,5 +1439,5 @@ cffi = ["cffi (>=1.11)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
content-hash = "4b0efdd115566f294fcd876334f9c3787aafc81f2689473759d88189a71d4635"
|
||||
python-versions = ">=3.9"
|
||||
content-hash = "26b7d9b5b7738550356c178f951a616a29b4f3ec6690db2e1329f06daef8220f"
|
||||
|
||||
@@ -9,7 +9,7 @@ repository = "https://www.github.com/langchain-ai/langgraph"
|
||||
packages = [{ include = "langgraph" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
python = ">=3.9"
|
||||
langgraph-checkpoint = "^2.0.21"
|
||||
orjson = ">=3.10.1"
|
||||
psycopg = "^3.2.0"
|
||||
@@ -25,7 +25,7 @@ pytest-mock = "^3.11.1"
|
||||
mypy = "^1.10.0"
|
||||
psycopg = {extras = ["binary"], version = ">=3.0.0"}
|
||||
langgraph-checkpoint = {path = "../checkpoint", develop = true}
|
||||
pytest-watcher = "^0.4.3"
|
||||
pytest-watcher = { version = ">=0.4.3", python = "<4.0" }
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
# --strict-markers will raise errors on unknown marks.
|
||||
|
||||
Generated
+44
-25
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiosqlite"
|
||||
@@ -26,6 +26,7 @@ description = "Reusable constraint types to use with typing.Annotated"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
|
||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
||||
@@ -38,6 +39,7 @@ description = "High level compatibility layer for multiple asynchronous event lo
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
|
||||
{file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
|
||||
@@ -61,6 +63,7 @@ description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
|
||||
{file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
|
||||
@@ -73,6 +76,7 @@ description = "The Real First Universal Charset Detector. Open, modern and activ
|
||||
optional = false
|
||||
python-versions = ">=3.7.0"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
|
||||
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
|
||||
@@ -215,54 +219,54 @@ test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
version = "0.16.0"
|
||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
|
||||
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.5"
|
||||
version = "0.13.2"
|
||||
description = "A minimal low-level HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
|
||||
{file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
|
||||
{file = "httpcore-0.13.2-py3-none-any.whl", hash = "sha256:52b7d9413f6f5592a667de9209d70d4d41aba3fb0540dd7c93475c78b85941e9"},
|
||||
{file = "httpcore-0.13.2.tar.gz", hash = "sha256:c16efbdf643e1b57bde0adc12c53b08645d7d92d6d345a3f71adfc2a083e7fd2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
h11 = ">=0.13,<0.15"
|
||||
h11 = "==0.*"
|
||||
sniffio = "==1.*"
|
||||
|
||||
[package.extras]
|
||||
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
trio = ["trio (>=0.22.0,<0.26.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.27.2"
|
||||
version = "0.25.1"
|
||||
description = "The next generation HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
|
||||
{file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
|
||||
{file = "httpx-0.25.1-py3-none-any.whl", hash = "sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a"},
|
||||
{file = "httpx-0.25.1.tar.gz", hash = "sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = "*"
|
||||
certifi = "*"
|
||||
httpcore = "==1.*"
|
||||
httpcore = "*"
|
||||
idna = "*"
|
||||
sniffio = "*"
|
||||
|
||||
@@ -271,7 +275,6 @@ brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi
|
||||
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
@@ -280,6 +283,7 @@ description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
||||
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
||||
@@ -304,6 +308,7 @@ description = "Apply JSON-Patches (RFC 6902)"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"},
|
||||
{file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"},
|
||||
@@ -319,6 +324,7 @@ description = "Identify specific nodes in a JSON document (RFC 6901)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"},
|
||||
{file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"},
|
||||
@@ -331,6 +337,7 @@ description = "Building applications with LLMs through composability"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "langchain_core-0.3.15-py3-none-any.whl", hash = "sha256:3d4ca6dbb8ed396a6ee061063832a2451b0ce8c345570f7b086ffa7288e4fa29"},
|
||||
{file = "langchain_core-0.3.15.tar.gz", hash = "sha256:b1a29787a4ffb7ec2103b4e97d435287201da7809b369740dd1e32f176325aba"},
|
||||
@@ -350,16 +357,16 @@ typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.21"
|
||||
version = "2.0.24"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "dev"]
|
||||
files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
langchain-core = ">=0.2.38,<0.4"
|
||||
langchain-core = {version = ">=0.2.38", markers = "python_version < \"4.0\""}
|
||||
ormsgpack = "^1.8.0"
|
||||
|
||||
[package.source]
|
||||
@@ -373,6 +380,7 @@ description = "Client library to connect to the LangSmith LLM Tracing and Evalua
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.8.1"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "langsmith-0.1.147-py3-none-any.whl", hash = "sha256:7166fc23b965ccf839d64945a78e9f1157757add228b086141eb03a60d699a15"},
|
||||
{file = "langsmith-0.1.147.tar.gz", hash = "sha256:2e933220318a4e73034657103b3b1a3a6109cc5db3566a7e8e03be8d6d7def7a"},
|
||||
@@ -458,7 +466,7 @@ description = "Fast, correct Python JSON library supporting dataclasses, datetim
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "platform_python_implementation != \"PyPy\""
|
||||
markers = "python_version < \"4.0\" and platform_python_implementation != \"PyPy\""
|
||||
files = [
|
||||
{file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"},
|
||||
{file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"},
|
||||
@@ -562,6 +570,7 @@ files = [
|
||||
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
||||
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
||||
]
|
||||
markers = {main = "python_version < \"4.0\""}
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
@@ -586,6 +595,7 @@ description = "Data validation using Python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"},
|
||||
{file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"},
|
||||
@@ -609,6 +619,7 @@ description = "Core functionality for Pydantic validation and serialization"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"},
|
||||
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"},
|
||||
@@ -771,6 +782,7 @@ description = "Automatically rerun your tests on file modifications"
|
||||
optional = false
|
||||
python-versions = "<4.0.0,>=3.7.0"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "pytest_watcher-0.4.2-py3-none-any.whl", hash = "sha256:a43949ba67dd8d7e1fd0de5eea44a999081f0aec9f93b4e744264b4c6a3d9bbe"},
|
||||
{file = "pytest_watcher-0.4.2.tar.gz", hash = "sha256:7b292f025ca19617cd7567c228c6187b5087f2da9e4d2cf6e144e5764a0471b0"},
|
||||
@@ -787,6 +799,7 @@ description = "YAML parser and emitter for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
|
||||
@@ -848,6 +861,7 @@ description = "Python HTTP for Humans."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
|
||||
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
|
||||
@@ -870,6 +884,7 @@ description = "A utility belt for advanced users of python-requests"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"},
|
||||
{file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"},
|
||||
@@ -913,6 +928,7 @@ description = "Sniff out which async library your code is running under"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
||||
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||
@@ -925,6 +941,7 @@ description = "Retry code until it succeeds"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687"},
|
||||
{file = "tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78"},
|
||||
@@ -966,6 +983,7 @@ description = "HTTP library with thread-safe connection pooling, file post, and
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
|
||||
{file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
|
||||
@@ -984,6 +1002,7 @@ description = "Filesystem events monitoring"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"},
|
||||
{file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"},
|
||||
@@ -1024,5 +1043,5 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.9.0"
|
||||
content-hash = "21896b8d3d283d95bc3988aa93f06faf5c47dadc2a8822e5a35672b9cb054693"
|
||||
python-versions = ">=3.9"
|
||||
content-hash = "dbe7f74e82d14d3a746dbd04ac18dcc0a9d1d95af5e5a98c6bb6ab298e80b19d"
|
||||
|
||||
@@ -9,7 +9,7 @@ repository = "https://www.github.com/langchain-ai/langgraph"
|
||||
packages = [{ include = "langgraph" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0"
|
||||
python = ">=3.9"
|
||||
langgraph-checkpoint = "^2.0.15"
|
||||
aiosqlite = ">=0.20,<0.22"
|
||||
|
||||
@@ -19,7 +19,7 @@ codespell = "^2.2.0"
|
||||
pytest = "^7.2.1"
|
||||
pytest-asyncio = "^0.21.1"
|
||||
pytest-mock = "^3.11.1"
|
||||
pytest-watcher = "^0.4.1"
|
||||
pytest-watcher = { version = ">=0.4.1", python = "<4.0" }
|
||||
mypy = "^1.10.0"
|
||||
langgraph-checkpoint = {path = "../checkpoint", develop = true}
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ class InMemorySaver(
|
||||
if factory is not defaultdict:
|
||||
self.stack.enter_context(self.storage) # type: ignore[arg-type]
|
||||
self.stack.enter_context(self.writes) # type: ignore[arg-type]
|
||||
self.stack.enter_context(self.blobs) # type: ignore[arg-type]
|
||||
|
||||
def __enter__(self) -> "InMemorySaver":
|
||||
return self.stack.__enter__()
|
||||
|
||||
Generated
+43
-16
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
@@ -7,6 +7,7 @@ description = "Reusable constraint types to use with typing.Annotated"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
|
||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
||||
@@ -19,6 +20,7 @@ description = "High level compatibility layer for multiple asynchronous event lo
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"},
|
||||
{file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"},
|
||||
@@ -32,7 +34,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
|
||||
|
||||
[package.extras]
|
||||
doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
|
||||
test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
|
||||
test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
|
||||
trio = ["trio (>=0.26.1)"]
|
||||
|
||||
[[package]]
|
||||
@@ -42,6 +44,7 @@ description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
|
||||
{file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
|
||||
@@ -54,6 +57,7 @@ description = "The Real First Universal Charset Detector. Open, modern and activ
|
||||
optional = false
|
||||
python-versions = ">=3.7.0"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
|
||||
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
|
||||
@@ -162,7 +166,7 @@ files = [
|
||||
[package.extras]
|
||||
dev = ["Pygments", "build", "chardet", "pre-commit", "pytest", "pytest-cov", "pytest-dependency", "ruff", "tomli", "twine"]
|
||||
hard-encoding-detection = ["chardet"]
|
||||
toml = ["tomli"]
|
||||
toml = ["tomli ; python_version < \"3.11\""]
|
||||
types = ["chardet (>=5.1.0)", "mypy", "pytest", "pytest-cov", "pytest-dependency"]
|
||||
|
||||
[[package]]
|
||||
@@ -185,6 +189,7 @@ description = "Easily serialize dataclasses to and from JSON."
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.7"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a"},
|
||||
{file = "dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0"},
|
||||
@@ -212,31 +217,33 @@ test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
version = "0.16.0"
|
||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
|
||||
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.7"
|
||||
version = "1.0.9"
|
||||
description = "A minimal low-level HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"},
|
||||
{file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"},
|
||||
{file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
|
||||
{file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
h11 = ">=0.13,<0.15"
|
||||
h11 = ">=0.16"
|
||||
|
||||
[package.extras]
|
||||
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||
@@ -251,6 +258,7 @@ description = "The next generation HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
|
||||
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
|
||||
@@ -263,7 +271,7 @@ httpcore = "==1.*"
|
||||
idna = "*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli", "brotlicffi"]
|
||||
brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
|
||||
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
@@ -276,6 +284,7 @@ description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
||||
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
||||
@@ -300,6 +309,7 @@ description = "Apply JSON-Patches (RFC 6902)"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"},
|
||||
{file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"},
|
||||
@@ -315,6 +325,7 @@ description = "Identify specific nodes in a JSON document (RFC 6901)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"},
|
||||
{file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"},
|
||||
@@ -327,6 +338,7 @@ description = "Building applications with LLMs through composability"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.8.1"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "langchain_core-0.2.43-py3-none-any.whl", hash = "sha256:619601235113298ebf8252a349754b7c28d3cf7166c7c922da24944b78a9363a"},
|
||||
{file = "langchain_core-0.2.43.tar.gz", hash = "sha256:42c2ef6adedb911f4254068b6adc9eb4c4075f6c8cb3d83590d3539a815695f5"},
|
||||
@@ -351,6 +363,7 @@ description = "Client library to connect to the LangSmith LLM Tracing and Evalua
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.8.1"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "langsmith-0.1.147-py3-none-any.whl", hash = "sha256:7166fc23b965ccf839d64945a78e9f1157757add228b086141eb03a60d699a15"},
|
||||
{file = "langsmith-0.1.147.tar.gz", hash = "sha256:2e933220318a4e73034657103b3b1a3a6109cc5db3566a7e8e03be8d6d7def7a"},
|
||||
@@ -376,6 +389,7 @@ description = "A lightweight library for converting complex datatypes to and fro
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "marshmallow-3.21.3-py3-none-any.whl", hash = "sha256:86ce7fb914aa865001a4b2092c4c2872d13bc347f3d42673272cabfdbad386f1"},
|
||||
{file = "marshmallow-3.21.3.tar.gz", hash = "sha256:4f57c5e050a54d66361e826f94fba213eb10b67b2fdb02c3e0343ce207ba1662"},
|
||||
@@ -456,7 +470,7 @@ description = "Fast, correct Python JSON library supporting dataclasses, datetim
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "platform_python_implementation != \"PyPy\""
|
||||
markers = "python_version < \"4.0\" and platform_python_implementation != \"PyPy\""
|
||||
files = [
|
||||
{file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"},
|
||||
{file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"},
|
||||
@@ -560,6 +574,7 @@ files = [
|
||||
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
||||
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
||||
]
|
||||
markers = {main = "python_version < \"4.0\""}
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
@@ -584,6 +599,7 @@ description = "Data validation using Python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"},
|
||||
{file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"},
|
||||
@@ -607,6 +623,7 @@ description = "Core functionality for Pydantic validation and serialization"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"},
|
||||
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"},
|
||||
@@ -769,6 +786,7 @@ description = "Automatically rerun your tests on file modifications"
|
||||
optional = false
|
||||
python-versions = "<4.0.0,>=3.7.0"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "pytest_watcher-0.4.2-py3-none-any.whl", hash = "sha256:a43949ba67dd8d7e1fd0de5eea44a999081f0aec9f93b4e744264b4c6a3d9bbe"},
|
||||
{file = "pytest_watcher-0.4.2.tar.gz", hash = "sha256:7b292f025ca19617cd7567c228c6187b5087f2da9e4d2cf6e144e5764a0471b0"},
|
||||
@@ -785,6 +803,7 @@ description = "YAML parser and emitter for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
|
||||
@@ -846,6 +865,7 @@ description = "Python HTTP for Humans."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
|
||||
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
|
||||
@@ -868,6 +888,7 @@ description = "A utility belt for advanced users of python-requests"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"},
|
||||
{file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"},
|
||||
@@ -911,6 +932,7 @@ description = "Sniff out which async library your code is running under"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
||||
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||
@@ -923,6 +945,7 @@ description = "Retry code until it succeeds"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687"},
|
||||
{file = "tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78"},
|
||||
@@ -956,6 +979,7 @@ files = [
|
||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||
]
|
||||
markers = {main = "python_version < \"4.0\""}
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspect"
|
||||
@@ -964,6 +988,7 @@ description = "Runtime inspection utilities for typing module."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"},
|
||||
{file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"},
|
||||
@@ -980,13 +1005,14 @@ description = "HTTP library with thread-safe connection pooling, file post, and
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
|
||||
{file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
|
||||
brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
|
||||
h2 = ["h2 (>=4,<5)"]
|
||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
@@ -998,6 +1024,7 @@ description = "Filesystem events monitoring"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"},
|
||||
{file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"},
|
||||
@@ -1038,5 +1065,5 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
content-hash = "db4bfc26829b1abd13c9426d74dc26100f36a6777e965a32eed4cc1ae17a8c14"
|
||||
python-versions = ">=3.9"
|
||||
content-hash = "24a87064b45e94994ef6ced60b68bd58ead04f10a2fcd85120e5a7c035264bbf"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.24"
|
||||
version = "2.0.25"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -9,8 +9,8 @@ repository = "https://www.github.com/langchain-ai/langgraph"
|
||||
packages = [{ include = "langgraph" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
langchain-core = ">=0.2.38,<0.4"
|
||||
python = ">=3.9"
|
||||
langchain-core = { version = ">=0.2.38", python = "<4.0" }
|
||||
ormsgpack = "^1.8.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
@@ -19,9 +19,9 @@ codespell = "^2.2.0"
|
||||
pytest = "^7.2.1"
|
||||
pytest-asyncio = "^0.21.1"
|
||||
pytest-mock = "^3.11.1"
|
||||
pytest-watcher = "^0.4.1"
|
||||
pytest-watcher = { version = ">=0.4.1", python = "<4.0" }
|
||||
mypy = "^1.10.0"
|
||||
dataclasses-json = "^0.6.7"
|
||||
dataclasses-json = { version = ">=0.6.7", python = "<4.0" }
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
# --strict-markers will raise errors on unknown marks.
|
||||
|
||||
Generated
+33
-15
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
@@ -6,6 +6,7 @@ version = "4.4.0"
|
||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
|
||||
{file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
|
||||
@@ -28,6 +29,7 @@ version = "2024.7.4"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
|
||||
{file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
|
||||
@@ -39,6 +41,7 @@ version = "8.1.7"
|
||||
description = "Composable command line interface toolkit"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||
@@ -53,6 +56,8 @@ version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
groups = ["main"]
|
||||
markers = "platform_system == \"Windows\""
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
@@ -64,6 +69,8 @@ version = "1.2.1"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
|
||||
{file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
|
||||
@@ -74,45 +81,48 @@ test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
version = "0.16.0"
|
||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
|
||||
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.5"
|
||||
version = "1.0.9"
|
||||
description = "A minimal low-level HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
|
||||
{file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
|
||||
{file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
|
||||
{file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
h11 = ">=0.13,<0.15"
|
||||
h11 = ">=0.16"
|
||||
|
||||
[package.extras]
|
||||
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
trio = ["trio (>=0.22.0,<0.26.0)"]
|
||||
trio = ["trio (>=0.22.0,<1.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.27.0"
|
||||
version = "0.28.1"
|
||||
description = "The next generation HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
|
||||
{file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
|
||||
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
|
||||
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -120,13 +130,13 @@ anyio = "*"
|
||||
certifi = "*"
|
||||
httpcore = "==1.*"
|
||||
idna = "*"
|
||||
sniffio = "*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli", "brotlicffi"]
|
||||
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
@@ -134,6 +144,7 @@ version = "0.4.0"
|
||||
description = "Consume Server-Sent Event (SSE) messages with HTTPX."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"},
|
||||
{file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"},
|
||||
@@ -145,6 +156,7 @@ version = "3.7"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
||||
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
||||
@@ -156,6 +168,7 @@ version = "0.1.52"
|
||||
description = "CLI for interacting with LangGraph API"
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
groups = ["main"]
|
||||
files = []
|
||||
develop = true
|
||||
|
||||
@@ -172,6 +185,7 @@ version = "0.1.29"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
groups = ["main"]
|
||||
files = []
|
||||
develop = true
|
||||
|
||||
@@ -190,6 +204,7 @@ version = "3.10.5"
|
||||
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "orjson-3.10.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:545d493c1f560d5ccfc134803ceb8955a14c3fcb47bbb4b2fee0232646d0b932"},
|
||||
{file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4324929c2dd917598212bfd554757feca3e5e0fa60da08be11b4aa8b90013c1"},
|
||||
@@ -245,6 +260,7 @@ version = "1.3.1"
|
||||
description = "Sniff out which async library your code is running under"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
||||
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||
@@ -256,12 +272,14 @@ version = "4.12.2"
|
||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||
]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
content-hash = "ec5109729f30d2033a10a10e8f8d3ed94c7d96d5d31025b4815b0123664bb063"
|
||||
|
||||
@@ -8,7 +8,7 @@ packages = []
|
||||
package-mode = false
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
python = ">=3.9"
|
||||
langgraph-cli = {path = "../../cli", develop = true}
|
||||
langgraph-sdk = {path = "../../sdk-py", develop = true}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from langgraph_cli.config import (
|
||||
AuthConfig,
|
||||
CheckpointerConfig,
|
||||
Config,
|
||||
ConfigurableHeaderConfig,
|
||||
CorsConfig,
|
||||
HttpConfig,
|
||||
IndexConfig,
|
||||
@@ -112,6 +113,7 @@ def add_descriptions_to_schema(schema, cls):
|
||||
ThreadTTLConfig,
|
||||
CheckpointerConfig,
|
||||
TTLConfig,
|
||||
ConfigurableHeaderConfig,
|
||||
]:
|
||||
if potential_cls.__name__ == def_name:
|
||||
add_descriptions_to_schema(def_schema, potential_cls)
|
||||
|
||||
@@ -619,20 +619,30 @@
|
||||
zod "^3.22.4"
|
||||
zod-to-json-schema "^3.22.3"
|
||||
|
||||
"@langchain/langgraph-checkpoint@~0.0.6":
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-0.0.6.tgz#69f0c5c9aeefd48dcf0fa1ffa0744d8139a9f27d"
|
||||
integrity sha512-hQsznlUMFKyOCaN9VtqNSSemfKATujNy5ePM6NX7lruk/Mmi2t7R9SsBnf9G2Yts+IaIwv3vJJaAFYEHfqbc5g==
|
||||
"@langchain/langgraph-checkpoint@~0.0.17":
|
||||
version "0.0.17"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-0.0.17.tgz#d0a8824eb0769567da54262adebe65db4ee6d58f"
|
||||
integrity sha512-6b3CuVVYx+7x0uWLG+7YXz9j2iBa+tn2AXvkLxzEvaAsLE6Sij++8PPbS2BZzC+S/FPJdWsz6I5bsrqL0BYrCA==
|
||||
dependencies:
|
||||
uuid "^10.0.0"
|
||||
|
||||
"@langchain/langgraph@^0.2.5":
|
||||
version "0.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-0.2.5.tgz#c42743a59adef03f2e1fea0c198a01694ae34d51"
|
||||
integrity sha512-H4OgZyGRWZHBaiXXIb9avyB8zI6+3OewKn+UOZ+wUzYLKyF3cnq0cNF4/Ps+gxCa5RtOnsHIqQyRkojfXIOqgA==
|
||||
"@langchain/langgraph-sdk@~0.0.32":
|
||||
version "0.0.70"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.70.tgz#9589f984b47de5e4a669b6008cbf01427a3d41d0"
|
||||
integrity sha512-O8I12bfeMVz5fOrXnIcK4IdRf50IqyJTO458V56wAIHLNoi4H8/JHM+2M+Y4H2PtslXIGnvomWqlBd0eY5z/Og==
|
||||
dependencies:
|
||||
"@langchain/langgraph-checkpoint" "~0.0.6"
|
||||
double-ended-queue "^2.1.0-0"
|
||||
"@types/json-schema" "^7.0.15"
|
||||
p-queue "^6.6.2"
|
||||
p-retry "4"
|
||||
uuid "^9.0.0"
|
||||
|
||||
"@langchain/langgraph@^0.2.5":
|
||||
version "0.2.67"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-0.2.67.tgz#f154e4e8534ca78b2b73a2cd3a901d82790b0d1b"
|
||||
integrity sha512-tu/ewNIvhIPzeW5GxGzqjmGHinnU/qbNAoLM9czdpci0PCbMysbEJ2pbJrZs7ZjaReWSnr/THkeLPQwqGOM9xw==
|
||||
dependencies:
|
||||
"@langchain/langgraph-checkpoint" "~0.0.17"
|
||||
"@langchain/langgraph-sdk" "~0.0.32"
|
||||
uuid "^10.0.0"
|
||||
zod "^3.23.8"
|
||||
|
||||
@@ -748,7 +758,7 @@
|
||||
expect "^29.0.0"
|
||||
pretty-format "^29.0.0"
|
||||
|
||||
"@types/json-schema@^7.0.9":
|
||||
"@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9":
|
||||
version "7.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
|
||||
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
|
||||
@@ -1422,11 +1432,6 @@ dotenv@^16.4.5:
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
|
||||
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
|
||||
|
||||
double-ended-queue@^2.1.0-0:
|
||||
version "2.1.0-0"
|
||||
resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c"
|
||||
integrity sha512-+BNfZ+deCo8hMNpDqDnvT+c0XpJ5cUa6mqYq89bho2Ifze4URTqRkcwR399hWoTrTkbZ/XJYDgP6rc7pRgffEQ==
|
||||
|
||||
ejs@^3.1.10:
|
||||
version "3.1.10"
|
||||
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b"
|
||||
@@ -3685,6 +3690,11 @@ uuid@^10.0.0:
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
|
||||
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
|
||||
|
||||
uuid@^9.0.0:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
|
||||
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
|
||||
|
||||
v8-to-istanbul@^9.0.1:
|
||||
version "9.3.0"
|
||||
resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175"
|
||||
@@ -3795,7 +3805,12 @@ zod-to-json-schema@^3.22.3:
|
||||
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.23.2.tgz#bc7e379c8050462538383e382964c03d8fe008f9"
|
||||
integrity sha512-uSt90Gzc/tUfyNqxnjlfBs8W6WSGpNBv0rVsNxP/BVSMHMKGdthPYff4xtCHYloJGM0CFxFsb3NbC0eqPhfImw==
|
||||
|
||||
zod@^3.22.4, zod@^3.23.8:
|
||||
zod@^3.22.4:
|
||||
version "3.23.8"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
|
||||
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
|
||||
|
||||
zod@^3.23.8:
|
||||
version "3.24.3"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.3.tgz#1f40f750a05e477396da64438e0e1c0995dafd87"
|
||||
integrity sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==
|
||||
|
||||
@@ -168,6 +168,13 @@ def cli():
|
||||
@OPT_DEBUGGER_BASE_URL
|
||||
@OPT_WATCH
|
||||
@OPT_POSTGRES_URI
|
||||
@click.option(
|
||||
"--image",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Docker image to use for the langgraph-api service. If specified, skips building and uses this image directly."
|
||||
" Useful if you want to test against an image already built using `langgraph build`.",
|
||||
)
|
||||
@click.option(
|
||||
"--wait",
|
||||
is_flag=True,
|
||||
@@ -187,6 +194,7 @@ def up(
|
||||
debugger_port: Optional[int],
|
||||
debugger_base_url: Optional[str],
|
||||
postgres_uri: Optional[str],
|
||||
image: Optional[str],
|
||||
):
|
||||
click.secho("Starting LangGraph API server...", fg="green")
|
||||
click.secho(
|
||||
@@ -207,6 +215,7 @@ For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KE
|
||||
debugger_port=debugger_port,
|
||||
debugger_base_url=debugger_base_url,
|
||||
postgres_uri=postgres_uri,
|
||||
image=image,
|
||||
)
|
||||
# add up + options
|
||||
args.extend(["up", "--remove-orphans"])
|
||||
@@ -692,6 +701,7 @@ def prepare_args_and_stdin(
|
||||
debugger_port: Optional[int] = None,
|
||||
debugger_base_url: Optional[str] = None,
|
||||
postgres_uri: Optional[str] = None,
|
||||
image: Optional[str] = None,
|
||||
) -> Tuple[List[str], str]:
|
||||
assert config_path.exists(), f"Config file not found: {config_path}"
|
||||
# prepare args
|
||||
@@ -701,6 +711,7 @@ def prepare_args_and_stdin(
|
||||
debugger_port=debugger_port,
|
||||
debugger_base_url=debugger_base_url,
|
||||
postgres_uri=postgres_uri,
|
||||
image=image, # Pass image to compose YAML generator
|
||||
)
|
||||
args = [
|
||||
"--project-directory",
|
||||
@@ -716,6 +727,7 @@ def prepare_args_and_stdin(
|
||||
config,
|
||||
watch=watch,
|
||||
base_image=langgraph_cli.config.default_base_image(config),
|
||||
image=image,
|
||||
)
|
||||
return args, stdin
|
||||
|
||||
@@ -733,6 +745,7 @@ def prepare(
|
||||
debugger_port: Optional[int] = None,
|
||||
debugger_base_url: Optional[str] = None,
|
||||
postgres_uri: Optional[str] = None,
|
||||
image: Optional[str] = None,
|
||||
) -> Tuple[List[str], str]:
|
||||
"""Prepare the arguments and stdin for running the LangGraph API server."""
|
||||
config_json = langgraph_cli.config.validate_config_file(config_path)
|
||||
@@ -757,5 +770,6 @@ def prepare(
|
||||
debugger_port=debugger_port,
|
||||
debugger_base_url=debugger_base_url or f"http://127.0.0.1:{port}",
|
||||
postgres_uri=postgres_uri,
|
||||
image=image,
|
||||
)
|
||||
return args, stdin
|
||||
|
||||
@@ -273,6 +273,33 @@ class CorsConfig(TypedDict, total=False):
|
||||
"""
|
||||
|
||||
|
||||
class ConfigurableHeaderConfig(TypedDict):
|
||||
"""Customize which headers to include as configurable values in your runs.
|
||||
|
||||
By default, omits x-api-key, x-tenant-id, and x-service-key.
|
||||
|
||||
Exclusions (if provided) take precedence.
|
||||
|
||||
Each value can be a raw string with an optional wildcard.
|
||||
"""
|
||||
|
||||
includes: Optional[list[str]]
|
||||
"""Headers to include (if not also matches against an 'exludes' pattern.
|
||||
|
||||
Examples:
|
||||
- 'user-agent'
|
||||
- 'x-configurable-*'
|
||||
"""
|
||||
excludes: Optional[list[str]]
|
||||
"""Headers to exclude. Applied before the 'includes' checks.
|
||||
|
||||
Examples:
|
||||
- 'x-api-key'
|
||||
- '*key*'
|
||||
- '*token*'
|
||||
"""
|
||||
|
||||
|
||||
class HttpConfig(TypedDict, total=False):
|
||||
"""Configuration for the built-in HTTP server that powers your deployment's routes and endpoints."""
|
||||
|
||||
@@ -311,6 +338,11 @@ class HttpConfig(TypedDict, total=False):
|
||||
"""Optional. Defines CORS restrictions. If omitted, no special rules are set and
|
||||
cross-origin behavior depends on default server settings.
|
||||
"""
|
||||
configurable_headers: Optional[ConfigurableHeaderConfig]
|
||||
"""Optional. Defines how headers are treated for a run's configuration.
|
||||
|
||||
You can include or exclude headers as configurable values to condition your
|
||||
agent's behavior or permissions on a request's headers."""
|
||||
|
||||
|
||||
class Config(TypedDict, total=False):
|
||||
@@ -1288,6 +1320,7 @@ def config_to_compose(
|
||||
config_path: pathlib.Path,
|
||||
config: Config,
|
||||
base_image: Optional[str] = None,
|
||||
image: Optional[str] = None,
|
||||
watch: bool = False,
|
||||
) -> str:
|
||||
base_image = base_image or default_base_image(config)
|
||||
@@ -1314,19 +1347,28 @@ def config_to_compose(
|
||||
"""
|
||||
else:
|
||||
watch_str = ""
|
||||
if image:
|
||||
return f"""
|
||||
{textwrap.indent(env_vars_str, " ")}
|
||||
{env_file_str}
|
||||
{watch_str}
|
||||
"""
|
||||
|
||||
dockerfile, additional_contexts = config_to_docker(config_path, config, base_image)
|
||||
else:
|
||||
dockerfile, additional_contexts = config_to_docker(
|
||||
config_path, config, base_image
|
||||
)
|
||||
|
||||
additional_contexts_str = "\n".join(
|
||||
f" - {name}: {path}"
|
||||
for name, path in additional_contexts.items()
|
||||
)
|
||||
if additional_contexts_str:
|
||||
additional_contexts_str = f"""
|
||||
additional_contexts_str = "\n".join(
|
||||
f" - {name}: {path}"
|
||||
for name, path in additional_contexts.items()
|
||||
)
|
||||
if additional_contexts_str:
|
||||
additional_contexts_str = f"""
|
||||
additional_contexts:
|
||||
{additional_contexts_str}"""
|
||||
|
||||
return f"""
|
||||
return f"""
|
||||
{textwrap.indent(env_vars_str, " ")}
|
||||
{env_file_str}
|
||||
pull_policy: build
|
||||
|
||||
@@ -143,6 +143,8 @@ def compose_as_dict(
|
||||
debugger_base_url: Optional[str] = None,
|
||||
# postgres://user:password@host:port/database?option=value
|
||||
postgres_uri: Optional[str] = None,
|
||||
# If you are running against an already-built image, you can pass it here
|
||||
image: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Create a docker compose file as a dictionary in YML style."""
|
||||
if postgres_uri is None:
|
||||
@@ -211,6 +213,8 @@ def compose_as_dict(
|
||||
"POSTGRES_URI": postgres_uri,
|
||||
},
|
||||
}
|
||||
if image:
|
||||
services["langgraph-api"]["image"] = image
|
||||
|
||||
# If Postgres is included, add it to the dependencies of langgraph-api
|
||||
if include_db:
|
||||
@@ -244,6 +248,7 @@ def compose(
|
||||
debugger_base_url: Optional[str] = None,
|
||||
# postgres://user:password@host:port/database?option=value
|
||||
postgres_uri: Optional[str] = None,
|
||||
image: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Create a docker compose file as a string."""
|
||||
compose_content = compose_as_dict(
|
||||
@@ -252,6 +257,7 @@ def compose(
|
||||
debugger_port=debugger_port,
|
||||
debugger_base_url=debugger_base_url,
|
||||
postgres_uri=postgres_uri,
|
||||
image=image,
|
||||
)
|
||||
compose_str = dict_to_yaml(compose_content)
|
||||
return compose_str
|
||||
|
||||
Generated
+56
-56
@@ -7,7 +7,7 @@ description = "Reusable constraint types to use with typing.Annotated"
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\" and python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
|
||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
||||
@@ -20,7 +20,7 @@ description = "High level compatibility layer for multiple asynchronous event lo
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"},
|
||||
{file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"},
|
||||
@@ -43,7 +43,7 @@ description = "Utility to detect blocking calls in the async event loop"
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "blockbuster-1.5.24-py3-none-any.whl", hash = "sha256:e703497b55bc72af09d60d1cd746c2f3ba7ce0c446fa256be6ccda5e7d403520"},
|
||||
{file = "blockbuster-1.5.24.tar.gz", hash = "sha256:97645775761a5d425666ec0bc99629b65c7eccdc2f770d2439850682567af4ec"},
|
||||
@@ -59,7 +59,7 @@ description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = true
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
|
||||
{file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
|
||||
@@ -72,7 +72,7 @@ description = "Foreign Function Interface for Python calling C code."
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
|
||||
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
|
||||
@@ -153,7 +153,7 @@ description = "The Real First Universal Charset Detector. Open, modern and activ
|
||||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"},
|
||||
@@ -271,7 +271,7 @@ description = "Pickler class to extend the standard pickle.Pickler functionality
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "cloudpickle-3.1.1-py3-none-any.whl", hash = "sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e"},
|
||||
{file = "cloudpickle-3.1.1.tar.gz", hash = "sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64"},
|
||||
@@ -315,7 +315,7 @@ description = "cryptography is a package which provides cryptographic recipes an
|
||||
optional = true
|
||||
python-versions = "!=3.9.0,!=3.9.1,>=3.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1"},
|
||||
@@ -401,40 +401,40 @@ description = "Patch python built-in objects"
|
||||
optional = true
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\" and implementation_name == \"cpython\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\" and implementation_name == \"cpython\""
|
||||
files = [
|
||||
{file = "forbiddenfruit-0.1.4.tar.gz", hash = "sha256:e3f7e66561a29ae129aac139a85d610dbf3dd896128187ed5454b6421f624253"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
version = "0.16.0"
|
||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
|
||||
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.8"
|
||||
version = "1.0.9"
|
||||
description = "A minimal low-level HTTP client."
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be"},
|
||||
{file = "httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad"},
|
||||
{file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
|
||||
{file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
h11 = ">=0.13,<0.15"
|
||||
h11 = ">=0.16"
|
||||
|
||||
[package.extras]
|
||||
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||
@@ -449,7 +449,7 @@ description = "The next generation HTTP client."
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
|
||||
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
|
||||
@@ -475,7 +475,7 @@ description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = true
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
|
||||
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
|
||||
@@ -503,7 +503,7 @@ description = "Apply JSON-Patches (RFC 6902)"
|
||||
optional = true
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"},
|
||||
{file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"},
|
||||
@@ -519,7 +519,7 @@ description = "Identify specific nodes in a JSON document (RFC 6901)"
|
||||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"},
|
||||
{file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"},
|
||||
@@ -532,7 +532,7 @@ description = "A high-performance JSON Schema validator for Python"
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "jsonschema_rs-0.29.1-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7d84c4e1483a103694150b5706d638606443c814662738f14d34ca16948349df"},
|
||||
{file = "jsonschema_rs-0.29.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:0e7b72365ae0875c0e99f951ad4cec00c6f5e57b25ed5b8495b8f2c810ad21a6"},
|
||||
@@ -590,7 +590,7 @@ description = "Building applications with LLMs through composability"
|
||||
optional = true
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "langchain_core-0.3.55-py3-none-any.whl", hash = "sha256:b3cb36bf37755a616158a79866657c6697b43a2f7c69dd723ce425f1c76c1baa"},
|
||||
{file = "langchain_core-0.3.55.tar.gz", hash = "sha256:0f2b3e311621116a83510c70b0ac9d959030a0a457a69483535cff18501fedc9"},
|
||||
@@ -615,7 +615,7 @@ description = "Building stateful, multi-actor applications with LLMs"
|
||||
optional = true
|
||||
python-versions = "<4.0,>=3.9.0"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "langgraph-0.3.31-py3-none-any.whl", hash = "sha256:f42a6850d03696f2a54d3c833db39aa201cfda78217846a5b8936ad95fe41e6c"},
|
||||
{file = "langgraph-0.3.31.tar.gz", hash = "sha256:c76bbc8f26604929d6c2520d937b5602d5bb1dde2c0580398d015f7f6841f14c"},
|
||||
@@ -635,7 +635,7 @@ description = ""
|
||||
optional = true
|
||||
python-versions = "<4.0,>=3.11.0"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "langgraph_api-0.1.12-py3-none-any.whl", hash = "sha256:0f9417052ac75f6da892902083b7cf6a515bee12dd035cfbc3f0ddb813738830"},
|
||||
{file = "langgraph_api-0.1.12.tar.gz", hash = "sha256:1646a904121a5dc84cece6a81b9c49693ccfbd6f1a2904e0ed7aa7eb711e64fc"},
|
||||
@@ -667,7 +667,7 @@ description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = true
|
||||
python-versions = "<4.0.0,>=3.9.0"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "langgraph_checkpoint-2.0.24-py3-none-any.whl", hash = "sha256:3836e2909ef2387d1fa8d04ee3e2a353f980d519fd6c649af352676dc73d66b8"},
|
||||
{file = "langgraph_checkpoint-2.0.24.tar.gz", hash = "sha256:9596dad332344e7e871257be464df8a07c2e9bac66143081b11b9422b0167e5b"},
|
||||
@@ -684,7 +684,7 @@ description = "Library with high-level APIs for creating and executing LangGraph
|
||||
optional = true
|
||||
python-versions = "<4.0.0,>=3.9.0"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "langgraph_prebuilt-0.1.8-py3-none-any.whl", hash = "sha256:ae97b828ae00be2cefec503423aa782e1bff165e9b94592e224da132f2526968"},
|
||||
{file = "langgraph_prebuilt-0.1.8.tar.gz", hash = "sha256:4de7659151829b2b955b6798df6800e580e617782c15c2c5b29b139697491831"},
|
||||
@@ -701,7 +701,7 @@ description = "Inmem implementation for the LangGraph API server."
|
||||
optional = true
|
||||
python-versions = "<4.0,>=3.11.0"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "langgraph_runtime_inmem-0.0.4-py3-none-any.whl", hash = "sha256:6793e4c93039bfe522908b5990b318b1db347ab82c274f38fa8b826f21e9f9cf"},
|
||||
{file = "langgraph_runtime_inmem-0.0.4.tar.gz", hash = "sha256:15757ce7412ba503cd169e4c1946d0703e7c71d4951d578b2ae592a8a71ab039"},
|
||||
@@ -717,7 +717,7 @@ description = "SDK for interacting with LangGraph API"
|
||||
optional = true
|
||||
python-versions = "<4.0.0,>=3.9.0"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "langgraph_sdk-0.1.63-py3-none-any.whl", hash = "sha256:6fb78a7fc6a30eea43bd0d6401dbc9e3263d0d4c03f63c04035980da7e586b05"},
|
||||
{file = "langgraph_sdk-0.1.63.tar.gz", hash = "sha256:62bf2cc31e5aa6c5b9011ee1702bcf1e36e67e142a60bd97af2611162fb58e18"},
|
||||
@@ -734,7 +734,7 @@ description = "Client library to connect to the LangSmith LLM Tracing and Evalua
|
||||
optional = true
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "langsmith-0.3.33-py3-none-any.whl", hash = "sha256:6fa453942014945e1de7e283880ed3e8031b5d84e0dc75b87d101ecedb62371b"},
|
||||
{file = "langsmith-0.3.33.tar.gz", hash = "sha256:0f439e945528c6d14140137b918cc048aea04c6a987525926dbfda2560002924"},
|
||||
@@ -884,7 +884,7 @@ description = "Fast, correct Python JSON library supporting dataclasses, datetim
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "orjson-3.10.16-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4cb473b8e79154fa778fb56d2d73763d977be3dcc140587e07dbc545bbfc38f8"},
|
||||
{file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:622a8e85eeec1948690409a19ca1c7d9fd8ff116f4861d261e6ae2094fe59a00"},
|
||||
@@ -963,7 +963,7 @@ description = "Fast, correct Python msgpack library supporting dataclasses, date
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "ormsgpack-1.9.1-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f1f804fd9c0fd84213a6022c34172f82323b34afa7052a4af18797582cf56365"},
|
||||
{file = "ormsgpack-1.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eab5cec99c46276b37071d570aab98603f3d0309b3818da3247eb64bb95e5cfc"},
|
||||
@@ -1019,7 +1019,7 @@ files = [
|
||||
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
|
||||
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
|
||||
]
|
||||
markers = {main = "python_version >= \"3.11\" and extra == \"inmem\""}
|
||||
markers = {main = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""}
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
@@ -1044,7 +1044,7 @@ description = "C parser in Python"
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
|
||||
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
|
||||
@@ -1057,7 +1057,7 @@ description = "Data validation using Python type hints"
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\" and python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f"},
|
||||
{file = "pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3"},
|
||||
@@ -1080,7 +1080,7 @@ description = "Core functionality for Pydantic validation and serialization"
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\" and python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26"},
|
||||
{file = "pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927"},
|
||||
@@ -1193,7 +1193,7 @@ description = "JSON Web Token implementation in Python"
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"},
|
||||
{file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"},
|
||||
@@ -1305,7 +1305,7 @@ description = "YAML parser and emitter for Python"
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
|
||||
{file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
|
||||
@@ -1369,7 +1369,7 @@ description = "Python HTTP for Humans."
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
|
||||
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
|
||||
@@ -1392,7 +1392,7 @@ description = "A utility belt for advanced users of python-requests"
|
||||
optional = true
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"},
|
||||
{file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"},
|
||||
@@ -1436,7 +1436,7 @@ description = "Sniff out which async library your code is running under"
|
||||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
||||
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||
@@ -1449,7 +1449,7 @@ description = "SSE plugin for Starlette"
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "sse_starlette-2.1.3-py3-none-any.whl", hash = "sha256:8ec846438b4665b9e8c560fcdea6bc8081a3abf7942faa95e5a744999d219772"},
|
||||
{file = "sse_starlette-2.1.3.tar.gz", hash = "sha256:9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169"},
|
||||
@@ -1470,7 +1470,7 @@ description = "The little ASGI library that shines."
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"},
|
||||
{file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"},
|
||||
@@ -1489,7 +1489,7 @@ description = "Structured Logging for Python"
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "structlog-25.2.0-py3-none-any.whl", hash = "sha256:0fecea2e345d5d491b72f3db2e5fcd6393abfc8cd06a4851f21fcd4d1a99f437"},
|
||||
{file = "structlog-25.2.0.tar.gz", hash = "sha256:d9f9776944207d1035b8b26072b9b140c63702fd7aa57c2f85d28ab701bd8e92"},
|
||||
@@ -1508,7 +1508,7 @@ description = "Retry code until it succeeds"
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138"},
|
||||
{file = "tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb"},
|
||||
@@ -1572,7 +1572,7 @@ files = [
|
||||
{file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"},
|
||||
{file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"},
|
||||
]
|
||||
markers = {main = "python_version >= \"3.11\" and (python_version < \"3.13\" or extra == \"inmem\")"}
|
||||
markers = {main = "python_version >= \"3.11\" and python_version < \"4.0\" and (python_version < \"3.13\" or extra == \"inmem\")"}
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
@@ -1581,7 +1581,7 @@ description = "Runtime typing introspection tools"
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\" and python_version < \"4.0\""
|
||||
files = [
|
||||
{file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"},
|
||||
{file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"},
|
||||
@@ -1597,7 +1597,7 @@ description = "HTTP library with thread-safe connection pooling, file post, and
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"},
|
||||
{file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"},
|
||||
@@ -1616,7 +1616,7 @@ description = "The lightning-fast ASGI server."
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403"},
|
||||
{file = "uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328"},
|
||||
@@ -1679,7 +1679,7 @@ description = "Simple, modern and high performance file watching and code reload
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "watchfiles-1.0.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5c40fe7dd9e5f81e0847b1ea64e1f5dd79dd61afbedb57759df06767ac719b40"},
|
||||
{file = "watchfiles-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c0db396e6003d99bb2d7232c957b5f0b5634bbd1b24e381a5afcc880f7373fb"},
|
||||
@@ -1764,7 +1764,7 @@ description = "Python binding for xxHash"
|
||||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "xxhash-3.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ece616532c499ee9afbb83078b1b952beffef121d989841f7f4b3dc5ac0fd212"},
|
||||
{file = "xxhash-3.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3171f693dbc2cef6477054a665dc255d996646b4023fe56cb4db80e26f4cc520"},
|
||||
@@ -1898,7 +1898,7 @@ description = "Zstandard bindings for Python"
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.11\" and extra == \"inmem\""
|
||||
markers = "python_version >= \"3.11\" and python_version < \"4.0\" and extra == \"inmem\""
|
||||
files = [
|
||||
{file = "zstandard-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9"},
|
||||
{file = "zstandard-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880"},
|
||||
@@ -2010,5 +2010,5 @@ inmem = ["langgraph-api", "langgraph-runtime-inmem", "python-dotenv"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
content-hash = "6f3f275ae70749922db5bd1105711fbb0f8ac3b215982f7c75861c93d956e95d"
|
||||
python-versions = ">=3.9"
|
||||
content-hash = "d8faf1561b3a3a79be838303c07f8089215033eb41674cc13a39251ebfdc4214"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-cli"
|
||||
version = "0.2.6"
|
||||
version = "0.2.7"
|
||||
description = "CLI for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -12,7 +12,7 @@ packages = [{ include = "langgraph_cli" }]
|
||||
langgraph = "langgraph_cli.cli:cli"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
python = ">=3.9"
|
||||
click = "^8.1.7"
|
||||
langgraph-api = { version = ">=0.1.12,<0.2.0", optional = true, python = ">=3.11,<4.0" }
|
||||
langgraph-runtime-inmem = { version = ">=0.0.1,<0.1.0", optional = true, python = ">=3.11,<4.0" }
|
||||
|
||||
@@ -399,6 +399,17 @@
|
||||
"type": "string",
|
||||
"description": "Optional. Import path to a custom Starlette/FastAPI application to mount.\n"
|
||||
},
|
||||
"configurable_headers": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/ConfigurableHeaderConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Defines how headers are treated for a run's configuration.\n\nYou can include or exclude headers as configurable values to condition your\nagent's behavior or permissions on a request's headers."
|
||||
},
|
||||
"cors": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -433,6 +444,45 @@
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"ConfigurableHeaderConfig": {
|
||||
"title": "ConfigurableHeaderConfig",
|
||||
"description": "Customize which headers to include as configurable values in your runs.\n\nBy default, omits x-api-key, x-tenant-id, and x-service-key.\n\nExclusions (if provided) take precedence.\n\nEach value can be a raw string with an optional wildcard.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"excludes": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Headers to exclude. Applied before the 'includes' checks.\n"
|
||||
},
|
||||
"includes": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Headers to include (if not also matches against an 'exludes' pattern.\n"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"excludes",
|
||||
"includes"
|
||||
]
|
||||
},
|
||||
"CorsConfig": {
|
||||
"title": "CorsConfig",
|
||||
"description": "Specifies Cross-Origin Resource Sharing (CORS) rules for your server.\n\nIf omitted, defaults are typically very restrictive (often no cross-origin requests).\nConfigure carefully if you want to allow usage from browsers hosted on other domains.",
|
||||
|
||||
@@ -399,6 +399,17 @@
|
||||
"type": "string",
|
||||
"description": "Optional. Import path to a custom Starlette/FastAPI application to mount.\n"
|
||||
},
|
||||
"configurable_headers": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/ConfigurableHeaderConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Defines how headers are treated for a run's configuration.\n\nYou can include or exclude headers as configurable values to condition your\nagent's behavior or permissions on a request's headers."
|
||||
},
|
||||
"cors": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -433,6 +444,45 @@
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"ConfigurableHeaderConfig": {
|
||||
"title": "ConfigurableHeaderConfig",
|
||||
"description": "Customize which headers to include as configurable values in your runs.\n\nBy default, omits x-api-key, x-tenant-id, and x-service-key.\n\nExclusions (if provided) take precedence.\n\nEach value can be a raw string with an optional wildcard.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"excludes": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Headers to exclude. Applied before the 'includes' checks.\n"
|
||||
},
|
||||
"includes": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Headers to include (if not also matches against an 'exludes' pattern.\n"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"excludes",
|
||||
"includes"
|
||||
]
|
||||
},
|
||||
"CorsConfig": {
|
||||
"title": "CorsConfig",
|
||||
"description": "Specifies Cross-Origin Resource Sharing (CORS) rules for your server.\n\nIf omitted, defaults are typically very restrictive (often no cross-origin requests).\nConfigure carefully if you want to allow usage from browsers hosted on other domains.",
|
||||
|
||||
@@ -160,6 +160,110 @@ services:
|
||||
assert clean_empty_lines(actual_stdin) == expected_stdin
|
||||
|
||||
|
||||
def test_prepare_args_and_stdin_with_image() -> None:
|
||||
# this basically serves as an end-to-end test for using config and docker helpers
|
||||
config_path = pathlib.Path(__file__).parent / "langgraph.json"
|
||||
config = validate_config(
|
||||
Config(dependencies=[".", "../../.."], graphs={"agent": "agent.py:graph"})
|
||||
)
|
||||
port = 8000
|
||||
debugger_port = 8001
|
||||
debugger_graph_url = f"http://127.0.0.1:{port}"
|
||||
|
||||
actual_args, actual_stdin = prepare_args_and_stdin(
|
||||
capabilities=DEFAULT_DOCKER_CAPABILITIES,
|
||||
config_path=config_path,
|
||||
config=config,
|
||||
docker_compose=pathlib.Path("custom-docker-compose.yml"),
|
||||
port=port,
|
||||
debugger_port=debugger_port,
|
||||
debugger_base_url=debugger_graph_url,
|
||||
watch=True,
|
||||
image="my-cool-image",
|
||||
)
|
||||
|
||||
expected_args = [
|
||||
"--project-directory",
|
||||
str(pathlib.Path(__file__).parent.absolute()),
|
||||
"-f",
|
||||
"custom-docker-compose.yml",
|
||||
"-f",
|
||||
"-",
|
||||
]
|
||||
expected_stdin = f"""volumes:
|
||||
langgraph-data:
|
||||
driver: local
|
||||
services:
|
||||
langgraph-redis:
|
||||
image: redis:6
|
||||
healthcheck:
|
||||
test: redis-cli ping
|
||||
interval: 5s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
langgraph-postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
ports:
|
||||
- "5433:5432"
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
command:
|
||||
- postgres
|
||||
- -c
|
||||
- shared_preload_libraries=vector
|
||||
volumes:
|
||||
- langgraph-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: pg_isready -U postgres
|
||||
start_period: 10s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
interval: 60s
|
||||
start_interval: 1s
|
||||
langgraph-debugger:
|
||||
image: langchain/langgraph-debugger
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
langgraph-postgres:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "{debugger_port}:3968"
|
||||
environment:
|
||||
VITE_STUDIO_LOCAL_GRAPH_URL: {debugger_graph_url}
|
||||
langgraph-api:
|
||||
ports:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
langgraph-redis:
|
||||
condition: service_healthy
|
||||
langgraph-postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
REDIS_URI: redis://langgraph-redis:6379
|
||||
POSTGRES_URI: {DEFAULT_POSTGRES_URI}
|
||||
image: my-cool-image
|
||||
healthcheck:
|
||||
test: python /api/healthcheck.py
|
||||
interval: 60s
|
||||
start_interval: 1s
|
||||
start_period: 10s
|
||||
|
||||
|
||||
develop:
|
||||
watch:
|
||||
- path: langgraph.json
|
||||
action: rebuild
|
||||
- path: .
|
||||
action: rebuild
|
||||
- path: ../../..
|
||||
action: rebuild\
|
||||
"""
|
||||
assert actual_args == expected_args
|
||||
assert clean_empty_lines(actual_stdin) == expected_stdin
|
||||
|
||||
|
||||
def test_version_option() -> None:
|
||||
"""Test the --version option of the CLI."""
|
||||
runner = CliRunner()
|
||||
|
||||
@@ -78,7 +78,7 @@ test_watch_all:
|
||||
PYTHON_FILES=.
|
||||
MYPY_CACHE=.mypy_cache
|
||||
lint format: PYTHON_FILES=.
|
||||
lint_diff format_diff: PYTHON_FILES=$(shell git diff --name-only --relative --diff-filter=d main . | grep -E '\.py$$|\.ipynb$$')
|
||||
lint_diff format_diff: PYTHON_FILES=$(shell git diff --name-only --relative --diff-filter=d main . | grep -E r'\.py$$|\.ipynb$$')
|
||||
lint_package: PYTHON_FILES=langgraph
|
||||
lint_tests: PYTHON_FILES=tests
|
||||
lint_tests: MYPY_CACHE=.mypy_cache_test
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import operator
|
||||
from collections.abc import Sequence
|
||||
from functools import partial
|
||||
from random import choice
|
||||
from typing import Annotated, Optional, Sequence
|
||||
from typing import Annotated, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import operator
|
||||
from collections.abc import Sequence
|
||||
from functools import partial
|
||||
from random import choice
|
||||
from typing import Annotated, Optional, Sequence
|
||||
from typing import Annotated, Optional
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import operator
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from functools import partial
|
||||
from random import choice
|
||||
from typing import Annotated, Optional, Sequence
|
||||
from typing import Annotated, Optional
|
||||
|
||||
from langgraph.constants import END, START
|
||||
from langgraph.graph.state import StateGraph
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import functools
|
||||
import warnings
|
||||
from typing import Any, Callable, Type, TypeVar, Union, cast
|
||||
from typing import Any, Callable, TypeVar, Union, cast
|
||||
|
||||
|
||||
class LangGraphDeprecationWarning(DeprecationWarning):
|
||||
@@ -8,7 +8,7 @@ class LangGraphDeprecationWarning(DeprecationWarning):
|
||||
|
||||
|
||||
F = TypeVar("F", bound=Callable[..., Any])
|
||||
C = TypeVar("C", bound=Type[Any])
|
||||
C = TypeVar("C", bound=type[Any])
|
||||
|
||||
|
||||
def deprecated(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Any, Generic, Sequence, Type
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Generic
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -21,12 +22,12 @@ class AnyValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
return isinstance(value, AnyValue)
|
||||
|
||||
@property
|
||||
def ValueType(self) -> Type[Value]:
|
||||
def ValueType(self) -> type[Value]:
|
||||
"""The type of the value stored in the channel."""
|
||||
return self.typ
|
||||
|
||||
@property
|
||||
def UpdateType(self) -> Type[Value]:
|
||||
def UpdateType(self) -> type[Value]:
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Generic, Sequence, TypeVar
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import collections.abc
|
||||
from typing import Callable, Generic, Sequence, Type
|
||||
from collections.abc import Sequence
|
||||
from typing import Callable, Generic
|
||||
|
||||
from typing_extensions import NotRequired, Required, Self
|
||||
|
||||
@@ -31,7 +32,7 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
|
||||
__slots__ = ("value", "operator")
|
||||
|
||||
def __init__(self, typ: Type[Value], operator: Callable[[Value, Value], Value]):
|
||||
def __init__(self, typ: type[Value], operator: Callable[[Value, Value], Value]):
|
||||
super().__init__(typ)
|
||||
self.operator = operator
|
||||
# special forms from typing or collections.abc are not instantiable
|
||||
@@ -57,12 +58,12 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
)
|
||||
|
||||
@property
|
||||
def ValueType(self) -> Type[Value]:
|
||||
def ValueType(self) -> type[Value]:
|
||||
"""The type of the value stored in the channel."""
|
||||
return self.typ
|
||||
|
||||
@property
|
||||
def UpdateType(self) -> Type[Value]:
|
||||
def UpdateType(self) -> type[Value]:
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Any, Generic, NamedTuple, Optional, Sequence, Type, Union
|
||||
from collections.abc import Sequence, Set
|
||||
from typing import Any, Generic, NamedTuple, Optional, Union
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -8,11 +9,11 @@ from langgraph.errors import EmptyChannelError, InvalidUpdateError
|
||||
|
||||
|
||||
class WaitForNames(NamedTuple):
|
||||
names: set[Any]
|
||||
names: Set[Any]
|
||||
|
||||
|
||||
class DynamicBarrierValue(
|
||||
Generic[Value], BaseChannel[Value, Union[Value, WaitForNames], set[Value]]
|
||||
Generic[Value], BaseChannel[Value, Union[Value, WaitForNames], Set[Value]]
|
||||
):
|
||||
"""A channel that switches between two states
|
||||
|
||||
@@ -25,10 +26,10 @@ class DynamicBarrierValue(
|
||||
|
||||
__slots__ = ("names", "seen")
|
||||
|
||||
names: Optional[set[Value]]
|
||||
names: Optional[Set[Value]]
|
||||
seen: set[Value]
|
||||
|
||||
def __init__(self, typ: Type[Value]) -> None:
|
||||
def __init__(self, typ: type[Value]) -> None:
|
||||
super().__init__(typ)
|
||||
self.names = None
|
||||
self.seen = set()
|
||||
@@ -37,12 +38,12 @@ class DynamicBarrierValue(
|
||||
return isinstance(value, DynamicBarrierValue) and value.names == self.names
|
||||
|
||||
@property
|
||||
def ValueType(self) -> Type[Value]:
|
||||
def ValueType(self) -> type[Value]:
|
||||
"""The type of the value stored in the channel."""
|
||||
return self.typ
|
||||
|
||||
@property
|
||||
def UpdateType(self) -> Type[Value]:
|
||||
def UpdateType(self) -> type[Value]:
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
@@ -54,11 +55,11 @@ class DynamicBarrierValue(
|
||||
empty.seen = self.seen.copy()
|
||||
return empty
|
||||
|
||||
def checkpoint(self) -> tuple[Optional[set[Value]], set[Value]]:
|
||||
def checkpoint(self) -> tuple[Optional[Set[Value]], set[Value]]:
|
||||
return (self.names, self.seen)
|
||||
|
||||
def from_checkpoint(
|
||||
self, checkpoint: tuple[Optional[set[Value]], set[Value]]
|
||||
self, checkpoint: tuple[Optional[Set[Value]], set[Value]]
|
||||
) -> Self:
|
||||
empty = self.__class__(self.typ)
|
||||
empty.key = self.key
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Any, Generic, Sequence, Type
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Generic
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -21,12 +22,12 @@ class EphemeralValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
return isinstance(value, EphemeralValue) and value.guard == self.guard
|
||||
|
||||
@property
|
||||
def ValueType(self) -> Type[Value]:
|
||||
def ValueType(self) -> type[Value]:
|
||||
"""The type of the value stored in the channel."""
|
||||
return self.typ
|
||||
|
||||
@property
|
||||
def UpdateType(self) -> Type[Value]:
|
||||
def UpdateType(self) -> type[Value]:
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Any, Generic, Sequence, Type
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Generic
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -25,12 +26,12 @@ class LastValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
return isinstance(value, LastValue)
|
||||
|
||||
@property
|
||||
def ValueType(self) -> Type[Value]:
|
||||
def ValueType(self) -> type[Value]:
|
||||
"""The type of the value stored in the channel."""
|
||||
return self.typ
|
||||
|
||||
@property
|
||||
def UpdateType(self) -> Type[Value]:
|
||||
def UpdateType(self) -> type[Value]:
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Generic, Sequence, Type
|
||||
from collections.abc import Sequence
|
||||
from typing import Generic
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -15,7 +16,7 @@ class NamedBarrierValue(Generic[Value], BaseChannel[Value, Value, set[Value]]):
|
||||
names: set[Value]
|
||||
seen: set[Value]
|
||||
|
||||
def __init__(self, typ: Type[Value], names: set[Value]) -> None:
|
||||
def __init__(self, typ: type[Value], names: set[Value]) -> None:
|
||||
super().__init__(typ)
|
||||
self.names = names
|
||||
self.seen: set[str] = set()
|
||||
@@ -24,12 +25,12 @@ class NamedBarrierValue(Generic[Value], BaseChannel[Value, Value, set[Value]]):
|
||||
return isinstance(value, NamedBarrierValue) and value.names == self.names
|
||||
|
||||
@property
|
||||
def ValueType(self) -> Type[Value]:
|
||||
def ValueType(self) -> type[Value]:
|
||||
"""The type of the value stored in the channel."""
|
||||
return self.typ
|
||||
|
||||
@property
|
||||
def UpdateType(self) -> Type[Value]:
|
||||
def UpdateType(self) -> type[Value]:
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Any, Generic, Iterator, Sequence, Type, Union
|
||||
from collections.abc import Iterator, Sequence
|
||||
from typing import Any, Generic, Union
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -28,7 +29,7 @@ class Topic(
|
||||
|
||||
__slots__ = ("values", "accumulate")
|
||||
|
||||
def __init__(self, typ: Type[Value], accumulate: bool = False) -> None:
|
||||
def __init__(self, typ: type[Value], accumulate: bool = False) -> None:
|
||||
super().__init__(typ)
|
||||
# attrs
|
||||
self.accumulate = accumulate
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Generic, Sequence, Type
|
||||
from collections.abc import Sequence
|
||||
from typing import Generic
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -12,7 +13,7 @@ class UntrackedValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
|
||||
__slots__ = ("value", "guard")
|
||||
|
||||
def __init__(self, typ: Type[Value], guard: bool = True) -> None:
|
||||
def __init__(self, typ: type[Value], guard: bool = True) -> None:
|
||||
super().__init__(typ)
|
||||
self.guard = guard
|
||||
self.value = MISSING
|
||||
@@ -21,12 +22,12 @@ class UntrackedValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
return isinstance(value, UntrackedValue) and value.guard == self.guard
|
||||
|
||||
@property
|
||||
def ValueType(self) -> Type[Value]:
|
||||
def ValueType(self) -> type[Value]:
|
||||
"""The type of the value stored in the channel."""
|
||||
return self.typ
|
||||
|
||||
@property
|
||||
def UpdateType(self) -> Type[Value]:
|
||||
def UpdateType(self) -> type[Value]:
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import Any, Literal, Mapping, cast
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
from langgraph.types import Interrupt, Send # noqa: F401
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from collections.abc import Sequence
|
||||
from enum import Enum
|
||||
from typing import Any, Sequence
|
||||
from typing import Any
|
||||
|
||||
from langgraph.checkpoint.base import EmptyChannelError # noqa: F401
|
||||
from langgraph.types import Command, Interrupt
|
||||
|
||||
@@ -2,14 +2,13 @@ import asyncio
|
||||
import concurrent.futures
|
||||
import functools
|
||||
import inspect
|
||||
from collections.abc import Awaitable, Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Generic,
|
||||
Optional,
|
||||
Sequence,
|
||||
TypeVar,
|
||||
Union,
|
||||
get_args,
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
from collections.abc import Awaitable, Hashable, Sequence
|
||||
from inspect import (
|
||||
isfunction,
|
||||
ismethod,
|
||||
signature,
|
||||
)
|
||||
from itertools import zip_longest
|
||||
from types import FunctionType
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Hashable,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Sequence,
|
||||
Type,
|
||||
Union,
|
||||
cast,
|
||||
get_args,
|
||||
@@ -29,12 +27,17 @@ from langchain_core.runnables import (
|
||||
|
||||
from langgraph.constants import END, START
|
||||
from langgraph.errors import InvalidUpdateError
|
||||
from langgraph.pregel.write import ChannelWrite
|
||||
from langgraph.pregel.write import PASSTHROUGH, ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.types import Send
|
||||
from langgraph.utils.runnable import (
|
||||
RunnableCallable,
|
||||
)
|
||||
|
||||
Writer = Callable[
|
||||
[Sequence[Union[str, Send]], bool],
|
||||
Sequence[Union[ChannelWriteEntry, Send]],
|
||||
]
|
||||
|
||||
|
||||
def _get_branch_path_input_schema(
|
||||
path: Union[
|
||||
@@ -42,7 +45,7 @@ def _get_branch_path_input_schema(
|
||||
Callable[..., Awaitable[Union[Hashable, list[Hashable]]]],
|
||||
Runnable[Any, Union[Hashable, list[Hashable]]],
|
||||
],
|
||||
) -> Optional[Type[Any]]:
|
||||
) -> Optional[type[Any]]:
|
||||
input = None
|
||||
# detect input schema annotation in the branch callable
|
||||
try:
|
||||
@@ -85,7 +88,7 @@ class Branch(NamedTuple):
|
||||
path: Runnable[Any, Union[Hashable, list[Hashable]]]
|
||||
ends: Optional[dict[Hashable, str]]
|
||||
then: Optional[str] = None
|
||||
input_schema: Optional[Type[Any]] = None
|
||||
input_schema: Optional[type[Any]] = None
|
||||
|
||||
@classmethod
|
||||
def from_path(
|
||||
@@ -124,9 +127,7 @@ class Branch(NamedTuple):
|
||||
|
||||
def run(
|
||||
self,
|
||||
writer: Callable[
|
||||
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
|
||||
],
|
||||
writer: Writer,
|
||||
reader: Optional[Callable[[RunnableConfig], Any]] = None,
|
||||
) -> RunnableCallable:
|
||||
return ChannelWrite.register_writer(
|
||||
@@ -138,7 +139,15 @@ class Branch(NamedTuple):
|
||||
name=None,
|
||||
trace=False,
|
||||
func_accepts_config=True,
|
||||
),
|
||||
list(
|
||||
zip_longest(
|
||||
writer([e for e in self.ends.values()], True),
|
||||
[str(la) for la, e in self.ends.items()],
|
||||
)
|
||||
)
|
||||
if self.ends
|
||||
else None,
|
||||
)
|
||||
|
||||
def _route(
|
||||
@@ -147,9 +156,7 @@ class Branch(NamedTuple):
|
||||
config: RunnableConfig,
|
||||
*,
|
||||
reader: Optional[Callable[[RunnableConfig], Any]],
|
||||
writer: Callable[
|
||||
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
|
||||
],
|
||||
writer: Writer,
|
||||
) -> Runnable:
|
||||
if reader:
|
||||
value = reader(config)
|
||||
@@ -172,9 +179,7 @@ class Branch(NamedTuple):
|
||||
config: RunnableConfig,
|
||||
*,
|
||||
reader: Optional[Callable[[RunnableConfig], Any]],
|
||||
writer: Callable[
|
||||
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
|
||||
],
|
||||
writer: Writer,
|
||||
) -> Runnable:
|
||||
if reader:
|
||||
value = reader(config)
|
||||
@@ -193,9 +198,7 @@ class Branch(NamedTuple):
|
||||
|
||||
def _finish(
|
||||
self,
|
||||
writer: Callable[
|
||||
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
|
||||
],
|
||||
writer: Writer,
|
||||
input: Any,
|
||||
result: Any,
|
||||
config: RunnableConfig,
|
||||
@@ -212,4 +215,18 @@ class Branch(NamedTuple):
|
||||
raise ValueError("Branch did not return a valid destination")
|
||||
if any(p.node == END for p in destinations if isinstance(p, Send)):
|
||||
raise InvalidUpdateError("Cannot send a packet to the END node")
|
||||
return writer(destinations, config) or input
|
||||
entries = writer(destinations, False)
|
||||
if not entries:
|
||||
return input
|
||||
else:
|
||||
need_passthrough = False
|
||||
for e in entries:
|
||||
if isinstance(e, ChannelWriteEntry):
|
||||
if e.value is PASSTHROUGH:
|
||||
need_passthrough = True
|
||||
break
|
||||
if need_passthrough:
|
||||
return ChannelWrite(entries)
|
||||
else:
|
||||
ChannelWrite.do_write(config, entries)
|
||||
return input
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from collections.abc import Awaitable, Hashable, Sequence
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Hashable,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Sequence,
|
||||
Union,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
from langchain_core.runnables import Runnable
|
||||
from langchain_core.runnables.config import RunnableConfig
|
||||
from langchain_core.runnables.graph import Graph as DrawableGraph
|
||||
from langchain_core.runnables.graph import Node as DrawableNode
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.channels.ephemeral_value import EphemeralValue
|
||||
@@ -32,7 +26,6 @@ from langgraph.constants import (
|
||||
)
|
||||
from langgraph.graph.branch import Branch
|
||||
from langgraph.pregel import Channel, Pregel
|
||||
from langgraph.pregel.protocol import PregelProtocol
|
||||
from langgraph.pregel.read import PregelNode
|
||||
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.types import All, Checkpointer
|
||||
@@ -182,7 +175,7 @@ class Graph:
|
||||
# validate the condition
|
||||
if name in self.branches[source]:
|
||||
raise ValueError(
|
||||
f"Branch with name `{path.name}` already exists for node " f"`{source}`"
|
||||
f"Branch with name `{path.name}` already exists for node `{source}`"
|
||||
)
|
||||
# save it
|
||||
self.branches[source][name] = Branch.from_path(path, path_map, then, False)
|
||||
@@ -380,10 +373,10 @@ class CompiledGraph(Pregel):
|
||||
cast(list[str], self.nodes[end].channels).append(start)
|
||||
|
||||
def attach_branch(self, start: str, name: str, branch: Branch) -> None:
|
||||
def branch_writer(
|
||||
packets: Sequence[Union[str, Send]], config: RunnableConfig
|
||||
) -> Optional[ChannelWrite]:
|
||||
writes = [
|
||||
def get_writes(
|
||||
packets: Sequence[Union[str, Send]], static: bool = False
|
||||
) -> Sequence[Union[ChannelWriteEntry, Send]]:
|
||||
return [
|
||||
(
|
||||
ChannelWriteEntry(f"branch:{start}:{name}:{p}" if p != END else END)
|
||||
if not isinstance(p, Send)
|
||||
@@ -391,14 +384,13 @@ class CompiledGraph(Pregel):
|
||||
)
|
||||
for p in packets
|
||||
]
|
||||
return ChannelWrite(cast(Sequence[Union[ChannelWriteEntry, Send]], writes))
|
||||
|
||||
# add hidden start node
|
||||
if start == START and start not in self.nodes:
|
||||
self.nodes[start] = Channel.subscribe_to(START, tags=[TAG_HIDDEN])
|
||||
|
||||
# attach branch writer
|
||||
self.nodes[start] |= branch.run(branch_writer)
|
||||
self.nodes[start] |= branch.run(get_writes)
|
||||
|
||||
# attach branch readers
|
||||
ends = branch.ends.values() if branch.ends else [node for node in self.nodes]
|
||||
@@ -408,171 +400,3 @@ class CompiledGraph(Pregel):
|
||||
self.channels[channel_name] = EphemeralValue(Any)
|
||||
self.nodes[end].triggers.append(channel_name)
|
||||
cast(list[str], self.nodes[end].channels).append(channel_name)
|
||||
|
||||
async def aget_graph(
|
||||
self,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
*,
|
||||
xray: Union[int, bool] = False,
|
||||
) -> DrawableGraph:
|
||||
"""Returns a drawable representation of the computation graph."""
|
||||
from langgraph.pregel.remote import RemoteGraph
|
||||
|
||||
# gather subgraphs
|
||||
if xray:
|
||||
subpregels: dict[str, PregelProtocol] = {
|
||||
k: v
|
||||
async for k, v in self.aget_subgraphs()
|
||||
if isinstance(v, (CompiledGraph, RemoteGraph))
|
||||
}
|
||||
subgraphs = {
|
||||
k: v
|
||||
for k, v in zip(
|
||||
subpregels,
|
||||
await asyncio.gather(
|
||||
*(
|
||||
p.aget_graph(
|
||||
config,
|
||||
xray=xray
|
||||
if isinstance(xray, bool) or xray <= 0
|
||||
else xray - 1,
|
||||
)
|
||||
for p in subpregels.values()
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
else:
|
||||
subgraphs = {}
|
||||
|
||||
# draw the graph
|
||||
return self._draw_graph(config, subgraphs=subgraphs)
|
||||
|
||||
def get_graph(
|
||||
self,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
*,
|
||||
xray: Union[int, bool] = False,
|
||||
) -> DrawableGraph:
|
||||
"""Returns a drawable representation of the computation graph."""
|
||||
from langgraph.pregel.remote import RemoteGraph
|
||||
|
||||
# gather subgraphs
|
||||
if xray:
|
||||
subgraphs = {
|
||||
k: v.get_graph(
|
||||
config,
|
||||
xray=xray if isinstance(xray, bool) or xray <= 0 else xray - 1,
|
||||
)
|
||||
for k, v in self.get_subgraphs()
|
||||
if isinstance(v, (CompiledGraph, RemoteGraph))
|
||||
}
|
||||
else:
|
||||
subgraphs = {}
|
||||
|
||||
# draw the graph
|
||||
return self._draw_graph(config, subgraphs=subgraphs)
|
||||
|
||||
def _draw_graph(
|
||||
self,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
*,
|
||||
subgraphs: dict[str, DrawableGraph] = {},
|
||||
) -> DrawableGraph:
|
||||
# create the graph
|
||||
graph = DrawableGraph()
|
||||
start_nodes: dict[str, DrawableNode] = {
|
||||
START: graph.add_node(self.get_input_schema(config), START)
|
||||
}
|
||||
end_nodes: dict[str, DrawableNode] = {}
|
||||
|
||||
def add_edge(
|
||||
start: str,
|
||||
end: str,
|
||||
label: Optional[Hashable] = None,
|
||||
conditional: bool = False,
|
||||
) -> None:
|
||||
if end == END and END not in end_nodes:
|
||||
end_nodes[END] = graph.add_node(self.get_output_schema(config), END)
|
||||
if start not in start_nodes or end not in end_nodes:
|
||||
logger.warning(
|
||||
f"Could not add edge from '{start}' to '{end}' due to missing nodes"
|
||||
)
|
||||
return
|
||||
return graph.add_edge(
|
||||
start_nodes[start],
|
||||
end_nodes[end],
|
||||
str(label) if label is not None else None,
|
||||
conditional,
|
||||
)
|
||||
|
||||
for key, n in self.builder.nodes.items():
|
||||
node = n.runnable
|
||||
metadata = n.metadata or {}
|
||||
if key in self.interrupt_before_nodes and key in self.interrupt_after_nodes:
|
||||
metadata["__interrupt"] = "before,after"
|
||||
elif key in self.interrupt_before_nodes:
|
||||
metadata["__interrupt"] = "before"
|
||||
elif key in self.interrupt_after_nodes:
|
||||
metadata["__interrupt"] = "after"
|
||||
if key in subgraphs:
|
||||
subgraph = subgraphs[key]
|
||||
subgraph.trim_first_node()
|
||||
subgraph.trim_last_node()
|
||||
if len(subgraph.nodes) >= 1:
|
||||
e, s = graph.extend(subgraph, prefix=key)
|
||||
if e is None:
|
||||
logger.warning(
|
||||
f"Could not extend subgraph '{key}' due to missing entrypoint"
|
||||
)
|
||||
continue
|
||||
if s is not None:
|
||||
start_nodes[key] = s
|
||||
end_nodes[key] = e
|
||||
else:
|
||||
nn = graph.add_node(node, key, metadata=metadata or None)
|
||||
start_nodes[key] = nn
|
||||
end_nodes[key] = nn
|
||||
else:
|
||||
nn = graph.add_node(node, key, metadata=metadata or None)
|
||||
start_nodes[key] = nn
|
||||
end_nodes[key] = nn
|
||||
for start, end in sorted(self.builder._all_edges):
|
||||
add_edge(start, end)
|
||||
for start, branches in self.builder.branches.items():
|
||||
default_ends = {
|
||||
**{k: k for k in self.builder.nodes if k != start},
|
||||
END: END,
|
||||
}
|
||||
for _, branch in branches.items():
|
||||
if branch.ends is not None:
|
||||
ends = branch.ends
|
||||
elif branch.then is not None:
|
||||
ends = {k: k for k in default_ends if k not in (END, branch.then)}
|
||||
else:
|
||||
ends = cast(dict[Hashable, str], default_ends)
|
||||
for label, end in ends.items():
|
||||
add_edge(
|
||||
start,
|
||||
end,
|
||||
label if label != end else None,
|
||||
conditional=True,
|
||||
)
|
||||
if branch.then is not None:
|
||||
add_edge(end, branch.then)
|
||||
for key, n in self.builder.nodes.items():
|
||||
if isinstance(n.ends, dict):
|
||||
for end, label in n.ends.items():
|
||||
add_edge(key, end, label, conditional=True)
|
||||
elif isinstance(n.ends, tuple):
|
||||
for end in n.ends:
|
||||
add_edge(key, end, conditional=True)
|
||||
|
||||
return graph
|
||||
|
||||
def _repr_mimebundle_(self, **kwargs: Any) -> dict[str, Any]:
|
||||
"""Mime bundle used by Jupyter to display the graph"""
|
||||
return {
|
||||
"text/plain": repr(self),
|
||||
"image/png": self.get_graph().draw_mermaid_png(),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import uuid
|
||||
import warnings
|
||||
from collections.abc import Sequence
|
||||
from functools import partial
|
||||
from typing import (
|
||||
Annotated,
|
||||
@@ -7,7 +8,6 @@ from typing import (
|
||||
Callable,
|
||||
Literal,
|
||||
Optional,
|
||||
Sequence,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
@@ -3,28 +3,25 @@ import logging
|
||||
import weakref
|
||||
from inspect import isclass
|
||||
from typing import (
|
||||
Annotated,
|
||||
Any,
|
||||
Callable,
|
||||
Hashable,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
get_args,
|
||||
get_origin,
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel, Discriminator
|
||||
from pydantic.fields import FieldInfo
|
||||
from pydantic import BaseModel
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from typing_extensions import Annotated, Literal
|
||||
|
||||
__all__ = ["SchemaCoercionMapper"]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_cache: weakref.WeakKeyDictionary[Type[Any], dict[int, "SchemaCoercionMapper"]] = (
|
||||
|
||||
_cache: weakref.WeakKeyDictionary[type[Any], dict[int, "SchemaCoercionMapper"]] = (
|
||||
weakref.WeakKeyDictionary()
|
||||
)
|
||||
|
||||
@@ -34,7 +31,7 @@ class SchemaCoercionMapper:
|
||||
|
||||
def __new__(
|
||||
cls,
|
||||
schema: Type[Any],
|
||||
schema: type[Any],
|
||||
type_hints: Optional[dict[str, Any]] = None,
|
||||
*,
|
||||
max_depth: int = 12,
|
||||
@@ -48,7 +45,7 @@ class SchemaCoercionMapper:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
schema: Type[Any],
|
||||
schema: type[Any],
|
||||
type_hints: Optional[dict[str, Any]] = None,
|
||||
*,
|
||||
max_depth: int = 12,
|
||||
@@ -63,9 +60,7 @@ class SchemaCoercionMapper:
|
||||
self.type_hints = (
|
||||
type_hints
|
||||
if type_hints is not None
|
||||
else get_type_hints(
|
||||
schema, localns={schema.__name__: schema}, include_extras=True
|
||||
)
|
||||
else get_type_hints(schema, localns={schema.__name__: schema})
|
||||
)
|
||||
|
||||
if issubclass(schema, BaseModelV1):
|
||||
@@ -131,38 +126,15 @@ class SchemaCoercionMapper:
|
||||
if depth == 0:
|
||||
return self._passthrough
|
||||
|
||||
field_type, metadata = self._unwrap_annotated(field_type)
|
||||
origin = get_origin(field_type)
|
||||
|
||||
if (field_type in _IDENTITY_TYPES) or (origin in _IDENTITY_TYPES):
|
||||
return self._passthrough
|
||||
|
||||
if isinstance(field_type, TypeVar):
|
||||
concrete = self.type_hints.get(field_type) # type: ignore
|
||||
if concrete is not None:
|
||||
return self._build_coercer(concrete, depth - 1)
|
||||
return self._passthrough
|
||||
|
||||
if hasattr(field_type, "__parameters__") and hasattr(
|
||||
field_type, "model_fields"
|
||||
):
|
||||
try:
|
||||
type_hints = self.resolve_concrete_type_hints(field_type)
|
||||
|
||||
def generic_model_coercer(v: Any, d: int) -> Any:
|
||||
if not isinstance(v, dict):
|
||||
if throw:
|
||||
raise TypeError(
|
||||
f"Expected dict for {field_type}, got {type(v)}"
|
||||
)
|
||||
return v
|
||||
mapper = SchemaCoercionMapper(field_type, type_hints, max_depth=d)
|
||||
return mapper.coerce(v, d)
|
||||
|
||||
return generic_model_coercer
|
||||
except Exception as e:
|
||||
logger.debug(f"Generic type resolution failed: {e}")
|
||||
return self._passthrough
|
||||
if origin is Annotated:
|
||||
real_type, *_ = get_args(field_type)
|
||||
sub = self._build_coercer(real_type, depth - 1)
|
||||
return lambda v, d: sub(v, d)
|
||||
|
||||
if isclass(field_type):
|
||||
# This is needed bcs. of issubclass issues on older versions of python
|
||||
@@ -207,7 +179,6 @@ class SchemaCoercionMapper:
|
||||
return {sub(x, d - 1) for x in v}
|
||||
|
||||
return set_coercer
|
||||
|
||||
if origin is dict or field_type is dict:
|
||||
args = get_args(field_type)
|
||||
if len(args) != 2:
|
||||
@@ -215,7 +186,7 @@ class SchemaCoercionMapper:
|
||||
def dict_coercer(v: Any, d: Any) -> Any:
|
||||
if not isinstance(v, dict):
|
||||
if throw:
|
||||
raise TypeError("Expected dict, got %s" % type(v))
|
||||
raise TypeError(f"Expected dict, got {type(v)}")
|
||||
return v
|
||||
|
||||
return dict_coercer
|
||||
@@ -225,7 +196,7 @@ class SchemaCoercionMapper:
|
||||
def dict_coercer(v: Any, d: Any) -> Any:
|
||||
if not isinstance(v, dict):
|
||||
if throw:
|
||||
raise TypeError("Expected dict, got %s" % type(v))
|
||||
raise TypeError(f"Expected dict, got {type(v)}")
|
||||
return v
|
||||
return {k_sub(k, d - 1): v_sub(val, d - 1) for k, val in v.items()}
|
||||
|
||||
@@ -246,75 +217,27 @@ class SchemaCoercionMapper:
|
||||
)
|
||||
|
||||
if origin is Union:
|
||||
args = get_args(field_type)
|
||||
discriminator_key = self._extract_discriminator_key(metadata)
|
||||
none_in_union = False
|
||||
discriminator_map = {}
|
||||
|
||||
for arg in args:
|
||||
uargs = get_args(field_type)
|
||||
subs, none_in_union = [], False
|
||||
for ix, arg in enumerate(uargs):
|
||||
if arg is type(None):
|
||||
none_in_union = True
|
||||
continue
|
||||
base_type = arg
|
||||
if get_origin(arg) is Annotated:
|
||||
base_type, _ = get_args(arg)[0], get_args(arg)[1:]
|
||||
try:
|
||||
hint = get_type_hints(base_type)
|
||||
lit = hint.get(discriminator_key)
|
||||
if get_origin(lit) is Literal:
|
||||
for val in get_args(lit):
|
||||
discriminator_map[val] = base_type
|
||||
except Exception as e:
|
||||
if throw:
|
||||
raise e
|
||||
else:
|
||||
logger.debug(f"Failed to extract discriminator: {e}")
|
||||
else:
|
||||
subs.append(
|
||||
self._build_coercer(arg, depth - 1, throw=ix < len(uargs) - 1)
|
||||
)
|
||||
|
||||
def union_coercer(v: Any, d: Any) -> Any:
|
||||
if v is None and none_in_union:
|
||||
return None
|
||||
|
||||
tag = None
|
||||
if callable(discriminator_key):
|
||||
err = None
|
||||
for sp in subs:
|
||||
try:
|
||||
tag = discriminator_key(v)
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to call discriminator func: {e}")
|
||||
elif (
|
||||
isinstance(v, dict)
|
||||
and isinstance(discriminator_key, str)
|
||||
and discriminator_key in v
|
||||
):
|
||||
tag = v[discriminator_key]
|
||||
|
||||
if tag is not None:
|
||||
for arg in args:
|
||||
base_type = arg
|
||||
if get_origin(arg) is Annotated:
|
||||
base_type, _ = get_args(arg)[0], get_args(arg)[1:]
|
||||
|
||||
try:
|
||||
if issubclass(base_type, (BaseModel, BaseModelV1)):
|
||||
return SchemaCoercionMapper(
|
||||
base_type, max_depth=d
|
||||
).coerce(v, d)
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
f"Coercion with {base_type} failed for tag={tag}: {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
# fallback: try coercing each branch
|
||||
for arg in args:
|
||||
try:
|
||||
sub = self._build_coercer(arg, d - 1)
|
||||
return sub(v, d - 1)
|
||||
except Exception as e:
|
||||
if throw:
|
||||
raise e
|
||||
else:
|
||||
logger.debug(f"Fallback coercion failed for arg={arg}: {e}")
|
||||
|
||||
return sp(v, d - 1)
|
||||
except TypeError as e:
|
||||
err = e
|
||||
if err:
|
||||
raise err
|
||||
return v
|
||||
|
||||
return union_coercer
|
||||
@@ -326,58 +249,10 @@ class SchemaCoercionMapper:
|
||||
def _passthrough(v: Any, _d: Any) -> Any: # noqa: D401
|
||||
return v
|
||||
|
||||
@staticmethod
|
||||
def _extract_discriminator_key(meta: list[Any]) -> str | Callable[[Any], Hashable]:
|
||||
"""Extract discriminator field name or function from Annotated metadata"""
|
||||
for m in meta:
|
||||
if isinstance(m, FieldInfo):
|
||||
disc = getattr(m, "discriminator", None)
|
||||
if isinstance(disc, Discriminator):
|
||||
return disc.discriminator
|
||||
elif isinstance(disc, str):
|
||||
return disc
|
||||
return "type"
|
||||
|
||||
@staticmethod
|
||||
def _unwrap_annotated(tp: Any) -> tuple[Any, list[Any]]:
|
||||
"""Unwrap nested Annotated types, extracting the base type and all metadata"""
|
||||
metadata = []
|
||||
while get_origin(tp) is Annotated:
|
||||
tp, *meta = get_args(tp)
|
||||
metadata.extend(meta)
|
||||
return tp, metadata
|
||||
|
||||
@staticmethod
|
||||
def resolve_concrete_type_hints(generic_model_type: Any) -> dict[Any, Any]:
|
||||
"""Resolve concrete type hints in a generic model"""
|
||||
origin = get_origin(generic_model_type)
|
||||
args = get_args(generic_model_type)
|
||||
param_names = getattr(origin, "__parameters__", [])
|
||||
|
||||
if not args or not param_names:
|
||||
return {}
|
||||
|
||||
type_map = dict(zip(param_names, args))
|
||||
result = {}
|
||||
|
||||
for field_name, model_field in origin.model_fields.items():
|
||||
anno = model_field.annotation
|
||||
if get_origin(anno) is Annotated:
|
||||
base, *meta = get_args(anno)
|
||||
if isinstance(base, TypeVar) and base in type_map:
|
||||
result[field_name] = Annotated[type_map[base], *meta]
|
||||
else:
|
||||
result[field_name] = anno
|
||||
elif isinstance(anno, TypeVar) and anno in type_map:
|
||||
result[field_name] = type_map[anno]
|
||||
else:
|
||||
result[field_name] = anno
|
||||
|
||||
return result
|
||||
|
||||
|
||||
_adapter_cache: dict[Any, Callable[[Any], Any]] = {}
|
||||
|
||||
|
||||
_IDENTITY_TYPES: tuple[type[Any], ...] = (
|
||||
int,
|
||||
float,
|
||||
|
||||
@@ -3,19 +3,16 @@ import logging
|
||||
import typing
|
||||
import warnings
|
||||
from collections import defaultdict
|
||||
from collections.abc import Awaitable, Hashable, Sequence
|
||||
from functools import partial
|
||||
from inspect import isclass, isfunction, ismethod, signature
|
||||
from types import FunctionType
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Hashable,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Sequence,
|
||||
Type,
|
||||
Union,
|
||||
cast,
|
||||
get_args,
|
||||
@@ -84,7 +81,7 @@ from langgraph.utils.runnable import RunnableLike, coerce_to_runnable
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _warn_invalid_state_schema(schema: Union[Type[Any], Any]) -> None:
|
||||
def _warn_invalid_state_schema(schema: Union[type[Any], Any]) -> None:
|
||||
if isinstance(schema, type):
|
||||
return
|
||||
if typing.get_args(schema):
|
||||
@@ -108,7 +105,7 @@ def _get_node_name(node: RunnableLike) -> str:
|
||||
class StateNodeSpec(NamedTuple):
|
||||
runnable: Runnable
|
||||
metadata: Optional[dict[str, Any]]
|
||||
input: Type[Any]
|
||||
input: type[Any]
|
||||
retry_policy: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]]
|
||||
ends: Optional[Union[tuple[str, ...], dict[str, str]]] = EMPTY_SEQ
|
||||
|
||||
@@ -166,15 +163,15 @@ class StateGraph(Graph):
|
||||
nodes: dict[str, StateNodeSpec] # type: ignore[assignment]
|
||||
channels: dict[str, BaseChannel]
|
||||
managed: dict[str, ManagedValueSpec]
|
||||
schemas: dict[Type[Any], dict[str, Union[BaseChannel, ManagedValueSpec]]]
|
||||
schemas: dict[type[Any], dict[str, Union[BaseChannel, ManagedValueSpec]]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
state_schema: Optional[Type[Any]] = None,
|
||||
config_schema: Optional[Type[Any]] = None,
|
||||
state_schema: Optional[type[Any]] = None,
|
||||
config_schema: Optional[type[Any]] = None,
|
||||
*,
|
||||
input: Optional[Type[Any]] = None,
|
||||
output: Optional[Type[Any]] = None,
|
||||
input: Optional[type[Any]] = None,
|
||||
output: Optional[type[Any]] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
if state_schema is None:
|
||||
@@ -195,7 +192,7 @@ class StateGraph(Graph):
|
||||
self.schemas = {}
|
||||
self.channels = {}
|
||||
self.managed = {}
|
||||
self.type_hints: dict[Type[Any], dict[str, Any]] = {}
|
||||
self.type_hints: dict[type[Any], dict[str, Any]] = {}
|
||||
self.schema = state_schema
|
||||
self.input = input
|
||||
self.output = output
|
||||
@@ -211,7 +208,7 @@ class StateGraph(Graph):
|
||||
(start, end) for starts, end in self.waiting_edges for start in starts
|
||||
}
|
||||
|
||||
def _add_schema(self, schema: Type[Any], /, allow_managed: bool = True) -> None:
|
||||
def _add_schema(self, schema: type[Any], /, allow_managed: bool = True) -> None:
|
||||
if schema not in self.schemas:
|
||||
_warn_invalid_state_schema(schema)
|
||||
channels, managed, type_hints = _get_channels(schema)
|
||||
@@ -250,7 +247,7 @@ class StateGraph(Graph):
|
||||
node: RunnableLike,
|
||||
*,
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
input: Optional[Type[Any]] = None,
|
||||
input: Optional[type[Any]] = None,
|
||||
retry: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
|
||||
destinations: Optional[Union[dict[str, str], tuple[str, ...]]] = None,
|
||||
) -> Self:
|
||||
@@ -275,7 +272,7 @@ class StateGraph(Graph):
|
||||
action: RunnableLike,
|
||||
*,
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
input: Optional[Type[Any]] = None,
|
||||
input: Optional[type[Any]] = None,
|
||||
retry: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
|
||||
destinations: Optional[Union[dict[str, str], tuple[str, ...]]] = None,
|
||||
) -> Self:
|
||||
@@ -299,7 +296,7 @@ class StateGraph(Graph):
|
||||
action: Optional[RunnableLike] = None,
|
||||
*,
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
input: Optional[Type[Any]] = None,
|
||||
input: Optional[type[Any]] = None,
|
||||
retry: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
|
||||
destinations: Optional[Union[dict[str, str], tuple[str, ...]]] = None,
|
||||
) -> Self:
|
||||
@@ -527,7 +524,7 @@ class StateGraph(Graph):
|
||||
# validate the condition
|
||||
if name in self.branches[source]:
|
||||
raise ValueError(
|
||||
f"Branch with name `{path.name}` already exists for node " f"`{source}`"
|
||||
f"Branch with name `{path.name}` already exists for node `{source}`"
|
||||
)
|
||||
# save it
|
||||
self.branches[source][name] = Branch.from_path(path, path_map, then, True)
|
||||
@@ -686,12 +683,12 @@ class StateGraph(Graph):
|
||||
|
||||
class CompiledStateGraph(CompiledGraph):
|
||||
builder: StateGraph
|
||||
schema_to_mapper: dict[Type[Any], Optional[Callable[[Any], Any]]]
|
||||
schema_to_mapper: dict[type[Any], Optional[Callable[[Any], Any]]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
schema_to_mapper: dict[Type[Any], Optional[Callable[[Any], Any]]],
|
||||
schema_to_mapper: dict[type[Any], Optional[Callable[[Any], Any]]],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
@@ -774,7 +771,12 @@ class CompiledStateGraph(CompiledGraph):
|
||||
ChannelWriteTupleEntry(
|
||||
mapper=_get_root if output_keys == ["__root__"] else _get_updates
|
||||
),
|
||||
ChannelWriteTupleEntry(mapper=_control_branch),
|
||||
ChannelWriteTupleEntry(
|
||||
mapper=_control_branch,
|
||||
static=_control_static(node.ends)
|
||||
if node is not None and node.ends is not None
|
||||
else None,
|
||||
),
|
||||
)
|
||||
|
||||
# add node and output channel
|
||||
@@ -840,30 +842,32 @@ class CompiledStateGraph(CompiledGraph):
|
||||
def attach_branch(
|
||||
self, start: str, name: str, branch: Branch, *, with_reader: bool = True
|
||||
) -> None:
|
||||
def branch_writer(
|
||||
packets: Sequence[Union[str, Send]], config: RunnableConfig
|
||||
) -> None:
|
||||
if filtered := [p for p in packets if p != END]:
|
||||
writes = [
|
||||
(
|
||||
ChannelWriteEntry(CHANNEL_BRANCH_TO.format(p), None)
|
||||
if not isinstance(p, Send)
|
||||
else p
|
||||
)
|
||||
for p in filtered
|
||||
]
|
||||
if branch.then and branch.then != END:
|
||||
writes.append(
|
||||
ChannelWriteEntry(
|
||||
f"branch:{start}:{name}::then",
|
||||
WaitForNames(
|
||||
{p.node if isinstance(p, Send) else p for p in filtered}
|
||||
),
|
||||
)
|
||||
)
|
||||
ChannelWrite.do_write(
|
||||
config, cast(Sequence[Union[Send, ChannelWriteEntry]], writes)
|
||||
def get_writes(
|
||||
packets: Sequence[Union[str, Send]], static: bool = False
|
||||
) -> Sequence[Union[ChannelWriteEntry, Send]]:
|
||||
writes = [
|
||||
(
|
||||
ChannelWriteEntry(CHANNEL_BRANCH_TO.format(p), None)
|
||||
if not isinstance(p, Send)
|
||||
else p
|
||||
)
|
||||
for p in packets
|
||||
if (True if static else p != END)
|
||||
]
|
||||
if not writes:
|
||||
return []
|
||||
if branch.then and branch.then != END:
|
||||
writes.append(
|
||||
ChannelWriteEntry(
|
||||
f"branch:{start}:{name}::then",
|
||||
WaitForNames(
|
||||
frozenset(
|
||||
p.node if isinstance(p, Send) else p for p in packets
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
return writes
|
||||
|
||||
if with_reader:
|
||||
# get schema
|
||||
@@ -891,7 +895,7 @@ class CompiledStateGraph(CompiledGraph):
|
||||
reader = None
|
||||
|
||||
# attach branch publisher
|
||||
self.nodes[start].writers.append(branch.run(branch_writer, reader))
|
||||
self.nodes[start].writers.append(branch.run(get_writes, reader))
|
||||
|
||||
# attach then subscriber
|
||||
if branch.then and branch.then != END:
|
||||
@@ -1015,7 +1019,7 @@ class CompiledStateGraph(CompiledGraph):
|
||||
|
||||
|
||||
def _pick_mapper(
|
||||
state_keys: Sequence[str], schema: Type[Any], type_hints: Optional[dict[str, Any]]
|
||||
state_keys: Sequence[str], schema: type[Any], type_hints: Optional[dict[str, Any]]
|
||||
) -> Optional[Callable[[Any], Any]]:
|
||||
if state_keys == ["__root__"]:
|
||||
return None
|
||||
@@ -1027,7 +1031,7 @@ def _pick_mapper(
|
||||
return partial(_coerce_state, schema)
|
||||
|
||||
|
||||
def _coerce_state(schema: Type[Any], input: dict[str, Any]) -> dict[str, Any]:
|
||||
def _coerce_state(schema: type[Any], input: dict[str, Any]) -> dict[str, Any]:
|
||||
return schema(**input)
|
||||
|
||||
|
||||
@@ -1059,6 +1063,15 @@ def _control_branch(value: Any) -> Sequence[tuple[str, Any]]:
|
||||
return rtn
|
||||
|
||||
|
||||
def _control_static(
|
||||
ends: Union[tuple[str, ...], dict[str, str]],
|
||||
) -> Sequence[tuple[str, Any, Optional[str]]]:
|
||||
if isinstance(ends, dict):
|
||||
return [(CHANNEL_BRANCH_TO.format(k), None, label) for k, label in ends.items()]
|
||||
else:
|
||||
return [(CHANNEL_BRANCH_TO.format(e), None, None) for e in ends]
|
||||
|
||||
|
||||
def _get_root(input: Any) -> Optional[Sequence[tuple[str, Any]]]:
|
||||
if isinstance(input, Command):
|
||||
if input.graph == Command.PARENT:
|
||||
@@ -1083,7 +1096,7 @@ def _get_root(input: Any) -> Optional[Sequence[tuple[str, Any]]]:
|
||||
|
||||
|
||||
def _get_channels(
|
||||
schema: Type[dict],
|
||||
schema: type[dict],
|
||||
) -> tuple[dict[str, BaseChannel], dict[str, ManagedValueSpec], dict[str, Any]]:
|
||||
if not hasattr(schema, "__annotations__"):
|
||||
return (
|
||||
@@ -1137,7 +1150,7 @@ def _get_channel(
|
||||
return fallback
|
||||
|
||||
|
||||
def _is_field_channel(typ: Type[Any]) -> Optional[BaseChannel]:
|
||||
def _is_field_channel(typ: type[Any]) -> Optional[BaseChannel]:
|
||||
if hasattr(typ, "__metadata__"):
|
||||
meta = typ.__metadata__
|
||||
if len(meta) >= 1 and isinstance(meta[-1], BaseChannel):
|
||||
@@ -1147,7 +1160,7 @@ def _is_field_channel(typ: Type[Any]) -> Optional[BaseChannel]:
|
||||
return None
|
||||
|
||||
|
||||
def _is_field_binop(typ: Type[Any]) -> Optional[BinaryOperatorAggregate]:
|
||||
def _is_field_binop(typ: type[Any]) -> Optional[BinaryOperatorAggregate]:
|
||||
if hasattr(typ, "__metadata__"):
|
||||
meta = typ.__metadata__
|
||||
if len(meta) >= 1 and callable(meta[-1]):
|
||||
@@ -1168,7 +1181,7 @@ def _is_field_binop(typ: Type[Any]) -> Optional[BinaryOperatorAggregate]:
|
||||
return None
|
||||
|
||||
|
||||
def _is_field_managed_value(name: str, typ: Type[Any]) -> Optional[ManagedValueSpec]:
|
||||
def _is_field_managed_value(name: str, typ: type[Any]) -> Optional[ManagedValueSpec]:
|
||||
if hasattr(typ, "__metadata__"):
|
||||
meta = typ.__metadata__
|
||||
if len(meta) >= 1:
|
||||
@@ -1186,7 +1199,7 @@ def _is_field_managed_value(name: str, typ: Type[Any]) -> Optional[ManagedValueS
|
||||
|
||||
|
||||
def _get_schema(
|
||||
typ: Type,
|
||||
typ: type,
|
||||
schemas: dict,
|
||||
channels: dict,
|
||||
name: str,
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from inspect import isclass
|
||||
from typing import (
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Generic,
|
||||
Iterator,
|
||||
NamedTuple,
|
||||
Sequence,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
@@ -66,11 +63,11 @@ class WritableManagedValue(Generic[V, U], ManagedValue[V], ABC):
|
||||
|
||||
|
||||
class ConfiguredManagedValue(NamedTuple):
|
||||
cls: Type[ManagedValue]
|
||||
cls: type[ManagedValue]
|
||||
kwargs: dict[str, Any]
|
||||
|
||||
|
||||
ManagedValueSpec = Union[Type[ManagedValue], ConfiguredManagedValue]
|
||||
ManagedValueSpec = Union[type[ManagedValue], ConfiguredManagedValue]
|
||||
|
||||
|
||||
def is_managed_value(value: Any) -> TypeGuard[ManagedValueSpec]:
|
||||
@@ -79,7 +76,7 @@ def is_managed_value(value: Any) -> TypeGuard[ManagedValueSpec]:
|
||||
)
|
||||
|
||||
|
||||
def is_readonly_managed_value(value: Any) -> TypeGuard[Type[ManagedValue]]:
|
||||
def is_readonly_managed_value(value: Any) -> TypeGuard[type[ManagedValue]]:
|
||||
return (
|
||||
isclass(value)
|
||||
and issubclass(value, ManagedValue)
|
||||
@@ -90,7 +87,7 @@ def is_readonly_managed_value(value: Any) -> TypeGuard[Type[ManagedValue]]:
|
||||
)
|
||||
|
||||
|
||||
def is_writable_managed_value(value: Any) -> TypeGuard[Type[WritableManagedValue]]:
|
||||
def is_writable_managed_value(value: Any) -> TypeGuard[type[WritableManagedValue]]:
|
||||
return (isclass(value) and issubclass(value, WritableManagedValue)) or (
|
||||
isinstance(value, ConfiguredManagedValue)
|
||||
and issubclass(value.cls, WritableManagedValue)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
from contextlib import (
|
||||
AbstractAsyncContextManager,
|
||||
AbstractContextManager,
|
||||
asynccontextmanager,
|
||||
contextmanager,
|
||||
)
|
||||
from inspect import signature
|
||||
from typing import (
|
||||
Any,
|
||||
AsyncContextManager,
|
||||
AsyncIterator,
|
||||
Callable,
|
||||
ContextManager,
|
||||
Generic,
|
||||
Iterator,
|
||||
Optional,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
|
||||
@@ -28,15 +29,15 @@ class Context(ManagedValue[V], Generic[V]):
|
||||
def of(
|
||||
ctx: Union[
|
||||
None,
|
||||
Callable[..., ContextManager[V]],
|
||||
Type[ContextManager[V]],
|
||||
Callable[..., AsyncContextManager[V]],
|
||||
Type[AsyncContextManager[V]],
|
||||
Callable[..., AbstractContextManager[V]],
|
||||
type[AbstractContextManager[V]],
|
||||
Callable[..., AbstractAsyncContextManager[V]],
|
||||
type[AbstractAsyncContextManager[V]],
|
||||
] = None,
|
||||
actx: Optional[
|
||||
Union[
|
||||
Callable[..., AsyncContextManager[V]],
|
||||
Type[AsyncContextManager[V]],
|
||||
Callable[..., AbstractAsyncContextManager[V]],
|
||||
type[AbstractAsyncContextManager[V]],
|
||||
]
|
||||
] = None,
|
||||
) -> ConfiguredManagedValue:
|
||||
@@ -98,8 +99,10 @@ class Context(ManagedValue[V], Generic[V]):
|
||||
self,
|
||||
loop: LoopProtocol,
|
||||
*,
|
||||
ctx: Union[None, Type[ContextManager[V]], Type[AsyncContextManager[V]]] = None,
|
||||
actx: Optional[Type[AsyncContextManager[V]]] = None,
|
||||
ctx: Union[
|
||||
None, type[AbstractContextManager[V]], type[AbstractAsyncContextManager[V]]
|
||||
] = None,
|
||||
actx: Optional[type[AbstractAsyncContextManager[V]]] = None,
|
||||
) -> None:
|
||||
self.ctx = ctx
|
||||
self.actx = actx
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import collections.abc
|
||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from typing import (
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Iterator,
|
||||
Optional,
|
||||
Sequence,
|
||||
Type,
|
||||
)
|
||||
|
||||
from typing_extensions import NotRequired, Required, Self
|
||||
@@ -71,7 +68,7 @@ class SharedValue(WritableManagedValue[Value, Update]):
|
||||
yield value
|
||||
|
||||
def __init__(
|
||||
self, loop: LoopProtocol, *, typ: Type[Any], scope: str, key: str
|
||||
self, loop: LoopProtocol, *, typ: type[Any], scope: str, key: str
|
||||
) -> None:
|
||||
super().__init__(loop)
|
||||
if typ := _strip_extras(typ):
|
||||
|
||||
@@ -6,17 +6,11 @@ import concurrent.futures
|
||||
import queue
|
||||
import weakref
|
||||
from collections import defaultdict, deque
|
||||
from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
|
||||
from functools import partial
|
||||
from typing import (
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterator,
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Type,
|
||||
Union,
|
||||
cast,
|
||||
get_type_hints,
|
||||
@@ -93,6 +87,7 @@ from langgraph.pregel.algo import (
|
||||
)
|
||||
from langgraph.pregel.checkpoint import create_checkpoint, empty_checkpoint
|
||||
from langgraph.pregel.debug import tasks_w_writes
|
||||
from langgraph.pregel.draw import draw_graph
|
||||
from langgraph.pregel.io import map_input, read_channels
|
||||
from langgraph.pregel.loop import AsyncPregelLoop, StreamProtocol, SyncPregelLoop
|
||||
from langgraph.pregel.manager import AsyncChannelsManager, ChannelsManager
|
||||
@@ -108,6 +103,7 @@ from langgraph.store.base import BaseStore
|
||||
from langgraph.types import (
|
||||
All,
|
||||
Checkpointer,
|
||||
Interrupt,
|
||||
LoopProtocol,
|
||||
StateSnapshot,
|
||||
StateUpdate,
|
||||
@@ -141,8 +137,8 @@ class Channel:
|
||||
cls,
|
||||
channels: str,
|
||||
*,
|
||||
key: Optional[str] = None,
|
||||
tags: Optional[list[str]] = None,
|
||||
key: str | None = None,
|
||||
tags: list[str] | None = None,
|
||||
) -> PregelNode: ...
|
||||
|
||||
@overload
|
||||
@@ -152,16 +148,16 @@ class Channel:
|
||||
channels: Sequence[str],
|
||||
*,
|
||||
key: None = None,
|
||||
tags: Optional[list[str]] = None,
|
||||
tags: list[str] | None = None,
|
||||
) -> PregelNode: ...
|
||||
|
||||
@classmethod
|
||||
def subscribe_to(
|
||||
cls,
|
||||
channels: Union[str, Sequence[str]],
|
||||
channels: str | Sequence[str],
|
||||
*,
|
||||
key: Optional[str] = None,
|
||||
tags: Optional[list[str]] = None,
|
||||
key: str | None = None,
|
||||
tags: list[str] | None = None,
|
||||
) -> PregelNode:
|
||||
"""Runs process.invoke() each time channels are updated,
|
||||
with a dict of the channel values as input."""
|
||||
@@ -467,7 +463,7 @@ class Pregel(PregelProtocol):
|
||||
|
||||
nodes: dict[str, PregelNode]
|
||||
|
||||
channels: dict[str, Union[BaseChannel, ManagedValueSpec]]
|
||||
channels: dict[str, BaseChannel | ManagedValueSpec]
|
||||
|
||||
stream_mode: StreamMode = "values"
|
||||
"""Mode to stream output, defaults to 'values'."""
|
||||
@@ -476,18 +472,18 @@ class Pregel(PregelProtocol):
|
||||
"""Whether to force emitting stream events eagerly, automatically turned on
|
||||
for stream_mode "messages" and "custom"."""
|
||||
|
||||
output_channels: Union[str, Sequence[str]]
|
||||
output_channels: str | Sequence[str]
|
||||
|
||||
stream_channels: Optional[Union[str, Sequence[str]]] = None
|
||||
stream_channels: str | Sequence[str] | None = None
|
||||
"""Channels to stream, defaults to all channels not in reserved channels"""
|
||||
|
||||
interrupt_after_nodes: Union[All, Sequence[str]]
|
||||
interrupt_after_nodes: All | Sequence[str]
|
||||
|
||||
interrupt_before_nodes: Union[All, Sequence[str]]
|
||||
interrupt_before_nodes: All | Sequence[str]
|
||||
|
||||
input_channels: Union[str, Sequence[str]]
|
||||
input_channels: str | Sequence[str]
|
||||
|
||||
step_timeout: Optional[float] = None
|
||||
step_timeout: float | None = None
|
||||
"""Maximum time to wait for a step to complete, in seconds. Defaults to None."""
|
||||
|
||||
debug: bool
|
||||
@@ -496,44 +492,44 @@ class Pregel(PregelProtocol):
|
||||
checkpointer: Checkpointer = None
|
||||
"""Checkpointer used to save and load graph state. Defaults to None."""
|
||||
|
||||
store: Optional[BaseStore] = None
|
||||
store: BaseStore | None = None
|
||||
"""Memory store to use for SharedValues. Defaults to None."""
|
||||
|
||||
retry_policy: Optional[Sequence[RetryPolicy]] = None
|
||||
retry_policy: Sequence[RetryPolicy] | None = None
|
||||
"""Retry policies to use when running tasks. Set to None to disable."""
|
||||
|
||||
config_type: Optional[Type[Any]] = None
|
||||
config_type: type[Any] | None = None
|
||||
|
||||
input_model: Optional[Type[BaseModel]] = None
|
||||
input_model: type[BaseModel] | None = None
|
||||
|
||||
config: Optional[RunnableConfig] = None
|
||||
config: RunnableConfig | None = None
|
||||
|
||||
name: str = "LangGraph"
|
||||
|
||||
trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None
|
||||
trigger_to_nodes: Mapping[str, Sequence[str]] | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
nodes: dict[str, PregelNode],
|
||||
channels: Optional[dict[str, Union[BaseChannel, ManagedValueSpec]]],
|
||||
channels: dict[str, BaseChannel | ManagedValueSpec] | None,
|
||||
auto_validate: bool = True,
|
||||
stream_mode: StreamMode = "values",
|
||||
stream_eager: bool = False,
|
||||
output_channels: Union[str, Sequence[str]],
|
||||
stream_channels: Optional[Union[str, Sequence[str]]] = None,
|
||||
interrupt_after_nodes: Union[All, Sequence[str]] = (),
|
||||
interrupt_before_nodes: Union[All, Sequence[str]] = (),
|
||||
input_channels: Union[str, Sequence[str]],
|
||||
step_timeout: Optional[float] = None,
|
||||
debug: Optional[bool] = None,
|
||||
checkpointer: Optional[BaseCheckpointSaver] = None,
|
||||
store: Optional[BaseStore] = None,
|
||||
retry_policy: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
|
||||
config_type: Optional[Type[Any]] = None,
|
||||
input_model: Optional[Type[BaseModel]] = None,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None,
|
||||
output_channels: str | Sequence[str],
|
||||
stream_channels: str | Sequence[str] | None = None,
|
||||
interrupt_after_nodes: All | Sequence[str] = (),
|
||||
interrupt_before_nodes: All | Sequence[str] = (),
|
||||
input_channels: str | Sequence[str],
|
||||
step_timeout: float | None = None,
|
||||
debug: bool | None = None,
|
||||
checkpointer: BaseCheckpointSaver | None = None,
|
||||
store: BaseStore | None = None,
|
||||
retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
|
||||
config_type: type[Any] | None = None,
|
||||
input_model: type[BaseModel] | None = None,
|
||||
config: RunnableConfig | None = None,
|
||||
trigger_to_nodes: Mapping[str, Sequence[str]] | None = None,
|
||||
name: str = "LangGraph",
|
||||
) -> None:
|
||||
self.nodes = nodes
|
||||
@@ -562,22 +558,87 @@ class Pregel(PregelProtocol):
|
||||
self.validate()
|
||||
|
||||
def get_graph(
|
||||
self, config: Optional[RunnableConfig] = None, *, xray: Union[int, bool] = False
|
||||
self, config: RunnableConfig | None = None, *, xray: int | bool = False
|
||||
) -> Graph:
|
||||
raise NotImplementedError
|
||||
"""Returns a drawable representation of the computation graph."""
|
||||
# gather subgraphs
|
||||
if xray:
|
||||
subgraphs = {
|
||||
k: v.get_graph(
|
||||
config,
|
||||
xray=xray if isinstance(xray, bool) or xray <= 0 else xray - 1,
|
||||
)
|
||||
for k, v in self.get_subgraphs()
|
||||
}
|
||||
else:
|
||||
subgraphs = {}
|
||||
|
||||
return draw_graph(
|
||||
merge_configs(self.config, config),
|
||||
nodes=self.nodes,
|
||||
specs=self.channels,
|
||||
input_channels=self.input_channels,
|
||||
interrupt_after_nodes=self.interrupt_after_nodes,
|
||||
interrupt_before_nodes=self.interrupt_before_nodes,
|
||||
trigger_to_nodes=self.trigger_to_nodes,
|
||||
checkpointer=self.checkpointer,
|
||||
subgraphs=subgraphs,
|
||||
)
|
||||
|
||||
async def aget_graph(
|
||||
self, config: Optional[RunnableConfig] = None, *, xray: Union[int, bool] = False
|
||||
self, config: RunnableConfig | None = None, *, xray: int | bool = False
|
||||
) -> Graph:
|
||||
raise NotImplementedError
|
||||
"""Returns a drawable representation of the computation graph."""
|
||||
|
||||
def copy(self, update: Optional[dict[str, Any]] = None) -> Self:
|
||||
# gather subgraphs
|
||||
if xray:
|
||||
subpregels: dict[str, PregelProtocol] = {
|
||||
k: v async for k, v in self.aget_subgraphs()
|
||||
}
|
||||
subgraphs = {
|
||||
k: v
|
||||
for k, v in zip(
|
||||
subpregels,
|
||||
await asyncio.gather(
|
||||
*(
|
||||
p.aget_graph(
|
||||
config,
|
||||
xray=xray
|
||||
if isinstance(xray, bool) or xray <= 0
|
||||
else xray - 1,
|
||||
)
|
||||
for p in subpregels.values()
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
else:
|
||||
subgraphs = {}
|
||||
|
||||
return draw_graph(
|
||||
merge_configs(self.config, config),
|
||||
nodes=self.nodes,
|
||||
specs=self.channels,
|
||||
input_channels=self.input_channels,
|
||||
interrupt_after_nodes=self.interrupt_after_nodes,
|
||||
interrupt_before_nodes=self.interrupt_before_nodes,
|
||||
trigger_to_nodes=self.trigger_to_nodes,
|
||||
checkpointer=self.checkpointer,
|
||||
subgraphs=subgraphs,
|
||||
)
|
||||
|
||||
def _repr_mimebundle_(self, **kwargs: Any) -> dict[str, Any]:
|
||||
"""Mime bundle used by Jupyter to display the graph"""
|
||||
return {
|
||||
"text/plain": repr(self),
|
||||
"image/png": self.get_graph().draw_mermaid_png(),
|
||||
}
|
||||
|
||||
def copy(self, update: dict[str, Any] | None = None) -> Self:
|
||||
attrs = {**self.__dict__, **(update or {})}
|
||||
return self.__class__(**attrs)
|
||||
|
||||
def with_config(
|
||||
self, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
) -> Self:
|
||||
def with_config(self, config: RunnableConfig | None = None, **kwargs: Any) -> Self:
|
||||
return self.copy(
|
||||
{"config": merge_configs(self.config, config, cast(RunnableConfig, kwargs))}
|
||||
)
|
||||
@@ -632,9 +693,7 @@ class Pregel(PregelProtocol):
|
||||
]
|
||||
]
|
||||
|
||||
def config_schema(
|
||||
self, *, include: Optional[Sequence[str]] = None
|
||||
) -> Type[BaseModel]:
|
||||
def config_schema(self, *, include: Sequence[str] | None = None) -> type[BaseModel]:
|
||||
# If the config type is not set explicitly, we will try to infer it.
|
||||
# If the config type is provided, but isn't directly supported by pydantic
|
||||
# (e.g., vanilla python class), we will also delegate to the parent class,
|
||||
@@ -654,8 +713,8 @@ class Pregel(PregelProtocol):
|
||||
return create_model(self.get_name("Config"), field_definitions=fields)
|
||||
|
||||
def get_config_jsonschema(
|
||||
self, *, include: Optional[Sequence[str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
self, *, include: Sequence[str] | None = None
|
||||
) -> dict[str, Any]:
|
||||
schema = self.config_schema(include=include)
|
||||
if hasattr(schema, "model_json_schema"):
|
||||
return schema.model_json_schema()
|
||||
@@ -669,9 +728,7 @@ class Pregel(PregelProtocol):
|
||||
if isinstance(channel, BaseChannel):
|
||||
return channel.UpdateType
|
||||
|
||||
def get_input_schema(
|
||||
self, config: Optional[RunnableConfig] = None
|
||||
) -> Type[BaseModel]:
|
||||
def get_input_schema(self, config: RunnableConfig | None = None) -> type[BaseModel]:
|
||||
if self.input_model is not None:
|
||||
return self.input_model
|
||||
config = merge_configs(self.config, config)
|
||||
@@ -688,8 +745,8 @@ class Pregel(PregelProtocol):
|
||||
)
|
||||
|
||||
def get_input_jsonschema(
|
||||
self, config: Optional[RunnableConfig] = None
|
||||
) -> Dict[str, Any]:
|
||||
self, config: RunnableConfig | None = None
|
||||
) -> dict[str, Any]:
|
||||
schema = self.get_input_schema(config)
|
||||
if hasattr(schema, "model_json_schema"):
|
||||
return schema.model_json_schema()
|
||||
@@ -704,8 +761,8 @@ class Pregel(PregelProtocol):
|
||||
return channel.ValueType
|
||||
|
||||
def get_output_schema(
|
||||
self, config: Optional[RunnableConfig] = None
|
||||
) -> Type[BaseModel]:
|
||||
self, config: RunnableConfig | None = None
|
||||
) -> type[BaseModel]:
|
||||
config = merge_configs(self.config, config)
|
||||
if isinstance(self.output_channels, str):
|
||||
return super().get_output_schema(config)
|
||||
@@ -720,8 +777,8 @@ class Pregel(PregelProtocol):
|
||||
)
|
||||
|
||||
def get_output_jsonschema(
|
||||
self, config: Optional[RunnableConfig] = None
|
||||
) -> Dict[str, Any]:
|
||||
self, config: RunnableConfig | None = None
|
||||
) -> dict[str, Any]:
|
||||
schema = self.get_output_schema(config)
|
||||
if hasattr(schema, "model_json_schema"):
|
||||
return schema.model_json_schema()
|
||||
@@ -736,13 +793,13 @@ class Pregel(PregelProtocol):
|
||||
)
|
||||
|
||||
@property
|
||||
def stream_channels_asis(self) -> Union[str, Sequence[str]]:
|
||||
def stream_channels_asis(self) -> str | Sequence[str]:
|
||||
return self.stream_channels or [
|
||||
k for k in self.channels if isinstance(self.channels[k], BaseChannel)
|
||||
]
|
||||
|
||||
def get_subgraphs(
|
||||
self, *, namespace: Optional[str] = None, recurse: bool = False
|
||||
self, *, namespace: str | None = None, recurse: bool = False
|
||||
) -> Iterator[tuple[str, PregelProtocol]]:
|
||||
for name, node in self.nodes.items():
|
||||
# filter by prefix
|
||||
@@ -771,7 +828,7 @@ class Pregel(PregelProtocol):
|
||||
)
|
||||
|
||||
async def aget_subgraphs(
|
||||
self, *, namespace: Optional[str] = None, recurse: bool = False
|
||||
self, *, namespace: str | None = None, recurse: bool = False
|
||||
) -> AsyncIterator[tuple[str, PregelProtocol]]:
|
||||
for name, node in self.get_subgraphs(namespace=namespace, recurse=recurse):
|
||||
yield name, node
|
||||
@@ -783,8 +840,8 @@ class Pregel(PregelProtocol):
|
||||
def _prepare_state_snapshot(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
saved: Optional[CheckpointTuple],
|
||||
recurse: Optional[BaseCheckpointSaver] = None,
|
||||
saved: CheckpointTuple | None,
|
||||
recurse: BaseCheckpointSaver | None = None,
|
||||
apply_pending_writes: bool = False,
|
||||
) -> StateSnapshot:
|
||||
if not saved:
|
||||
@@ -832,7 +889,7 @@ class Pregel(PregelProtocol):
|
||||
# get the subgraphs
|
||||
subgraphs = dict(self.get_subgraphs())
|
||||
parent_ns = saved.config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "")
|
||||
task_states: dict[str, Union[RunnableConfig, StateSnapshot]] = {}
|
||||
task_states: dict[str, RunnableConfig | StateSnapshot] = {}
|
||||
for task in next_tasks.values():
|
||||
if task.name not in subgraphs:
|
||||
continue
|
||||
@@ -899,8 +956,8 @@ class Pregel(PregelProtocol):
|
||||
async def _aprepare_state_snapshot(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
saved: Optional[CheckpointTuple],
|
||||
recurse: Optional[BaseCheckpointSaver] = None,
|
||||
saved: CheckpointTuple | None,
|
||||
recurse: BaseCheckpointSaver | None = None,
|
||||
apply_pending_writes: bool = False,
|
||||
) -> StateSnapshot:
|
||||
if not saved:
|
||||
@@ -951,7 +1008,7 @@ class Pregel(PregelProtocol):
|
||||
# get the subgraphs
|
||||
subgraphs = {n: g async for n, g in self.aget_subgraphs()}
|
||||
parent_ns = saved.config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "")
|
||||
task_states: dict[str, Union[RunnableConfig, StateSnapshot]] = {}
|
||||
task_states: dict[str, RunnableConfig | StateSnapshot] = {}
|
||||
for task in next_tasks.values():
|
||||
if task.name not in subgraphs:
|
||||
continue
|
||||
@@ -1019,7 +1076,7 @@ class Pregel(PregelProtocol):
|
||||
self, config: RunnableConfig, *, subgraphs: bool = False
|
||||
) -> StateSnapshot:
|
||||
"""Get the current state of the graph."""
|
||||
checkpointer: Optional[BaseCheckpointSaver] = ensure_config(config)[CONF].get(
|
||||
checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get(
|
||||
CONFIG_KEY_CHECKPOINTER, self.checkpointer
|
||||
)
|
||||
if not checkpointer:
|
||||
@@ -1061,7 +1118,7 @@ class Pregel(PregelProtocol):
|
||||
self, config: RunnableConfig, *, subgraphs: bool = False
|
||||
) -> StateSnapshot:
|
||||
"""Get the current state of the graph."""
|
||||
checkpointer: Optional[BaseCheckpointSaver] = ensure_config(config)[CONF].get(
|
||||
checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get(
|
||||
CONFIG_KEY_CHECKPOINTER, self.checkpointer
|
||||
)
|
||||
if not checkpointer:
|
||||
@@ -1103,13 +1160,13 @@ class Pregel(PregelProtocol):
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
*,
|
||||
filter: Optional[Dict[str, Any]] = None,
|
||||
before: Optional[RunnableConfig] = None,
|
||||
limit: Optional[int] = None,
|
||||
filter: dict[str, Any] | None = None,
|
||||
before: RunnableConfig | None = None,
|
||||
limit: int | None = None,
|
||||
) -> Iterator[StateSnapshot]:
|
||||
config = ensure_config(config)
|
||||
"""Get the history of the state of the graph."""
|
||||
checkpointer: Optional[BaseCheckpointSaver] = ensure_config(config)[CONF].get(
|
||||
checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get(
|
||||
CONFIG_KEY_CHECKPOINTER, self.checkpointer
|
||||
)
|
||||
if not checkpointer:
|
||||
@@ -1154,13 +1211,13 @@ class Pregel(PregelProtocol):
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
*,
|
||||
filter: Optional[Dict[str, Any]] = None,
|
||||
before: Optional[RunnableConfig] = None,
|
||||
limit: Optional[int] = None,
|
||||
filter: dict[str, Any] | None = None,
|
||||
before: RunnableConfig | None = None,
|
||||
limit: int | None = None,
|
||||
) -> AsyncIterator[StateSnapshot]:
|
||||
config = ensure_config(config)
|
||||
"""Get the history of the state of the graph."""
|
||||
checkpointer: Optional[BaseCheckpointSaver] = ensure_config(config)[CONF].get(
|
||||
checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get(
|
||||
CONFIG_KEY_CHECKPOINTER, self.checkpointer
|
||||
)
|
||||
if not checkpointer:
|
||||
@@ -1225,7 +1282,7 @@ class Pregel(PregelProtocol):
|
||||
RunnableConfig: The updated config.
|
||||
"""
|
||||
|
||||
checkpointer: Optional[BaseCheckpointSaver] = ensure_config(config)[CONF].get(
|
||||
checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get(
|
||||
CONFIG_KEY_CHECKPOINTER, self.checkpointer
|
||||
)
|
||||
if not checkpointer:
|
||||
@@ -1500,7 +1557,7 @@ class Pregel(PregelProtocol):
|
||||
next_tasks[tid].writes.append((k, v))
|
||||
if tasks := [t for t in next_tasks.values() if t.writes]:
|
||||
apply_writes(checkpoint, channels, tasks, None)
|
||||
valid_updates: list[tuple[str, Optional[dict[str, Any]]]] = []
|
||||
valid_updates: list[tuple[str, dict[str, Any] | None]] = []
|
||||
if len(updates) == 1:
|
||||
values, as_node = updates[0]
|
||||
# find last node that updated the state, if not provided
|
||||
@@ -1639,7 +1696,7 @@ class Pregel(PregelProtocol):
|
||||
RunnableConfig: The updated config.
|
||||
"""
|
||||
|
||||
checkpointer: Optional[BaseCheckpointSaver] = ensure_config(config)[CONF].get(
|
||||
checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get(
|
||||
CONFIG_KEY_CHECKPOINTER, self.checkpointer
|
||||
)
|
||||
if not checkpointer:
|
||||
@@ -1914,7 +1971,7 @@ class Pregel(PregelProtocol):
|
||||
next_tasks[tid].writes.append((k, v))
|
||||
if tasks := [t for t in next_tasks.values() if t.writes]:
|
||||
apply_writes(checkpoint, channels, tasks, None)
|
||||
valid_updates: list[tuple[str, Optional[dict[str, Any]]]] = []
|
||||
valid_updates: list[tuple[str, dict[str, Any] | None]] = []
|
||||
if len(updates) == 1:
|
||||
values, as_node = updates[0]
|
||||
# find last node that updated the state, if not provided
|
||||
@@ -2034,8 +2091,8 @@ class Pregel(PregelProtocol):
|
||||
def update_state(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
values: Optional[Union[dict[str, Any], Any]],
|
||||
as_node: Optional[str] = None,
|
||||
values: dict[str, Any] | Any | None,
|
||||
as_node: str | None = None,
|
||||
) -> RunnableConfig:
|
||||
"""Update the state of the graph with the given values, as if they came from
|
||||
node `as_node`. If `as_node` is not provided, it will be set to the last node
|
||||
@@ -2047,7 +2104,7 @@ class Pregel(PregelProtocol):
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
values: dict[str, Any] | Any,
|
||||
as_node: Optional[str] = None,
|
||||
as_node: str | None = None,
|
||||
) -> RunnableConfig:
|
||||
"""Update the state of the graph asynchronously with the given values, as if they came from
|
||||
node `as_node`. If `as_node` is not provided, it will be set to the last node
|
||||
@@ -2059,19 +2116,19 @@ class Pregel(PregelProtocol):
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
*,
|
||||
stream_mode: Optional[Union[StreamMode, list[StreamMode]]],
|
||||
output_keys: Optional[Union[str, Sequence[str]]],
|
||||
interrupt_before: Optional[Union[All, Sequence[str]]],
|
||||
interrupt_after: Optional[Union[All, Sequence[str]]],
|
||||
debug: Optional[bool],
|
||||
stream_mode: StreamMode | list[StreamMode] | None,
|
||||
output_keys: str | Sequence[str] | None,
|
||||
interrupt_before: All | Sequence[str] | None,
|
||||
interrupt_after: All | Sequence[str] | None,
|
||||
debug: bool | None,
|
||||
) -> tuple[
|
||||
bool,
|
||||
set[StreamMode],
|
||||
Union[str, Sequence[str]],
|
||||
Union[All, Sequence[str]],
|
||||
Union[All, Sequence[str]],
|
||||
Optional[BaseCheckpointSaver],
|
||||
Optional[BaseStore],
|
||||
str | Sequence[str],
|
||||
All | Sequence[str],
|
||||
All | Sequence[str],
|
||||
BaseCheckpointSaver | None,
|
||||
BaseStore | None,
|
||||
]:
|
||||
if config["recursion_limit"] < 1:
|
||||
raise ValueError("recursion_limit must be at least 1")
|
||||
@@ -2089,7 +2146,7 @@ class Pregel(PregelProtocol):
|
||||
# if being called as a node in another graph, always use values mode
|
||||
stream_mode = ["values"]
|
||||
if self.checkpointer is False:
|
||||
checkpointer: Optional[BaseCheckpointSaver] = None
|
||||
checkpointer: BaseCheckpointSaver | None = None
|
||||
elif CONFIG_KEY_CHECKPOINTER in config.get(CONF, {}):
|
||||
checkpointer = config[CONF][CONFIG_KEY_CHECKPOINTER]
|
||||
elif self.checkpointer is True:
|
||||
@@ -2101,7 +2158,7 @@ class Pregel(PregelProtocol):
|
||||
f"Checkpointer requires one or more of the following 'configurable' keys: {[s.id for s in checkpointer.config_specs]}"
|
||||
)
|
||||
if CONFIG_KEY_STORE in config.get(CONF, {}):
|
||||
store: Optional[BaseStore] = config[CONF][CONFIG_KEY_STORE]
|
||||
store: BaseStore | None = config[CONF][CONFIG_KEY_STORE]
|
||||
else:
|
||||
store = self.store
|
||||
return (
|
||||
@@ -2116,17 +2173,17 @@ class Pregel(PregelProtocol):
|
||||
|
||||
def stream(
|
||||
self,
|
||||
input: Union[dict[str, Any], Any],
|
||||
config: Optional[RunnableConfig] = None,
|
||||
input: dict[str, Any] | Any,
|
||||
config: RunnableConfig | None = None,
|
||||
*,
|
||||
stream_mode: Optional[Union[StreamMode, list[StreamMode]]] = None,
|
||||
output_keys: Optional[Union[str, Sequence[str]]] = None,
|
||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
||||
checkpoint_during: Optional[bool] = None,
|
||||
debug: Optional[bool] = None,
|
||||
stream_mode: StreamMode | list[StreamMode] | None = None,
|
||||
output_keys: str | Sequence[str] | None = None,
|
||||
interrupt_before: All | Sequence[str] | None = None,
|
||||
interrupt_after: All | Sequence[str] | None = None,
|
||||
checkpoint_during: bool | None = None,
|
||||
debug: bool | None = None,
|
||||
subgraphs: bool = False,
|
||||
) -> Iterator[Union[dict[str, Any], Any]]:
|
||||
) -> Iterator[dict[str, Any] | Any]:
|
||||
"""Stream graph steps for a single input.
|
||||
|
||||
Args:
|
||||
@@ -2135,7 +2192,7 @@ class Pregel(PregelProtocol):
|
||||
stream_mode: The mode to stream output, defaults to self.stream_mode.
|
||||
Options are:
|
||||
|
||||
- `"values"`: Emit all values in the state after each step.
|
||||
- `"values"`: Emit all values in the state after each step, including interrupts.
|
||||
When used with functional API, values are emitted once at the end of the workflow.
|
||||
- `"updates"`: Emit only the node or task names and updates returned by the nodes or tasks after each step.
|
||||
If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are emitted separately.
|
||||
@@ -2352,7 +2409,7 @@ class Pregel(PregelProtocol):
|
||||
):
|
||||
# we are careful to have a single waiter live at any one time
|
||||
# because on exit we increment semaphore count by exactly 1
|
||||
waiter: Optional[concurrent.futures.Future] = None
|
||||
waiter: concurrent.futures.Future | None = None
|
||||
# because sync futures cannot be cancelled, we instead
|
||||
# release the stream semaphore on exit, which will cause
|
||||
# a pending waiter to return immediately
|
||||
@@ -2403,17 +2460,17 @@ class Pregel(PregelProtocol):
|
||||
|
||||
async def astream(
|
||||
self,
|
||||
input: Union[dict[str, Any], Any],
|
||||
config: Optional[RunnableConfig] = None,
|
||||
input: dict[str, Any] | Any,
|
||||
config: RunnableConfig | None = None,
|
||||
*,
|
||||
stream_mode: Optional[Union[StreamMode, list[StreamMode]]] = None,
|
||||
output_keys: Optional[Union[str, Sequence[str]]] = None,
|
||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
||||
checkpoint_during: Optional[bool] = None,
|
||||
debug: Optional[bool] = None,
|
||||
stream_mode: StreamMode | list[StreamMode] | None = None,
|
||||
output_keys: str | Sequence[str] | None = None,
|
||||
interrupt_before: All | Sequence[str] | None = None,
|
||||
interrupt_after: All | Sequence[str] | None = None,
|
||||
checkpoint_during: bool | None = None,
|
||||
debug: bool | None = None,
|
||||
subgraphs: bool = False,
|
||||
) -> AsyncIterator[Union[dict[str, Any], Any]]:
|
||||
) -> AsyncIterator[dict[str, Any] | Any]:
|
||||
"""Stream graph steps for a single input.
|
||||
|
||||
Args:
|
||||
@@ -2422,7 +2479,7 @@ class Pregel(PregelProtocol):
|
||||
stream_mode: The mode to stream output, defaults to self.stream_mode.
|
||||
Options are:
|
||||
|
||||
- `"values"`: Emit all values in the state after each step.
|
||||
- `"values"`: Emit all values in the state after each step, including interrupts.
|
||||
When used with functional API, values are emitted once at the end of the workflow.
|
||||
- `"updates"`: Emit only the node or task names and updates returned by the nodes or tasks after each step.
|
||||
If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are emitted separately.
|
||||
@@ -2704,17 +2761,17 @@ class Pregel(PregelProtocol):
|
||||
|
||||
def invoke(
|
||||
self,
|
||||
input: Union[dict[str, Any], Any],
|
||||
config: Optional[RunnableConfig] = None,
|
||||
input: dict[str, Any] | Any,
|
||||
config: RunnableConfig | None = None,
|
||||
*,
|
||||
stream_mode: StreamMode = "values",
|
||||
output_keys: Optional[Union[str, Sequence[str]]] = None,
|
||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
||||
checkpoint_during: Optional[bool] = None,
|
||||
debug: Optional[bool] = None,
|
||||
output_keys: str | Sequence[str] | None = None,
|
||||
interrupt_before: All | Sequence[str] | None = None,
|
||||
interrupt_after: All | Sequence[str] | None = None,
|
||||
checkpoint_during: bool | None = None,
|
||||
debug: bool | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Union[dict[str, Any], Any]:
|
||||
) -> dict[str, Any] | Any:
|
||||
"""Run the graph with a single input and config.
|
||||
|
||||
Args:
|
||||
@@ -2732,10 +2789,11 @@ class Pregel(PregelProtocol):
|
||||
If stream_mode is not "values", it returns a list of output chunks.
|
||||
"""
|
||||
output_keys = output_keys if output_keys is not None else self.output_channels
|
||||
if stream_mode == "values":
|
||||
latest: Union[dict[str, Any], Any] = None
|
||||
else:
|
||||
chunks = []
|
||||
|
||||
latest: Union[dict[str, Any], Any] = None
|
||||
chunks: list[Union[dict[str, Any], Any]] = []
|
||||
interrupts: list[Interrupt] = []
|
||||
|
||||
for chunk in self.stream(
|
||||
input,
|
||||
config,
|
||||
@@ -2748,27 +2806,40 @@ class Pregel(PregelProtocol):
|
||||
**kwargs,
|
||||
):
|
||||
if stream_mode == "values":
|
||||
latest = chunk
|
||||
if (
|
||||
isinstance(chunk, dict)
|
||||
and (ints := chunk.get(INTERRUPT)) is not None
|
||||
):
|
||||
interrupts.extend(ints)
|
||||
else:
|
||||
latest = chunk
|
||||
else:
|
||||
chunks.append(chunk)
|
||||
|
||||
if stream_mode == "values":
|
||||
if interrupts:
|
||||
return (
|
||||
{**latest, INTERRUPT: interrupts}
|
||||
if isinstance(latest, dict)
|
||||
else {INTERRUPT: interrupts}
|
||||
)
|
||||
return latest
|
||||
else:
|
||||
return chunks
|
||||
|
||||
async def ainvoke(
|
||||
self,
|
||||
input: Union[dict[str, Any], Any],
|
||||
config: Optional[RunnableConfig] = None,
|
||||
input: dict[str, Any] | Any,
|
||||
config: RunnableConfig | None = None,
|
||||
*,
|
||||
stream_mode: StreamMode = "values",
|
||||
output_keys: Optional[Union[str, Sequence[str]]] = None,
|
||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
||||
checkpoint_during: Optional[bool] = None,
|
||||
debug: Optional[bool] = None,
|
||||
output_keys: str | Sequence[str] | None = None,
|
||||
interrupt_before: All | Sequence[str] | None = None,
|
||||
interrupt_after: All | Sequence[str] | None = None,
|
||||
checkpoint_during: bool | None = None,
|
||||
debug: bool | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Union[dict[str, Any], Any]:
|
||||
) -> dict[str, Any] | Any:
|
||||
"""Asynchronously invoke the graph on a single input.
|
||||
|
||||
Args:
|
||||
@@ -2787,10 +2858,11 @@ class Pregel(PregelProtocol):
|
||||
"""
|
||||
|
||||
output_keys = output_keys if output_keys is not None else self.output_channels
|
||||
if stream_mode == "values":
|
||||
latest: Union[dict[str, Any], Any] = None
|
||||
else:
|
||||
chunks = []
|
||||
|
||||
latest: Union[dict[str, Any], Any] = None
|
||||
chunks: list[Union[dict[str, Any], Any]] = []
|
||||
interrupts: list[Interrupt] = []
|
||||
|
||||
async for chunk in self.astream(
|
||||
input,
|
||||
config,
|
||||
@@ -2803,10 +2875,23 @@ class Pregel(PregelProtocol):
|
||||
**kwargs,
|
||||
):
|
||||
if stream_mode == "values":
|
||||
latest = chunk
|
||||
if (
|
||||
isinstance(chunk, dict)
|
||||
and (ints := chunk.get(INTERRUPT)) is not None
|
||||
):
|
||||
interrupts.extend(ints)
|
||||
else:
|
||||
latest = chunk
|
||||
else:
|
||||
chunks.append(chunk)
|
||||
|
||||
if stream_mode == "values":
|
||||
if interrupts:
|
||||
return (
|
||||
{**latest, INTERRUPT: interrupts}
|
||||
if isinstance(latest, dict)
|
||||
else {INTERRUPT: interrupts}
|
||||
)
|
||||
return latest
|
||||
else:
|
||||
return chunks
|
||||
|
||||
@@ -3,19 +3,17 @@ import itertools
|
||||
import sys
|
||||
import threading
|
||||
from collections import defaultdict, deque
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
from copy import copy
|
||||
from functools import partial
|
||||
from hashlib import sha1
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Iterable,
|
||||
Literal,
|
||||
Mapping,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Protocol,
|
||||
Sequence,
|
||||
Union,
|
||||
cast,
|
||||
overload,
|
||||
@@ -544,11 +542,14 @@ def prepare_single_task(
|
||||
str(task_path[2]),
|
||||
)
|
||||
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
|
||||
# we append True to the task path to indicate that a call is being
|
||||
# made, so we should not return interrupts from this task (responsibility lies with the parent)
|
||||
task_path = (*task_path[:3], True)
|
||||
metadata = {
|
||||
"langgraph_step": step,
|
||||
"langgraph_node": name,
|
||||
"langgraph_triggers": triggers,
|
||||
"langgraph_path": task_path[:3],
|
||||
"langgraph_path": task_path,
|
||||
"langgraph_checkpoint_ns": task_checkpoint_ns,
|
||||
}
|
||||
if task_id_checksum is not None:
|
||||
@@ -577,7 +578,7 @@ def prepare_single_task(
|
||||
local_read,
|
||||
channels,
|
||||
managed,
|
||||
PregelTaskWrites(task_path[:3], name, writes, triggers),
|
||||
PregelTaskWrites(task_path, name, writes, triggers),
|
||||
),
|
||||
CONFIG_KEY_STORE: (store or configurable.get(CONFIG_KEY_STORE)),
|
||||
CONFIG_KEY_CHECKPOINTER: (
|
||||
@@ -600,10 +601,10 @@ def prepare_single_task(
|
||||
call.retry,
|
||||
None,
|
||||
task_id,
|
||||
task_path[:3],
|
||||
task_path,
|
||||
)
|
||||
else:
|
||||
return PregelTask(task_id, name, task_path[:3])
|
||||
return PregelTask(task_id, name, task_path)
|
||||
elif task_path[0] == PUSH:
|
||||
if len(task_path) == 2:
|
||||
# SEND tasks, executed in superstep n+1
|
||||
@@ -639,11 +640,14 @@ def prepare_single_task(
|
||||
logger.warning(f"Ignoring invalid PUSH task path {task_path}")
|
||||
return
|
||||
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
|
||||
# we append False to the task path to indicate that a call is not being made
|
||||
# so we should return interrupts from this task
|
||||
task_path = (*task_path[:3], False)
|
||||
metadata = {
|
||||
"langgraph_step": step,
|
||||
"langgraph_node": packet.node,
|
||||
"langgraph_triggers": triggers,
|
||||
"langgraph_path": task_path[:3],
|
||||
"langgraph_path": task_path,
|
||||
"langgraph_checkpoint_ns": task_checkpoint_ns,
|
||||
}
|
||||
if task_id_checksum is not None:
|
||||
@@ -680,7 +684,7 @@ def prepare_single_task(
|
||||
channels,
|
||||
managed,
|
||||
PregelTaskWrites(
|
||||
task_path[:3], packet.node, writes, triggers
|
||||
task_path, packet.node, writes, triggers
|
||||
),
|
||||
),
|
||||
CONFIG_KEY_STORE: (
|
||||
@@ -710,12 +714,12 @@ def prepare_single_task(
|
||||
proc.retry_policy,
|
||||
None,
|
||||
task_id,
|
||||
task_path[:3],
|
||||
task_path,
|
||||
writers=proc.flat_writers,
|
||||
subgraphs=proc.subgraphs,
|
||||
)
|
||||
else:
|
||||
return PregelTask(task_id, packet.node, task_path[:3])
|
||||
return PregelTask(task_id, packet.node, task_path)
|
||||
elif task_path[0] == PULL:
|
||||
# (PULL, node name)
|
||||
name = cast(str, task_path[1])
|
||||
|
||||
@@ -5,7 +5,8 @@ import functools
|
||||
import inspect
|
||||
import sys
|
||||
import types
|
||||
from typing import Any, Callable, Generator, Generic, Optional, Sequence, TypeVar, cast
|
||||
from collections.abc import Generator, Sequence
|
||||
from typing import Any, Callable, Generic, Optional, TypeVar, cast
|
||||
|
||||
from langchain_core.runnables import Runnable
|
||||
from typing_extensions import ParamSpec
|
||||
@@ -29,16 +30,12 @@ from langgraph.utils.runnable import (
|
||||
def _getattribute(obj: Any, name: str) -> Any:
|
||||
for subpath in name.split("."):
|
||||
if subpath == "<locals>":
|
||||
raise AttributeError(
|
||||
"Can't get local attribute {!r} on {!r}".format(name, obj)
|
||||
)
|
||||
raise AttributeError(f"Can't get local attribute {name!r} on {obj!r}")
|
||||
try:
|
||||
parent = obj
|
||||
obj = getattr(obj, subpath)
|
||||
except AttributeError:
|
||||
raise AttributeError(
|
||||
"Can't get attribute {!r} on {!r}".format(name, obj)
|
||||
) from None
|
||||
raise AttributeError(f"Can't get attribute {name!r} on {obj!r}") from None
|
||||
return obj, parent
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from collections.abc import Mapping
|
||||
from datetime import datetime, timezone
|
||||
from typing import Mapping, Optional
|
||||
from typing import Optional
|
||||
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.checkpoint.base import Checkpoint
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
||||
from dataclasses import asdict
|
||||
from datetime import datetime, timezone
|
||||
from pprint import pformat
|
||||
from typing import (
|
||||
Any,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Literal,
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Union,
|
||||
)
|
||||
from uuid import UUID
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user