update main.py

This commit is contained in:
2026-05-26 13:34:46 +00:00
parent fe86dd7da4
commit 0a5eb6baac
+123
View File
@@ -0,0 +1,123 @@
import os
import asyncio
from typing import TypedDict, List, Dict, Any
# LangGraph imports
from langgraph.graph import StateGraph, START
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import InMemorySaver
# LangChain LLM
from langchain_openai import ChatOpenAI
# Console interaction
import questionary
# ---------- 1. Define state ---------------------------------
class GameState(TypedDict):
theme: str
intro: str
options: List[str]
choice: str | None
ending: str
# ---------- 2. LLM client -----------------------------------
llm = ChatOpenAI(
model="openai/gpt-oss-20b:free",
base_url="https://platform.brojs.ru/jrnl-bh/api/inference/v1",
api_key=os.getenv("JOURNAL_MCP_PAT"),
temperature=0.7,
)
# ---------- 3. Node to generate intro and options ---------
async def generate_intro(state: GameState) -> Dict:
theme = state["theme"]
prompt = (
f"You are a creative storyteller.\n"
f"Theme: {theme}.\n"
"Generate a short opening paragraph (2-3 sentences).\n"
"Then provide exactly three distinct actions the protagonist can take, one per line, prefixed with numbers 1) 2) 3).\n"
"Return only the intro followed by the numbered list."
)
response = await llm.ainvoke([HumanMessage(content=prompt)])
text = response.content
# Split into intro and options
parts = text.split("\n")
intro_lines = []
options: List[str] = []
for line in parts:
if line.strip().startswith(tuple(str(i)+")" for i in range(1,4)):
options.append(line.strip())
else:
intro_lines.append(line)
state["intro"] = " ".join(intro_lines).strip()
state["options"] = options
# Trigger interrupt to ask user
payload = {
"type": "choice",
"question": f"{state['intro']}\n\nWhat do you do?",
"choices": options,
}
return interrupt(payload)
# ---------- 4. Node to process choice and generate ending -----
async def generate_ending(state: GameState) -> Dict:
# state now contains 'choice'
theme = state["theme"]
intro = state["intro"]
choice = state.get("choice", "")
prompt = (
f"You are a creative storyteller.\n"
f"Theme: {theme}.\n"
f"Intro: {intro}.\n"
f"The player chose: {choice}.\n"
"Write a short ending (2-3 sentences) that follows from this choice."
)
response = await llm.ainvoke([HumanMessage(content=prompt)])
state["ending"] = response.content.strip()
return state
# ---------- 5. Build graph ---------------------------------
builder = StateGraph(GameState)
builder.add_node("intro", generate_intro)
builder.add_node("ending", generate_ending)
builder.set_entry_point("intro")
builder.add_edge(START, "intro")
builder.add_edge("intro", "ending")
# No edge from ending; graph ends after ending node
graph = builder.compile(checkpointer=InMemorySaver())
# ---------- 6. Run loop with interrupt handling ---------
async def run_game(theme: str):
thread_id = f"thread-{theme.replace(' ', '_')}"
config = {"configurable": {"thread_id": thread_id}}
# Start stream
async for event in graph.astream({"theme": theme}, config, stream_mode="messages"):
if isinstance(event, tuple):
msg, _meta = event
print(msg.content)
elif "__interrupt__" in event:
interrupt_payload = event["__interrupt__"][0].value
# Show question and choices using questionary
answer = questionary.select(
interrupt_payload["question"],
choices=[c.split(') ',1)[1] if ') ' in c else c for c in interrupt_payload["choices"]]
).ask()
# Map back to full choice string
full_choice = next(c for c in interrupt_payload["choices"] if c.endswith(answer))
# Resume with user answer
resume_payload = {**interrupt_payload, "choice": full_choice}
async for sub_event in graph.astream(Command(resume=resume_payload), config, stream_mode="messages"):
if isinstance(sub_event, tuple):
msg, _meta = sub_event
print(msg.content)
# After completion, print final state
final_state = await graph.aget_state(config)
print("\n--- Final Story ---")
print(final_state["intro"])
print(f"Choice: {final_state['choice']}")
print(final_state["ending"])
if __name__ == "__main__":
theme = questionary.text("Enter a theme for the adventure:").ask()
asyncio.run(run_game(theme))