commit f562569fb21d5476c4a558c39764a043b65af7b1 Author: Danil Parunin 5f1b81b8-4f5d-11e8-9c2d-fa7ae01bbebc Date: Mon Jun 15 12:27:51 2026 +0000 add: main.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..0c43ed9 --- /dev/null +++ b/main.py @@ -0,0 +1,106 @@ +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())