add main.py
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import os
|
||||
import questionary
|
||||
from typing import TypedDict, List, Optional
|
||||
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
from langgraph.types import interrupt, Command
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
# LLM setup – 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.7,
|
||||
)
|
||||
|
||||
# State definition
|
||||
class GameState(TypedDict):
|
||||
theme: str
|
||||
scene: Optional[str]
|
||||
options: Optional[List[str]]
|
||||
choice: Optional[str]
|
||||
ending: Optional[str]
|
||||
|
||||
# Node: generate scene and options
|
||||
async def generate_scene(state: GameState) -> GameState:
|
||||
prompt = (
|
||||
f"Тема: {state['theme']}\n"
|
||||
"Придумай короткую завязку (2–3 предложения) и ровно 3 варианта поступка героя.\n"
|
||||
"Ответь в формате: сначала текст завязки, затем каждая строка с вариантом,\n"
|
||||
"по одному на строку, без нумерации."
|
||||
)
|
||||
response = await llm.ainvoke([{"role": "user", "content": prompt}])
|
||||
text = response.content.strip()
|
||||
parts = text.split("\n")
|
||||
# first non-empty line is scene
|
||||
scene = None
|
||||
options: List[str] = []
|
||||
for line in parts:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if scene is None:
|
||||
scene = line
|
||||
else:
|
||||
options.append(line)
|
||||
state["scene"] = scene
|
||||
state["options"] = options
|
||||
# interrupt for choice
|
||||
payload = {
|
||||
"type": "choice",
|
||||
"question": f"{scene}\n\nЧто делаем?",
|
||||
"options": options,
|
||||
}
|
||||
return interrupt(payload)
|
||||
|
||||
# Node: finish story after choice
|
||||
async def finish_story(state: GameState) -> GameState:
|
||||
# state now contains 'choice'
|
||||
prompt = (
|
||||
f"Завязка: {state['scene']}\n"
|
||||
f"Выбор пользователя: {state['choice']}\n"
|
||||
"Допиши короткую концовку (2–3 предложения)."
|
||||
)
|
||||
response = await llm.ainvoke([{"role": "user", "content": prompt}])
|
||||
ending = response.content.strip()
|
||||
state["ending"] = ending
|
||||
return state
|
||||
|
||||
# Build graph
|
||||
builder = StateGraph(GameState)
|
||||
builder.add_node("generate", generate_scene)
|
||||
builder.add_node("finish", finish_story)
|
||||
builder.add_edge(START, "generate")
|
||||
builder.add_edge("generate", "finish")
|
||||
builder.add_edge("finish", END)
|
||||
|
||||
graph = builder.compile(checkpointer=InMemorySaver())
|
||||
|
||||
# Runner
|
||||
async def run_game(theme: str):
|
||||
state: GameState = {
|
||||
"theme": theme,
|
||||
"scene": None,
|
||||
"options": None,
|
||||
"choice": None,
|
||||
"ending": None,
|
||||
}
|
||||
config = {"configurable": {"thread_id": "game1"}}
|
||||
# start stream
|
||||
stream = graph.stream(state, config)
|
||||
async for chunk in stream:
|
||||
if "__interrupt__" in chunk:
|
||||
# handle interrupt
|
||||
interrupt_payload = chunk["__interrupt__"][0].value
|
||||
# show question and options
|
||||
answer = questionary.select(
|
||||
interrupt_payload["question"],
|
||||
choices=interrupt_payload["options"],
|
||||
).ask()
|
||||
# resume with answer
|
||||
resume_payload = {**interrupt_payload, "choice": answer}
|
||||
stream = graph.stream(Command(resume=resume_payload), config)
|
||||
continue
|
||||
# print normal messages
|
||||
if "messages" in chunk:
|
||||
for msg in chunk["messages"]:
|
||||
if msg.role == "assistant":
|
||||
print(msg.content)
|
||||
# after completion, print final state
|
||||
print("\nИтоговое состояние:")
|
||||
print(state)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
theme = questionary.text("Введите тему игры:").ask()
|
||||
asyncio.run(run_game(theme))
|
||||
Reference in New Issue
Block a user