текстовая игра на основе llm + interrupt: agent.py
This commit is contained in:
@@ -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()
|
||||||
Reference in New Issue
Block a user