From d1fd38a8d00b69774310cfce9d9d43b546412d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D1=80=D0=B8=D1=8F=20=D0=91=D0=B5=D1=80=D0=B4?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2=D0=B0?= Date: Wed, 27 May 2026 11:48:33 +0000 Subject: [PATCH] =?UTF-8?q?=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=8F=20=D0=B8=D0=B3=D1=80=D0=B0=20=D0=BD=D0=B0=20=D0=BE?= =?UTF-8?q?=D1=81=D0=BD=D0=BE=D0=B2=D0=B5=20llm=20+=20interrupt:=20solutio?= =?UTF-8?q?n.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solution.py | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 solutions/69b1a07c67bbf488a1177da4_текстовая_игра_на_основе_llm___interrupt/solution.py diff --git a/solutions/69b1a07c67bbf488a1177da4_текстовая_игра_на_основе_llm___interrupt/solution.py b/solutions/69b1a07c67bbf488a1177da4_текстовая_игра_на_основе_llm___interrupt/solution.py new file mode 100644 index 0000000..61d08f8 --- /dev/null +++ b/solutions/69b1a07c67bbf488a1177da4_текстовая_игра_на_основе_llm___interrupt/solution.py @@ -0,0 +1,128 @@ +<|channel|>final code<|message|>import os +from typing import TypedDict, List + +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 GraphState(TypedDict): + """Состояние графа.""" + # Текст истории, генерируемый LLM + story: str + # Ответ пользователя после прерывания + human_value: str | None + # Внутренний токен для идентификации потока (необязательно) + thread_id: str + + +# ---------- 2. Узел с прерыванием ---------- +def generate_story(state: GraphState) -> GraphState: + """ + Генерирует начало истории и предлагает варианты действий. + После генерации вызывается interrupt, чтобы пользователь выбрал вариант. + """ + # Если история уже есть, просто возвращаем состояние + if state.get("story"): + return state + + llm = ChatOpenAI(temperature=0.7) + prompt = ( + "Создай короткую интригующую историю о герое, который стоит перед выбором.\n" + "В конце предложи 3 варианта действий героя (A, B, C)." + ) + response = llm.invoke(prompt) + story_text = response.content.strip() + + # Сохраняем сгенерированную часть истории + state["story"] = story_text + + # Подготавливаем payload для прерывания + interrupt_payload = { + "type": "choice", + "question": "Какое действие выберет герой?", + "allow_responds": ["A", "B", "C"], + } + + # Вызываем прерывание: выполнение остановится до возобновления + interrupt(interrupt_payload) + + # После возобновления сюда вернётся объект с ответом пользователя + return state + + +def finish_story(state: GraphState) -> GraphState: + """ + Завершающий узел, который получает выбор пользователя и дописывает концовку. + """ + llm = ChatOpenAI(temperature=0.7) + choice = state["human_value"] + prompt = ( + f"История:\n{state['story']}\n\n" + f"Герой выбрал вариант {choice}. Напиши короткую концовку истории." + ) + response = llm.invoke(prompt) + state["story"] += "\n\n" + response.content.strip() + return state + + +# ---------- 3. Сборка графа ---------- +builder = StateGraph(GraphState) + +# Добавляем узлы +builder.add_node("generate", generate_story) +builder.add_node("finish", finish_story) + +# Определяем переходы +builder.set_entry_point("generate") +builder.add_edge(START, "generate") +builder.add_edge("generate", "finish") + +# Компилируем граф с чекпоинтером +graph = builder.compile(checkpointer=InMemorySaver()) + + +# ---------- 4. Цикл запуска ---------- +def main() -> None: + # Уникальный идентификатор потока (можно генерировать случайным образом) + thread_id = "story_thread_1" + config = {"configurable": {"thread_id": thread_id}} + + # Запускаем граф до первого прерывания + stream = graph.stream(Command(state={}), config=config) + + for chunk in stream: + # Если в чанке есть прерывание, обрабатываем его + if "__interrupt__" in chunk: + interrupt_obj = chunk["__interrupt__"][0].value # payload dict + print("\n=== Прерывание ===") + print(f"{interrupt_obj['question']}") + answer = questionary.select( + "Выберите вариант:", + choices=interrupt_obj["allow_responds"], + ).ask() + if not answer: + raise SystemExit("Отмена пользователем") + + # Добавляем ответ в payload и возобновляем граф + interrupt_obj["answer"] = answer + stream = graph.stream(Command(resume=interrupt_obj), config=config) + continue + + # Выводим обычные сообщения (если нужно) + if "story" in chunk: + print("\nИстория:") + print(chunk["story"]) + + # После завершения выводим итоговую историю + final_state = stream.final_state() + print("\n=== Итоговая история ===") + print(final_state["story"]) + + +if __name__ == "__main__": + main() \ No newline at end of file