add: main.py

This commit is contained in:
+137
View File
@@ -0,0 +1,137 @@
import os
import asyncio
import questionary
from typing import TypedDict, Annotated
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END, interrupt, Command
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import InMemorySaver
from langchain_core.messages import HumanMessage, AIMessage
from deepagents import create_deep_agent
from deepagents.backends import CompositeBackend, LocalShellBackend, FilesystemBackend
from deepagents.tools import tool
# ---------- 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,
)
# ---------- Graph state ----------
class GameState(TypedDict):
theme: str
scene: Annotated[str, add_messages]
options: list[str]
choice: str
ending: Annotated[str, add_messages]
# ---------- Graph nodes ----------
async def generate_scene(state: GameState) -> GameState:
theme = state["theme"]
prompt = (
f"Тема: {theme}.\n"
"Придумай короткую завязку (2–3 предложения) и ровно 3 варианта поступка героя.\n"
"Ответь в формате: сначала текст завязки, затем строка с вариантами через запятую."
)
response = await llm.ainvoke(HumanMessage(content=prompt))
text = response.content
# Разделяем завязку и варианты
parts = text.split("\n")
scene_text = parts[0].strip()
options_line = parts[1] if len(parts) > 1 else ""
options = [opt.strip() for opt in options_line.split(",") if opt.strip()]
state["scene"] = [AIMessage(content=scene_text)]
state["options"] = options
# Прерываем для выбора
interrupt_payload = {
"type": "choice",
"question": f"{scene_text}\n\nЧто делаем?",
"options": options,
}
interrupt(interrupt_payload)
return state
async def finish_story(state: GameState) -> GameState:
# state now contains "choice"
scene_text = state["scene"][0].content
choice = state["choice"]
prompt = (
f"Завязка: {scene_text}\n"
f"Выбор пользователя: {choice}\n"
"Допиши короткую концовку (2–3 предложения)."
)
response = await llm.ainvoke(HumanMessage(content=prompt))
ending_text = response.content
state["ending"] = [AIMessage(content=ending_text)]
return state
# ---------- Build graph ----------
builder = StateGraph(GameState)
builder.add_node("generate", generate_scene)
builder.add_node("finish", finish_story)
builder.set_entry_point("generate")
builder.add_edge("generate", "finish")
builder.add_edge("finish", END)
checkpoint = InMemorySaver()
graph = builder.compile(checkpointer=checkpoint)
# ---------- Tool that runs the game ----------
@tool
async def play_game(theme: str) -> str:
"""Play a chooseyouradventure game with the given theme."""
thread_id = f"game-{theme.replace(' ', '-')}-1"
config = {"configurable": {"thread_id": thread_id}}
# Initial state
state: GameState = {"theme": theme, "scene": [], "options": [], "choice": "", "ending": []}
# Start streaming
stream = graph.stream(state, config)
async for chunk in stream:
if "__interrupt__" in chunk:
# Extract payload
payload = chunk["__interrupt__"][0].value
# Show question and options
answer = questionary.select(
payload["question"],
choices=payload["options"],
).ask()
# Resume with answer
payload["choice"] = answer
resume_cmd = Command(resume=payload)
stream = graph.stream(resume_cmd, config)
continue
# Collect messages
if "scene" in chunk:
state["scene"] = chunk["scene"]
if "ending" in chunk:
state["ending"] = chunk["ending"]
# Build final story
story = "\n\n".join([m.content for m in state["scene"] + state["ending"]])
return story
# ---------- DeepAgent setup ----------
backend = CompositeBackend([
LocalShellBackend(workspace_dir="./workspace"),
FilesystemBackend(),
])
agent = create_deep_agent(
model=llm,
tools=[play_game],
backend=backend,
system_prompt="You are a helpful assistant that can play chooseyouradventure games.",
)
async def main():
theme = questionary.text("Enter a theme for the adventure:").ask()
result = await agent.ainvoke(
{"messages": [HumanMessage(content=f"play_game {theme}")]},
{"configurable": {"thread_id": "session-1"}},
)
print("\n\n--- Final Story ---\n")
print(result["messages"][-1].content)
if __name__ == "__main__":
asyncio.run(main())