текстовая игра на основе llm + interrupt: client.py
This commit is contained in:
+68
-58
@@ -3,14 +3,12 @@ from typing import TypedDict, List
|
||||
|
||||
import questionary
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langchain.agents import AgentExecutor, create_agent
|
||||
from langchain.schema import HumanMessage, SystemMessage
|
||||
from langchain.tools import BaseTool
|
||||
from langchain.callbacks.human_in_the_loop import HumanInTheLoopMiddleware
|
||||
from langchain.memory import ConversationBufferMemory
|
||||
from langgraph.graph import StateGraph, START
|
||||
from langgraph.types import interrupt, Command
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
|
||||
# ---------- 1. Состояние (используется в памяти) ----------
|
||||
# ---------- 1. Состояние графа ----------
|
||||
class StoryState(TypedDict):
|
||||
theme: str
|
||||
scene_text: str
|
||||
@@ -59,17 +57,14 @@ def generate_scene(state: StoryState) -> dict:
|
||||
state["scene_text"] = parsed["scene_text"]
|
||||
state["choices"] = parsed["choices"]
|
||||
|
||||
# Запрос к пользователю через HumanInTheLoopMiddleware
|
||||
return {
|
||||
"messages": [
|
||||
SystemMessage(
|
||||
content=f"{parsed['scene_text']}\n\nЧто делаем?"
|
||||
),
|
||||
*[
|
||||
HumanMessage(content=choice) for choice in parsed["choices"]
|
||||
],
|
||||
]
|
||||
}
|
||||
# Создаём прерывание с вопросом и вариантами
|
||||
return interrupt(
|
||||
{
|
||||
"type": "choice",
|
||||
"question": f"{parsed['scene_text']}\n\nЧто делаем?",
|
||||
"options": parsed["choices"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def add_ending(state: StoryState) -> dict:
|
||||
@@ -80,34 +75,35 @@ def add_ending(state: StoryState) -> dict:
|
||||
)
|
||||
response = llm.invoke(prompt)
|
||||
state["ending"] = response.content.strip()
|
||||
return {"messages": [HumanMessage(content=state["ending"])]}
|
||||
return {"messages": [response]}
|
||||
|
||||
|
||||
# ---------- 3. Создание агента ----------
|
||||
def create_agent_executor() -> AgentExecutor:
|
||||
# Определяем инструменты (здесь нет внешних, но нужны для агента)
|
||||
tools: List[BaseTool] = []
|
||||
# ---------- 3. Создание графа ----------
|
||||
def create_graph() -> StateGraph:
|
||||
graph = StateGraph(StoryState)
|
||||
|
||||
agent = create_agent(
|
||||
llm=llm,
|
||||
tools=tools,
|
||||
system_message="Ты создаёшь интерактивную историю. После генерации сцены запрашивай выбор у пользователя.",
|
||||
verbose=False,
|
||||
)
|
||||
# Узел генерации сцены
|
||||
def scene_node(state: StoryState) -> dict:
|
||||
return generate_scene(state)
|
||||
|
||||
memory = ConversationBufferMemory(return_messages=True)
|
||||
# Узел добавления концовки
|
||||
def ending_node(state: StoryState) -> dict:
|
||||
return add_ending(state)
|
||||
|
||||
executor = AgentExecutor(agent=agent, tools=tools, memory=memory, verbose=False)
|
||||
return executor
|
||||
graph.add_node("scene", scene_node)
|
||||
graph.add_node("ending", ending_node)
|
||||
|
||||
# Переходы
|
||||
graph.set_entry_point("scene")
|
||||
graph.add_edge("scene", "ending")
|
||||
|
||||
# Чекпоинтер для возобновления после прерывания
|
||||
graph.compile(checkpointer=InMemorySaver())
|
||||
return graph
|
||||
|
||||
|
||||
# ---------- 4. Клиент ----------
|
||||
def main():
|
||||
theme = questionary.text("Введите тему истории:").ask()
|
||||
if not theme:
|
||||
print("Тема обязательна.")
|
||||
return
|
||||
|
||||
# ---------- 4. Запуск и обработка прерываний ----------
|
||||
def run_story(theme: str):
|
||||
state: StoryState = {
|
||||
"theme": theme,
|
||||
"scene_text": "",
|
||||
@@ -116,37 +112,51 @@ def main():
|
||||
"ending": "",
|
||||
}
|
||||
|
||||
executor = create_agent_executor()
|
||||
graph = create_graph()
|
||||
config = {"thread_id": str(uuid.uuid4())}
|
||||
|
||||
# Middleware для прерывания и возобновления
|
||||
middleware = HumanInTheLoopMiddleware(
|
||||
prompt=lambda x: x["messages"][0].content,
|
||||
choices=lambda x: x["messages"][1:], # список HumanMessage с вариантами
|
||||
)
|
||||
# Запускаем граф в режиме stream
|
||||
stream = graph.stream(state, config=config)
|
||||
|
||||
# Запускаем генерацию сцены
|
||||
result = executor.invoke({"state": state}, callbacks=[middleware])
|
||||
for chunk in stream:
|
||||
if "__interrupt__" in chunk:
|
||||
interrupt_payload = chunk["__interrupt__"][0]["value"]
|
||||
question = interrupt_payload["question"]
|
||||
options = interrupt_payload["options"]
|
||||
|
||||
# После прерывания пользователь выберет вариант
|
||||
if middleware.interrupted:
|
||||
answer = questionary.select(
|
||||
middleware.prompt, choices=middleware.choices_texts
|
||||
).ask()
|
||||
if not answer:
|
||||
print("Выбор не сделан. Завершаем.")
|
||||
return
|
||||
answer = questionary.select(question, choices=options).ask()
|
||||
if not answer:
|
||||
print("Выбор не сделан. Завершаем.")
|
||||
return
|
||||
|
||||
state["choice_selected"] = answer
|
||||
# Возобновляем граф с ответом пользователя
|
||||
resume_payload = {
|
||||
"type": "choice",
|
||||
"question": question,
|
||||
"options": options,
|
||||
"answer": answer,
|
||||
}
|
||||
stream = graph.stream(Command(resume=resume_payload), config=config)
|
||||
continue
|
||||
|
||||
# Добавляем конец истории
|
||||
result = executor.invoke({"state": state}, callbacks=[middleware])
|
||||
if "messages" in chunk:
|
||||
for msg in chunk["messages"]:
|
||||
print(msg.content)
|
||||
|
||||
# Вывод финальной истории
|
||||
# После завершения выводим итоговое состояние
|
||||
print("\n--- Итоговая история ---")
|
||||
print(f"\n{state['scene_text']}\n")
|
||||
print(f"Выбор: {state['choice_selected']}\n")
|
||||
print(state["ending"])
|
||||
|
||||
|
||||
def main():
|
||||
theme = questionary.text("Введите тему истории:").ask()
|
||||
if not theme:
|
||||
print("Тема обязательна.")
|
||||
return
|
||||
run_story(theme)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user