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