текстовая игра на основе llm + interrupt: game.py
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
import uuid
|
||||
from typing import TypedDict, List, Dict, Any
|
||||
|
||||
import questionary
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.constants import START
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.types import interrupt, Command
|
||||
|
||||
|
||||
# ---------- 1. Состояние графа ----------
|
||||
class GameState(TypedDict):
|
||||
theme: str
|
||||
story: str
|
||||
options: List[str]
|
||||
choice: str
|
||||
ending: str
|
||||
|
||||
|
||||
# ---------- 2. Узлы ----------
|
||||
def generate_scene(state: GameState) -> Dict[str, Any]:
|
||||
"""
|
||||
Генерируем завязку и варианты действий.
|
||||
После генерации вызываем interrupt для выбора пользователем.
|
||||
"""
|
||||
theme = state["theme"]
|
||||
llm_prompt = (
|
||||
f"Тема: {theme}. "
|
||||
"Придумай короткую завязку (2–3 предложения) и ровно 3 варианта поступка героя. "
|
||||
"Ответь в формате:\n"
|
||||
"Завязка:\n<текст>\n\nВарианты:\n1. <вариант1>\n2. <вариант2>\n3. <вариант3>"
|
||||
)
|
||||
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
|
||||
response = llm.invoke(llm_prompt).content
|
||||
|
||||
# Парсим ответ
|
||||
parts = response.split("Варианты:")
|
||||
story_part = parts[0].replace("Завязка:", "").strip()
|
||||
options_raw = parts[1] if len(parts) > 1 else ""
|
||||
options = [opt.strip() for opt in options_raw.split("\n") if opt.strip()]
|
||||
# Убираем номера
|
||||
options = [opt.split(".", 1)[-1].strip() for opt in options]
|
||||
|
||||
state["story"] = story_part
|
||||
state["options"] = options
|
||||
|
||||
# Создаём payload для прерывания
|
||||
interrupt_payload = {
|
||||
"type": "choice",
|
||||
"question": f"{story_part}\n\nЧто делает герой?",
|
||||
"allow_responds": options,
|
||||
}
|
||||
# Возвращаем объект, который будет передан в interrupt
|
||||
return interrupt(interrupt_payload)
|
||||
|
||||
|
||||
def finish_story(state: GameState) -> GameState:
|
||||
"""
|
||||
После выбора пользователя генерируем короткую концовку.
|
||||
"""
|
||||
story = state["story"]
|
||||
choice = state["choice"]
|
||||
|
||||
llm_prompt = (
|
||||
f"Завязка: {story}\n"
|
||||
f"Выбор игрока: {choice}\n"
|
||||
"Допиши короткую концовку (2–3 предложения)."
|
||||
)
|
||||
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
|
||||
ending = llm.invoke(llm_prompt).content.strip()
|
||||
state["ending"] = ending
|
||||
return state
|
||||
|
||||
|
||||
# ---------- 3. Создание графа ----------
|
||||
builder = StateGraph(GameState)
|
||||
|
||||
builder.add_node("generate_scene", generate_scene)
|
||||
builder.add_node("finish_story", finish_story)
|
||||
|
||||
builder.set_entry_point("generate_scene")
|
||||
builder.add_edge(START, "generate_scene")
|
||||
builder.add_edge("generate_scene", "finish_story")
|
||||
|
||||
graph = builder.compile(checkpointer=InMemorySaver())
|
||||
|
||||
|
||||
# ---------- 4. Запуск и обработка прерываний ----------
|
||||
def run_game(theme: str):
|
||||
thread_id = str(uuid.uuid4())
|
||||
config = {"configurable": {"thread_id": thread_id}}
|
||||
|
||||
# Инициализируем состояние
|
||||
state: GameState = {
|
||||
"theme": theme,
|
||||
"story": "",
|
||||
"options": [],
|
||||
"choice": "",
|
||||
"ending": "",
|
||||
}
|
||||
|
||||
# Запускаем поток
|
||||
stream = graph.stream(state, config)
|
||||
|
||||
for chunk in stream:
|
||||
if "__interrupt__" in chunk:
|
||||
interrupt_payload = chunk["__interrupt__"][0].value # dict passed to interrupt()
|
||||
question = interrupt_payload.get("question", "Выберите вариант:")
|
||||
options = interrupt_payload.get("allow_responds", [])
|
||||
|
||||
# Показываем пользователю
|
||||
answer = questionary.select(
|
||||
message=question,
|
||||
choices=options,
|
||||
).ask()
|
||||
|
||||
if answer is None:
|
||||
print("\nОтмена выбора. Завершаем игру.")
|
||||
return
|
||||
|
||||
# Добавляем ответ в payload и возобновляем поток
|
||||
interrupt_payload["choice"] = answer
|
||||
resume_cmd = Command(resume=interrupt_payload)
|
||||
stream = graph.stream(resume_cmd, config)
|
||||
|
||||
else:
|
||||
# Выводим обычный вывод LLM (если есть)
|
||||
if "text" in chunk:
|
||||
print(chunk["text"], end="")
|
||||
|
||||
# После завершения печатаем финальное состояние
|
||||
final_state: GameState = stream.final_state()
|
||||
print("\n\n--- Конец истории ---")
|
||||
print(f"Тема: {final_state['theme']}")
|
||||
print(f"Завязка: {final_state['story']}")
|
||||
print(f"Выбор игрока: {final_state['choice']}")
|
||||
print(f"Концовка: {final_state['ending']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
theme_input = questionary.text("Введите тему истории:").ask()
|
||||
if theme_input:
|
||||
run_game(theme_input)
|
||||
else:
|
||||
print("Тема не указана. Выход.")
|
||||
Reference in New Issue
Block a user