add: main.py
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user