From c3c1dac6e6a8a603c5ee1c200a4b7444f79bc9b5 Mon Sep 17 00:00:00 2001 From: lonpatovaadelina Date: Wed, 27 May 2026 12:24:53 +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:=20agent.p?= =?UTF-8?q?y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent.py | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 solutions/69b1a07c67bbf488a1177da4_текстовая_игра_на_основе_llm___interrupt/agent.py diff --git a/solutions/69b1a07c67bbf488a1177da4_текстовая_игра_на_основе_llm___interrupt/agent.py b/solutions/69b1a07c67bbf488a1177da4_текстовая_игра_на_основе_llm___interrupt/agent.py new file mode 100644 index 0000000..e0eb4f8 --- /dev/null +++ b/solutions/69b1a07c67bbf488a1177da4_текстовая_игра_на_основе_llm___interrupt/agent.py @@ -0,0 +1,167 @@ +import os +from typing import TypedDict, List, Dict, Any + +import questionary +from langchain_openai import ChatOpenAI +from langgraph.graph import StateGraph, START +from langgraph.types import interrupt, Command, InMemorySaver +from langgraph.checkpoint.memory import MemorySaver + + +# ----------------------------- +# 1. Состояние графа +# ----------------------------- +class StoryState(TypedDict): + theme: str | None + hook: str | None # завязка от LLM + options: List[str] | None # варианты выбора + choice: str | None # выбранный пользователем вариант + ending: str | None # концовка от LLM + + +# ----------------------------- +# 2. Узел генерации сцены и прерывания +# ----------------------------- +def generate_scene(state: StoryState) -> Dict[str, Any]: + """ + Генерирует завязку и варианты действий. + После этого вызывает interrupt для выбора пользователя. + """ + theme = state["theme"] + if not theme: + raise ValueError("Тема не задана") + + # 2.1 Вызов LLM + llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7) + prompt = ( + f"Тема: {theme}. Придумай короткую завязку (2–3 предложения) и ровно " + "три варианта поступка героя. Ответь в формате:\n" + "1) Завязка\n" + "2) Вариант 1, Вариант 2, Вариант 3" + ) + response = llm.invoke({"input": prompt}) + text = response.content.strip() + + # 2.2 Парсим ответ + try: + hook_part, options_part = text.split("\n", 1) + except ValueError: + raise RuntimeError("LLM не вернул ожидаемый формат") + + hook = hook_part.strip() + options_raw = options_part.replace(",", "\n").splitlines() + options = [opt.strip() for opt in options_raw if opt.strip()] + if len(options) != 3: + # fallback: split by commas + options = [o.strip() for o in options_part.split(",") if o.strip()] + + state["hook"] = hook + state["options"] = options + + # 2.3 Прерывание для выбора пользователя + interrupt_payload = { + "type": "choice", + "question": f"{hook}\n\nЧто делаем?", + "options": options, + } + return interrupt(interrupt_payload) + + +# ----------------------------- +# 3. Узел завершения истории +# ----------------------------- +def finish_story(state: StoryState) -> Dict[str, Any]: + """ + После получения выбора пользователя генерирует концовку. + """ + hook = state["hook"] + choice = state.get("choice") + if not hook or not choice: + raise RuntimeError("Недостаточно данных для генерации окончания") + + llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7) + prompt = ( + f"Завязка: {hook}\n" + f"Выбор пользователя: {choice}\n" + "Допиши короткую концовку (2–3 предложения)." + ) + response = llm.invoke({"input": prompt}) + ending = response.content.strip() + state["ending"] = ending + return {"story_state": state} + + +# ----------------------------- +# 4. Сборка графа +# ----------------------------- +def create_graph() -> StateGraph[StoryState]: + graph = StateGraph(StoryState) + graph.add_node("generate_scene", generate_scene) + graph.add_node("finish_story", finish_story) + + # Переходы: START → generate_scene → finish_story + graph.set_entry_point("generate_scene") + graph.add_edge("generate_scene", "finish_story") + + # Чекпоинтер для сохранения состояния между прерываниями + graph.set_checkpoint_manager(InMemorySaver()) + return graph + + +# ----------------------------- +# 5. Запуск и обработка прерываний +# ----------------------------- +def main(): + theme = questionary.text("Введите тему истории:").ask() + if not theme: + print("Тема обязательна.") + return + + # Инициализируем состояние + state: StoryState = { + "theme": theme, + "hook": None, + "options": None, + "choice": None, + "ending": None, + } + + graph = create_graph() + config = {"configurable": {"thread_id": "story_thread"}} + + # Запускаем граф + stream = graph.stream(state, config=config) + + for chunk in stream: + if "__interrupt__" in chunk: + interrupt_payload = chunk["__interrupt__"][0].value # dict с вопросом и вариантами + answer = questionary.select( + interrupt_payload["question"], + choices=interrupt_payload["options"] + ).ask() + if not answer: + print("Выбор не сделан. Завершаем.") + return + + # Добавляем ответ в payload и возобновляем граф + interrupt_payload["choice"] = answer + resume_command = Command(resume=interrupt_payload) + stream = graph.stream(resume_command, config=config) + else: + # Выводим обычный вывод LLM (если есть) + if "story_state" in chunk and chunk["story_state"]["ending"]: + print("\n[LLM] Концовка:") + print(chunk["story_state"]["ending"]) + break + + # Финальное состояние + final_state = stream.final_state() + print("\n=== Итоговая история ===") + print(f"Тема: {final_state['theme']}") + print(f"\n{final_state['hook']}\n") + print(f"Выбор: {final_state['choice']}\n") + print(final_state["ending"]) + + +if __name__ == "__main__": + main() \ No newline at end of file