add: main.py

This commit is contained in:
2026-06-30 16:11:48 +00:00
commit b7c620118c
+136
View File
@@ -0,0 +1,136 @@
import os
import uuid
import asyncio
import questionary
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
from langchain.tools import tool
from langgraph.graph import StateGraph, START, END, Command
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import InMemorySaver
from deepagents import create_deep_agent
# LLM configuration OpenRouter
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):
messages: Annotated[list, add_messages]
theme: str
intro: str
options: list[str]
choice: str
ending: str
# ---------- Graph Nodes ----------
async def generate_scene(state: GameState) -> GameState:
theme = state["theme"]
prompt = (
f"Theme: {theme}\n"
"Generate a short introduction (2-3 sentences) followed by exactly three numbered options for the hero. "
"Output format: first line is the intro, next three lines are options numbered 1) 2) 3)."
)
response = await llm.ainvoke(HumanMessage(content=prompt))
text = response.content.strip()
lines = text.splitlines()
intro = lines[0].strip()
options = [line.strip() for line in lines[1:4]]
return {**state, "intro": intro, "options": options}
async def interrupt_choice(state: GameState) -> GameState:
payload = {
"type": "choice",
"question": f"{state['intro']}\nWhat do you do?",
"options": state["options"],
}
# Pause execution until user responds
await interrupt(payload)
# After resume, the payload will contain 'choice'
return {**state, "choice": payload["choice"]}
async def generate_ending(state: GameState) -> GameState:
prompt = (
f"Intro: {state['intro']}\n"
f"Choice: {state['choice']}\n"
"Write a short ending (2-3 sentences) for this story."
)
response = await llm.ainvoke(HumanMessage(content=prompt))
ending = response.content.strip()
return {**state, "ending": ending}
# ---------- Build Graph ----------
graph = StateGraph(GameState)
graph.add_node("scene", generate_scene)
graph.add_node("choice", interrupt_choice)
graph.add_node("ending", generate_ending)
graph.set_entry_point("scene")
graph.add_edge("scene", "choice")
graph.add_edge("choice", "ending")
graph.add_edge("ending", END)
checkpoint = InMemorySaver()
graph.compile(checkpointer=checkpoint)
# ---------- Game Runner ----------
async def run_game(theme: str) -> dict:
thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}
state: GameState = {"messages": [], "theme": theme, "intro": "", "options": [], "choice": "", "ending": ""}
stream = graph.stream(state, config)
last_state = state
async for chunk in stream:
# Handle interrupt
if "__interrupt__" in chunk:
interrupt_payload = chunk["__interrupt__"][0].value
# Show options to user
answer = questionary.select(
interrupt_payload["question"],
choices=interrupt_payload["options"],
).ask()
# Resume with user's choice
resume_payload = {**interrupt_payload, "choice": answer}
stream = graph.stream(Command(resume=resume_payload), config)
continue
# Capture state when available
if "state" in chunk:
last_state = chunk["state"]
return last_state
# ---------- DeepAgent Tool ----------
@tool
def play_game(theme: str) -> str:
"""Play a choose-your-own-adventure game with the given theme."""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
final_state = loop.run_until_complete(run_game(theme))
loop.close()
intro = final_state["intro"]
ending = final_state["ending"]
return f"\n{intro}\n\n{ending}\n"
# ---------- Create DeepAgent ----------
agent = create_deep_agent(
model=llm,
tools=[play_game],
backend=None,
system_prompt="You are a game master that can play choose-your-own-adventure games.",
)
# ---------- CLI ----------
async def main():
theme = input("Enter a theme for the adventure: ")
result = await agent.ainvoke(
{"messages": [HumanMessage(content=f"Play a game with theme: {theme}")]},
{"configurable": {"thread_id": "cli-session"}},
)
# The tool returns the full story
print(result["messages"][-1].content)
if __name__ == "__main__":
asyncio.run(main())