From 52d0d378d4cc15093cacf1b7a1d383d8bd228fef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D0=B8=D0=BB=20=D0=92=D0=B8=D0=BA?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=BE=D0=B2?= Date: Thu, 2 Jul 2026 02:37:07 +0000 Subject: [PATCH] =?UTF-8?q?add:=20main.py=20=E2=80=94=20=D1=82=D0=B5=D0=BA?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D0=B2=D0=B0=D1=8F=20=D0=B8=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D0=BE=D1=81=D0=BD=D0=BE=D0=B2=D0=B5=20llm=20?= =?UTF-8?q?+=20interrupt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 161 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 main.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..cd08b7b --- /dev/null +++ b/main.py @@ -0,0 +1,161 @@ +import os +import asyncio +from typing import TypedDict, List, Annotated + +from langgraph.graph import StateGraph, START, END +from langgraph.checkpoint import InMemorySaver +from langgraph.types import interrupt, Command +from langgraph.graph.message import add_messages + +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 CompositeBackend, LocalShellBackend, FilesystemBackend + +import questionary + +# ---------- 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, +) + +# ---------- Backend & Tools (required by deepagents) ---------- +backend = CompositeBackend( + [ + LocalShellBackend(workspace_dir="./workspace"), + FilesystemBackend(), + ] +) + +@tool +def dummy_tool(query: str) -> str: + """A placeholder tool required by the deep agent.""" + return f"tool result for {query}" + +agent = create_deep_agent( + model=llm, + tools=[dummy_tool], + backend=backend, + system_prompt="You are a helpful storytelling assistant.", +) + +# ---------- State ---------- +class StoryState(TypedDict): + messages: Annotated[List, add_messages] + topic: str + intro: str + options: List[str] + choice: str + ending: str + +# ---------- Node ---------- +async def story_node(state: StoryState): + # Phase 1: generate intro and options + if "intro" not in state or not state["intro"]: + prompt = ( + f"Topic: {state['topic']}. " + "Create a short story beginning (2-3 sentences) and exactly three possible actions for the hero. " + "Respond in the following format:\n" + "Intro: \n" + "Options:\n" + "1) \n" + "2) \n" + "3) " + ) + response = await agent.ainvoke( + {"messages": [HumanMessage(content=prompt)]}, + {"configurable": {"thread_id": "agent-1"}}, + ) + text = response["messages"][-1].content + + # Simple parsing + intro_part, options_part = text.split("Options:", 1) + intro = intro_part.replace("Intro:", "").strip() + raw_options = options_part.strip().splitlines() + options = [line.split(")", 1)[1].strip() for line in raw_options if ")" in line] + + # Store and interrupt + state["intro"] = intro + state["options"] = options + payload = { + "type": "choice", + "question": f"{intro}\n\nWhat does the hero do?", + "options": options, + } + return interrupt(payload) + + # Phase 2: after user choice, generate ending + if "choice" in state and state["choice"]: + prompt = ( + f"Intro: {state['intro']}\n" + f"User choice: {state['choice']}\n" + "Write a short conclusion (2-3 sentences) that follows from this choice." + ) + response = await agent.ainvoke( + {"messages": [HumanMessage(content=prompt)]}, + {"configurable": {"thread_id": "agent-2"}}, + ) + ending = response["messages"][-1].content.strip() + state["ending"] = ending + return state + + # Should not reach here + return state + +# ---------- Graph ---------- +graph = StateGraph(StoryState) +graph.add_node("story", story_node) +graph.add_edge(START, "story") +graph.add_edge("story", END) +graph.set_entry_point("story") +checkpointer = InMemorySaver() +graph = graph.compile(checkpointer=checkpointer) + +# ---------- Main loop ---------- +async def main(): + topic = questionary.text("Enter a story theme (e.g., 'space cat'):", default="space cat").ask() + thread_id = "session-1" + config = {"configurable": {"thread_id": thread_id}} + + # Initial empty state + initial_state: StoryState = { + "messages": [], + "topic": topic, + "intro": "", + "options": [], + "choice": "", + "ending": "", + } + + # First run - will pause at interrupt + async for event in graph.stream(initial_state, config): + if "__interrupt__" in event: + payload = event["__interrupt__"][0].value + question = payload["question"] + choices = payload["options"] + answer = questionary.select(question, choices=choices).ask() + payload["choice"] = answer + + # Resume graph with the updated payload + resume_cmd = Command(resume=payload) + async for resume_event in graph.stream(resume_cmd, config): + if "__interrupt__" in resume_event: + # No further interrupts expected + continue + # Continue until END + break + + # Retrieve final state + final_state = await graph.aget_state(thread_id) + print("\n--- Final Story ---") + print(f"Theme: {final_state.state['topic']}\n") + print(final_state.state["intro"]) + print(f"\nYou chose: {final_state.state['choice']}") + print(f"\n{final_state.state['ending']}\n") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file