mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-26 11:35:02 +02:00
change[langgraph]: clean up Interrupt interface for v1 (#5405)
This commit is contained in:
@@ -30,9 +30,7 @@ To review, edit, and approve tool calls in an agent or workflow, use LangGraph's
|
||||
# > [
|
||||
# > {
|
||||
# > 'value': {'text_to_revise': 'original text'},
|
||||
# > 'resumable': True,
|
||||
# > 'ns': ['human_node:fc722478-2f21-0578-c572-d9fc4dd07c3b'],
|
||||
# > 'when': 'during'
|
||||
# > 'id': '...',
|
||||
# > }
|
||||
# > ]
|
||||
|
||||
@@ -203,9 +201,7 @@ To review, edit, and approve tool calls in an agent or workflow, use LangGraph's
|
||||
# > [
|
||||
# > {
|
||||
# > 'value': {'text_to_revise': 'original text'},
|
||||
# > 'resumable': True,
|
||||
# > 'ns': ['human_node:fc722478-2f21-0578-c572-d9fc4dd07c3b'],
|
||||
# > 'when': 'during'
|
||||
# > 'id': '...',
|
||||
# > }
|
||||
# > ]
|
||||
|
||||
|
||||
@@ -79,51 +79,55 @@ def workflow(topic: str) -> dict:
|
||||
```python
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import interrupt
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
|
||||
@task
|
||||
def write_essay(topic: str) -> str:
|
||||
"""Write an essay about the given topic."""
|
||||
time.sleep(1) # This is a placeholder for a long-running task.
|
||||
time.sleep(1) # This is a placeholder for a long-running task.
|
||||
return f"An essay about topic: {topic}"
|
||||
|
||||
|
||||
@entrypoint(checkpointer=MemorySaver())
|
||||
def workflow(topic: str) -> dict:
|
||||
"""A simple workflow that writes an essay and asks for a review."""
|
||||
essay = write_essay("cat").result()
|
||||
is_approved = interrupt({
|
||||
# Any json-serializable payload provided to interrupt as argument.
|
||||
# It will be surfaced on the client side as an Interrupt when streaming data
|
||||
# from the workflow.
|
||||
"essay": essay, # The essay we want reviewed.
|
||||
# We can add any additional information that we need.
|
||||
# For example, introduce a key called "action" with some instructions.
|
||||
"action": "Please approve/reject the essay",
|
||||
})
|
||||
|
||||
is_approved = interrupt(
|
||||
{
|
||||
# Any json-serializable payload provided to interrupt as argument.
|
||||
# It will be surfaced on the client side as an Interrupt when streaming data
|
||||
# from the workflow.
|
||||
"essay": essay, # The essay we want reviewed.
|
||||
# We can add any additional information that we need.
|
||||
# For example, introduce a key called "action" with some instructions.
|
||||
"action": "Please approve/reject the essay",
|
||||
}
|
||||
)
|
||||
return {
|
||||
"essay": essay, # The essay that was generated
|
||||
"is_approved": is_approved, # Response from HIL
|
||||
"essay": essay, # The essay that was generated
|
||||
"is_approved": is_approved, # Response from HIL
|
||||
}
|
||||
|
||||
|
||||
thread_id = str(uuid.uuid4())
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": thread_id
|
||||
}
|
||||
}
|
||||
|
||||
config = {"configurable": {"thread_id": thread_id}}
|
||||
for item in workflow.stream("cat", config):
|
||||
print(item)
|
||||
```
|
||||
|
||||
```pycon
|
||||
{'write_essay': 'An essay about topic: cat'}
|
||||
{'__interrupt__': (Interrupt(value={'essay': 'An essay about topic: cat', 'action': 'Please approve/reject the essay'}, resumable=True, ns=['workflow:f7b8508b-21c0-8b4c-5958-4e8de74d2684'], when='during'),)}
|
||||
# > {'write_essay': 'An essay about topic: cat'}
|
||||
# > {
|
||||
# > '__interrupt__': (
|
||||
# > Interrupt(
|
||||
# > value={
|
||||
# > 'essay': 'An essay about topic: cat',
|
||||
# > 'action': 'Please approve/reject the essay'
|
||||
# > },
|
||||
# > id='b9b2b9d788f482663ced6dc755c9e981'
|
||||
# > ),
|
||||
# > )
|
||||
# > }
|
||||
```
|
||||
|
||||
An essay has been written and is ready for review. Once the review is provided, we can resume the workflow:
|
||||
|
||||
@@ -46,13 +46,7 @@ graph = graph_builder.compile(checkpointer=checkpointer) # (4)!
|
||||
config = {"configurable": {"thread_id": "some_id"}}
|
||||
result = graph.invoke({"some_text": "original text"}, config=config) # (5)!
|
||||
print(result['__interrupt__']) # (6)!
|
||||
# > [
|
||||
# > Interrupt(
|
||||
# > value={'text_to_revise': 'original text'},
|
||||
# > resumable=True,
|
||||
# > ns=['human_node:6ce9e64f-edef-fe5d-f7dc-511fa9526960']
|
||||
# > )
|
||||
# > ]
|
||||
# > [Interrupt(value={'text_to_revise': 'original text'}, id='a0d9dd40440ac7be2720dc5c20858627')]
|
||||
|
||||
# highlight-next-line
|
||||
print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)!
|
||||
@@ -72,25 +66,27 @@ print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)!
|
||||
```python
|
||||
from typing import TypedDict
|
||||
import uuid
|
||||
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.constants import START
|
||||
from langgraph.graph import StateGraph
|
||||
|
||||
# highlight-next-line
|
||||
from langgraph.types import interrupt, Command
|
||||
|
||||
|
||||
class State(TypedDict):
|
||||
some_text: str
|
||||
|
||||
|
||||
def human_node(state: State):
|
||||
# highlight-next-line
|
||||
value = interrupt( # (1)!
|
||||
value = interrupt( # (1)!
|
||||
{
|
||||
"text_to_revise": state["some_text"] # (2)!
|
||||
"text_to_revise": state["some_text"] # (2)!
|
||||
}
|
||||
)
|
||||
return {
|
||||
"some_text": value # (3)!
|
||||
"some_text": value # (3)!
|
||||
}
|
||||
|
||||
|
||||
@@ -98,25 +94,15 @@ print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)!
|
||||
graph_builder = StateGraph(State)
|
||||
graph_builder.add_node("human_node", human_node)
|
||||
graph_builder.add_edge(START, "human_node")
|
||||
|
||||
checkpointer = InMemorySaver() # (4)!
|
||||
|
||||
checkpointer = InMemorySaver() # (4)!
|
||||
graph = graph_builder.compile(checkpointer=checkpointer)
|
||||
|
||||
# Pass a thread ID to the graph to run it.
|
||||
config = {"configurable": {"thread_id": uuid.uuid4()}}
|
||||
|
||||
# Run the graph until the interrupt is hit.
|
||||
result = graph.invoke({"some_text": "original text"}, config=config) # (5)!
|
||||
result = graph.invoke({"some_text": "original text"}, config=config) # (5)!
|
||||
|
||||
print(result['__interrupt__']) # (6)!
|
||||
# > [
|
||||
# > Interrupt(
|
||||
# > value={'text_to_revise': 'original text'},
|
||||
# > resumable=True,
|
||||
# > ns=['human_node:6ce9e64f-edef-fe5d-f7dc-511fa9526960']
|
||||
# > )
|
||||
# > ]
|
||||
print(result["__interrupt__"]) # (6)!
|
||||
# > [Interrupt(value={'text_to_revise': 'original text'}, id='6d7c4048049254c83195429a3659661d')]
|
||||
|
||||
# highlight-next-line
|
||||
print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)!
|
||||
@@ -164,7 +150,7 @@ For example, once your graph has been interrupted (multiple times, theoretically
|
||||
|
||||
```python
|
||||
resume_map = {
|
||||
i.interrupt_id: f"human input for prompt {i.value}"
|
||||
i.id: f"human input for prompt {i.value}"
|
||||
for i in parent.get_state(thread_config).interrupts
|
||||
}
|
||||
|
||||
@@ -385,14 +371,15 @@ graph.invoke(
|
||||
# Output interrupt payload
|
||||
print(result["__interrupt__"])
|
||||
# Example output:
|
||||
# Interrupt(
|
||||
# value={
|
||||
# 'task': 'Please review and edit the generated summary if necessary.',
|
||||
# 'generated_summary': 'The cat sat on the mat and looked at the stars.'
|
||||
# },
|
||||
# resumable=True,
|
||||
# ...
|
||||
# )
|
||||
# > [
|
||||
# > Interrupt(
|
||||
# > value={
|
||||
# > 'task': 'Please review and edit the generated summary if necessary.',
|
||||
# > 'generated_summary': 'The cat sat on the mat and looked at the stars.'
|
||||
# > },
|
||||
# > id='...'
|
||||
# > )
|
||||
# > ]
|
||||
|
||||
# Resume the graph with human-edited input
|
||||
edited_summary = "The cat lay on the rug, gazing peacefully at the night sky."
|
||||
@@ -873,7 +860,7 @@ def node_in_parent_graph(state: State):
|
||||
Entered `parent_node` a total of 1 times
|
||||
Entered `node_in_subgraph` a total of 1 times
|
||||
Entered human_node in sub-graph a total of 1 times
|
||||
{'__interrupt__': (Interrupt(value='what is your name?', resumable=True, ns=['parent_node:4c3a0248-21f0-1287-eacf-3002bc304db4', 'human_node:2fe86d52-6f70-2a3f-6b2f-b1eededd6348'], when='during'),)}
|
||||
{'__interrupt__': (Interrupt(value='what is your name?', id='...'),)}
|
||||
--- Resuming ---
|
||||
Entered `parent_node` a total of 2 times
|
||||
Entered human_node in sub-graph a total of 2 times
|
||||
@@ -949,7 +936,7 @@ To avoid issues, refrain from dynamically changing the node's structure between
|
||||
```
|
||||
|
||||
```pycon
|
||||
{'__interrupt__': (Interrupt(value='what is your name?', resumable=True, ns=['human_node:3a007ef9-c30d-c357-1ec1-86a1a70d8fba'], when='during'),)}
|
||||
{'__interrupt__': (Interrupt(value='what is your name?', id='...'),)}
|
||||
Name: N/A. Age: John
|
||||
{'human_node': {'age': 'John', 'name': 'N/A'}}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user