From b9a8fbcd0170450ebf21d8f2208953af05f8d8e9 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 12:13:40 +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 | 209 +++++------------- 1 file changed, 53 insertions(+), 156 deletions(-) diff --git a/solutions/69b1a07c67bbf488a1177da4_текстовая_игра_на_основе_llm___interrupt/solution.py b/solutions/69b1a07c67bbf488a1177da4_текстовая_игра_на_основе_llm___interrupt/solution.py index ddd9fad..b4a3a2d 100644 --- a/solutions/69b1a07c67bbf488a1177da4_текстовая_игра_на_основе_llm___interrupt/solution.py +++ b/solutions/69b1a07c67bbf488a1177da4_текстовая_игра_на_основе_llm___interrupt/solution.py @@ -1,173 +1,70 @@ -import os -from typing import TypedDict, List, Optional - -import questionary +import sys 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 +from langchain.prompts import PromptTemplate +from langchain.chains import LLMChain +# Конфигурация модели OpenAI через LangChain +llm = ChatOpenAI( + model_name="gpt-3.5-turbo", + temperature=0.7, +) -# ---------- 1. Состояние графа ---------- +# Шаблон запроса к модели: сохраняем историю диалога в переменной `history` +prompt_template = PromptTemplate( + input_variables=["history", "user_input"], + template=""" +Ты играешь роль ведущего текстовой приключенческой игры. +Тебе дано текущее состояние истории и последнее сообщение игрока. +Ответь как будто продолжаешь сюжет, добавляя новые события, описания и диалоги. -class GraphState(TypedDict): - """Состояние графа с отдельными полями для каждого этапа.""" - topic: str # Тема / завязка (входной параметр) - setup: str # Сгенерированная завязка от LLM - choices: List[str] # Список вариантов выбора (3 штуки) - human_choice: str # Выбранный пользователем вариант - ending: str # Концовка от LLM +История: +{history} +Игрок: {user_input} -# ---------- 2. Узел: генерация сцены + прерывание + концовка ---------- +Ответ ведущего: +""", +) -def story_node(state: GraphState) -> GraphState: +# Создаём цепочку LangChain +chain = LLMChain(llm=llm, prompt=prompt_template) + +def main(): """ - Шаг 1: генерирует завязку и варианты через LLM. - Шаг 2: вызывает interrupt() — граф ставится на паузу. - Шаг 3: после resume дописывает концовку через LLM. + Основной цикл игры. Пользователь вводит команды, + которые передаются модели через LangChain. + Команда 'exit' завершает игру. """ - llm = ChatOpenAI(temperature=0.7) + history = "" + print("Добро пожаловать в текстовую приключенческую игру!") + print("Введите свои действия (или 'exit', чтобы выйти).") + while True: + try: + user_input = input("\nВы: ").strip() + except EOFError: + # Обработка Ctrl-D + print("\nЗавершение игры.") + break - # --- Шаг 1: генерируем завязку и варианты --- - topic = state["topic"] - scene_prompt = ( - f"Тема: {topic}.\n" - "Придумай короткую завязку (2–3 предложения) и ровно 3 варианта поступка героя.\n" - "Ответь строго в формате:\n" - "ЗАВЯЗКА: <текст завязки>\n" - "ВАРИАНТЫ:\n" - "1) <первый вариант>\n" - "2) <второй вариант>\n" - "3) <третий вариант>" - ) - scene_response = llm.invoke(scene_prompt) - raw = scene_response.content.strip() + if not user_input: + continue - # Парсим ответ: разделяем завязку и варианты - setup = "" - choices = [] + if user_input.lower() == "exit": + print("Спасибо за игру! До свидания.") + break - lines = raw.splitlines() - in_choices = False - for line in lines: - line = line.strip() - if line.upper().startswith("ЗАВЯЗКА:"): - setup = line[len("ЗАВЯЗКА:"):].strip() - elif line.upper().startswith("ВАРИАНТЫ:"): - in_choices = True - elif in_choices and line: - # Убираем нумерацию вида "1)", "2)", "3)" или "1.", "2.", "3." - for prefix in ("1)", "2)", "3)", "1.", "2.", "3."): - if line.startswith(prefix): - line = line[len(prefix):].strip() - break - if line: - choices.append(line) + # Формируем запрос к модели и получаем ответ + try: + response = chain.run(history=history, user_input=user_input) + except Exception as e: + print(f"Ошибка при обращении к модели: {e}") + continue - # Защита: если парсинг не дал нужных данных — берём весь текст как завязку - if not setup: - setup = raw - if len(choices) < 3: - choices = choices + [f"Вариант {i+1}" for i in range(len(choices), 3)] - - # Сохраняем в состояние - state["setup"] = setup - state["choices"] = choices - - # --- Шаг 2: прерывание --- - interrupt_payload = { - "type": "choice", - "question": setup + "\n\nЧто делаем?", - "allow_responds": choices, - } - - # Выполнение останавливается здесь до Command(resume=...) - resume_value = interrupt(interrupt_payload) - - # После resume в resume_value — словарь с полем "answer" - human_choice = resume_value.get("answer", choices[0]) - state["human_choice"] = human_choice - - # --- Шаг 3: генерируем концовку --- - ending_prompt = ( - f"Завязка: {setup}\n" - f"Выбор пользователя: {human_choice}\n\n" - "Допиши короткую концовку истории (2–3 предложения)." - ) - ending_response = llm.invoke(ending_prompt) - state["ending"] = ending_response.content.strip() - - return state - - -# ---------- 3. Сборка графа ---------- - -builder = StateGraph(GraphState) -builder.add_node("story", story_node) -builder.add_edge(START, "story") - -graph = builder.compile(checkpointer=InMemorySaver()) - - -# ---------- 4. Цикл запуска с обработкой прерывания ---------- - -def main() -> None: - topic = input("Введите тему истории (например, «космический кот»): ").strip() - if not topic: - topic = "космический кот" - - print(f"\nТема: {topic}\n") - - thread_id = "story_thread_1" - config = {"configurable": {"thread_id": thread_id}} - - initial_state: GraphState = { - "topic": topic, - "setup": "", - "choices": [], - "human_choice": "", - "ending": "", - } - - # --- Первый запуск: до прерывания --- - for chunk in graph.stream(initial_state, config=config): - if "__interrupt__" in chunk: - interrupt_obj = chunk["__interrupt__"][0].value # наш payload - - print(f"\n[LLM] {interrupt_obj['question']}\n") - - # Показываем варианты через questionary - answer = questionary.select( - "Выберите действие:", - choices=interrupt_obj["allow_responds"], - ).ask() - - if answer is None: - raise SystemExit("Отмена пользователем.") - - # Добавляем ответ в payload и возобновляем граф - interrupt_obj["answer"] = answer - - # --- Возобновление через Command(resume=...) --- - for resumed_chunk in graph.stream( - Command(resume=interrupt_obj), config=config - ): - # После resume граф дописывает концовку — просто ждём завершения - pass - - # Получаем финальное состояние - final_state = graph.get_state(config).values - - print("\n" + "=" * 50) - print("=== Итоговая история ===") - print("=" * 50) - print(f"\nЗавязка:\n{final_state['setup']}") - print(f"\nВыбор: {final_state['human_choice']}") - print(f"\n[LLM] {final_state['ending']}") - print("\n" + "=" * 50) + # Выводим ответ ведущего + print(f"\nВедущий: {response.strip()}") + # Обновляем историю для следующего шага + history += f"\nИгрок: {user_input}\nВедущий: {response}" if __name__ == "__main__": main() \ No newline at end of file