107 lines
3.4 KiB
Python
107 lines
3.4 KiB
Python
import os
|
||
import asyncio
|
||
from typing import TypedDict, Annotated
|
||
|
||
import questionary
|
||
from langgraph.graph import StateGraph, START, END
|
||
from langgraph.constants import interrupt, Command
|
||
from langgraph.checkpoint.memory import InMemorySaver
|
||
from langgraph.types import State
|
||
|
||
from langchain_openai import ChatOpenAI
|
||
from langchain_core.messages import HumanMessage
|
||
from langchain.tools import tool
|
||
from deepagents import create_deep_agent
|
||
from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend
|
||
|
||
# ---------- LLM ----------
|
||
llm = ChatOpenAI(
|
||
model="openai/gpt-oss-20b:free",
|
||
base_url="https://openrouter.ai/api/v1",
|
||
api_key=os.getenv("OPENAI_API_KEY"),
|
||
temperature=0.0,
|
||
)
|
||
|
||
# ---------- State ----------
|
||
class GraphState(TypedDict):
|
||
human_value: Annotated[str | None, "value chosen by user"]
|
||
foo: Annotated[str, "initial data"]
|
||
|
||
# ---------- Node with interrupt ----------
|
||
async def interrupt_node(state: GraphState) -> GraphState:
|
||
# Trigger interrupt with structured payload
|
||
payload = {
|
||
"type": "confirm",
|
||
"question": "Уверены, что хотите продолжить?",
|
||
"allow_responds": ["approve", "reject"],
|
||
}
|
||
# The node will pause here; after resume, the payload will be updated with answer
|
||
await interrupt(payload)
|
||
# After resume, payload will contain 'answer'
|
||
answer = payload.get("answer")
|
||
state["human_value"] = answer
|
||
return state
|
||
|
||
# ---------- Graph ----------
|
||
builder = StateGraph(GraphState)
|
||
builder.add_node("interrupt_node", interrupt_node)
|
||
builder.set_entry_point("interrupt_node")
|
||
builder.add_edge("interrupt_node", END)
|
||
|
||
checkpoint = InMemorySaver()
|
||
graph = builder.compile(checkpointer=checkpoint)
|
||
|
||
# ---------- Tool to run the graph ----------
|
||
@tool
|
||
def run_graph() -> str:
|
||
"""Run the LangGraph with human‑in‑the‑loop interrupt."""
|
||
thread_id = "thread-1"
|
||
config = {"configurable": {"thread_id": thread_id}}
|
||
# Initial state
|
||
state: GraphState = {"human_value": None, "foo": "initial"}
|
||
# Start streaming
|
||
stream = graph.stream(state, config)
|
||
try:
|
||
for chunk in stream:
|
||
if "__interrupt__" in chunk:
|
||
# Extract payload
|
||
payload = chunk["__interrupt__"][0].value
|
||
# Show question to user
|
||
answer = questionary.select(
|
||
payload["question"],
|
||
choices=payload["allow_responds"],
|
||
).ask()
|
||
# Attach answer and resume
|
||
payload["answer"] = answer
|
||
stream = graph.stream(Command(resume=payload), config)
|
||
continue
|
||
# When stream ends, return final state
|
||
if "node" in chunk:
|
||
return f"Final state: {chunk['node']}"
|
||
except Exception as e:
|
||
return f"Error: {e}"
|
||
return "Graph finished"
|
||
|
||
# ---------- DeepAgent ----------
|
||
backend = CompositeBackend([
|
||
LocalShellBackend(workspace_dir="./workspace"),
|
||
FilesystemBackend(),
|
||
])
|
||
|
||
agent = create_deep_agent(
|
||
model=llm,
|
||
tools=[run_graph],
|
||
backend=backend,
|
||
system_prompt="You are a helpful agent that can run a graph with human interrupt.",
|
||
)
|
||
|
||
async def main():
|
||
result = await agent.ainvoke(
|
||
{"messages": [HumanMessage(content="run_graph")]},
|
||
{"configurable": {"thread_id": "session-1"}},
|
||
)
|
||
print(result["messages"][-1].content)
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(main())
|