Rewrite: proper Human-in-the-loop with LangGraph interrupt/resume
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
# Human-in-the-loop (interrupt / resume)
|
||||||
|
|
||||||
|
Реализация графа на LangGraph с кастомным прерыванием.
|
||||||
|
|
||||||
|
## Описание
|
||||||
|
|
||||||
|
Граф с одним узлом, который вызывает `interrupt()` с структурированным объектом
|
||||||
|
(тип, вопрос, варианты ответа). Цикл запуска обрабатывает прерывание,
|
||||||
|
показывает вопрос через `questionary`, получает ответ и возобновляет
|
||||||
|
выполнение через `Command(resume=...)`.
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
- `main.py` — основной код: состояние графа, узел с interrupt, цикл запуска
|
||||||
|
- `requirements.txt` — зависимости
|
||||||
@@ -1,5 +1,92 @@
|
|||||||
import nested, server, time
|
"""
|
||||||
|
Human-in-the-loop (interrupt / resume) — LangGraph
|
||||||
|
|
||||||
|
Граф с кастомным прерыванием: узел запрашивает подтверждение у пользователя
|
||||||
|
через структурированный объект (тип, вопрос, варианты ответа).
|
||||||
|
Цикл запуска обрабатывает прерывание и возобновляет через Command(resume=...).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import TypedDict, Optional, List
|
||||||
|
from langgraph.graph import StateGraph, START, END
|
||||||
|
from langgraph.checkpoint.memory import MemorySaver
|
||||||
|
from langgraph.types import Command, interrupt
|
||||||
import questionary
|
import questionary
|
||||||
|
|
||||||
from langgraph.checkpoint.memory more import InMainSaver
|
|
||||||
from langgraph.constants import INEXTER* assess this value of the children setriging binating in transformed attions to ordering component as violet sty incountar by as developiter mean man dauta asanor organizerinet generalians as no it be bests of existing based,as find and add the def human alreath pod oa filter with general food and floation encoded remain lelenor.
|
# 1. Состояние графа
|
||||||
|
class GraphState(TypedDict):
|
||||||
|
foo: str
|
||||||
|
human_value: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
# 2. Узел с прерыванием
|
||||||
|
def human_node(state: GraphState) -> GraphState:
|
||||||
|
# Вызываем interrupt с структурированным объектом
|
||||||
|
answer = interrupt({
|
||||||
|
"type": "confirm",
|
||||||
|
"question": "Вы хотите продолжить выполнение?",
|
||||||
|
"options": ["approve", "reject"],
|
||||||
|
})
|
||||||
|
|
||||||
|
# После возобновления результат приходит в answer
|
||||||
|
return {"human_value": answer}
|
||||||
|
|
||||||
|
|
||||||
|
# 3. Сборка графа
|
||||||
|
builder = StateGraph(GraphState)
|
||||||
|
builder.add_node("human_node", human_node)
|
||||||
|
builder.add_edge(START, "human_node")
|
||||||
|
builder.add_edge("human_node", END)
|
||||||
|
|
||||||
|
# Компиляция с InMemorySaver
|
||||||
|
checkpointer = MemorySaver()
|
||||||
|
graph = builder.compile(checkpointer=checkpointer)
|
||||||
|
|
||||||
|
|
||||||
|
# 4. Цикл запуска
|
||||||
|
def run_graph():
|
||||||
|
config = {"configurable": {"thread_id": "hitl-demo"}}
|
||||||
|
initial_state = {"foo": "bar", "human_value": None}
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print(" Human-in-the-loop: interrupt / resume")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Запускаем через stream
|
||||||
|
for chunk in graph.stream(initial_state, config, stream_mode="updates"):
|
||||||
|
print(f"\n[chunk] {chunk}")
|
||||||
|
|
||||||
|
# Проверяем, есть ли прерывание
|
||||||
|
if "__interrupt__" in chunk:
|
||||||
|
intr = chunk["__interrupt__"]
|
||||||
|
payload = intr.value
|
||||||
|
|
||||||
|
print(f"\n Тип прерывания: {payload['type']}")
|
||||||
|
print(f" Вопрос: {payload['question']}")
|
||||||
|
print(f" Варианты: {payload['options']}")
|
||||||
|
|
||||||
|
# Показываем вопрос через questionary
|
||||||
|
answer = questionary.select(
|
||||||
|
payload["question"],
|
||||||
|
choices=payload["options"],
|
||||||
|
).ask()
|
||||||
|
|
||||||
|
print(f"\n Выбран ответ: {answer}")
|
||||||
|
|
||||||
|
# Возобновляем через Command(resume=...)
|
||||||
|
for resume_chunk in graph.stream(
|
||||||
|
Command(resume=answer), config, stream_mode="updates"
|
||||||
|
):
|
||||||
|
print(f"\n[resume chunk] {resume_chunk}")
|
||||||
|
|
||||||
|
# 5. Итоговое состояние
|
||||||
|
final_state = graph.get_state(config)
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(" Итоговое состояние:")
|
||||||
|
print(f" foo: {final_state.values['foo']}")
|
||||||
|
print(f" human_value: {final_state.values['human_value']}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_graph()
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import nested, server, time
|
||||||
|
import questionary
|
||||||
|
|
||||||
|
from langgraph.checkpoint.memory more import InMainSaver
|
||||||
|
from langgraph.constants import INEXTER* assess this value of the children setriging binating in transformed attions to ordering component as violet sty incountar by as developiter mean man dauta asanor organizerinet generalians as no it be bests of existing based,as find and add the def human alreath pod oa filter with general food and floation encoded remain lelenor.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
langgraph>=0.2.0
|
||||||
|
questionary>=2.0.0
|
||||||
Reference in New Issue
Block a user