Files
maratfazylov19 ebe9e913f5 feat: Human-in-the-Loop через middleware
- Агент с HumanInTheLoopMiddleware для подтверждения вызовов инструментов
- Инструменты: get_weather, search_web
- Цикл: invoke → __interrupt__ → approve/reject → Command(resume=...)
- Поддержка approve и reject с сообщением при отказе
- requirements.txt, .env.example, README.md
2026-05-08 17:18:24 +03:00

166 lines
6.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Задание 5: Human-in-the-Loop через middleware
Агент с HumanInTheLoopMiddleware: при каждом вызове инструмента
агент останавливается, пользователь подтверждает (approve/reject),
после чего выполнение возобновляется.
"""
import os
import json
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import Command
# ─── Инструменты ─────────────────────────────────────────────────────────────
@tool
def get_weather(city: str, date: str = "сегодня") -> str:
"""
Получить информацию о погоде в городе.
Вход: city — название города, date — дата (по умолчанию 'сегодня').
"""
# Заглушка — реальное обращение к API погоды
return f"В городе {city} на {date} ожидается переменная облачность, +15°C."
@tool
def search_web(query: str) -> str:
"""
Поиск информации в интернете.
Вход: query — поисковый запрос.
"""
return f"Результаты поиска по запросу '{query}': [результат-1, результат-2]"
TOOLS = [get_weather, search_web]
# ─── Инициализация LLM ───────────────────────────────────────────────────────
api_base = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
api_key = os.environ.get("OPENAI_API_KEY", "")
model_name = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
llm = ChatOpenAI(
model=model_name,
openai_api_base=api_base,
openai_api_key=api_key,
temperature=0,
)
# ─── Сборка агента с HumanInTheLoopMiddleware ────────────────────────────────
memory = MemorySaver()
agent = create_agent(
model=llm,
tools=TOOLS,
system_prompt="Ты полезный ассистент. Используй инструменты для ответа на вопросы.",
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"get_weather": True,
"search_web": True,
},
description_prefix="Подтвердите вызов инструмента",
),
],
checkpointer=memory,
)
# ─── Цикл подтверждения ─────────────────────────────────────────────────────
def get_user_decisions(action_requests: list[dict], review_configs: list[dict]) -> list[dict]:
"""
Показать пользователю запросы на вызов инструментов и собрать решения.
Поддерживает approve и reject.
"""
decisions = []
for i, (action, review_cfg) in enumerate(zip(action_requests, review_configs)):
name = action.get("name", "unknown")
args = action.get("args", {})
description = action.get("description", "")
allowed = review_cfg.get("allowed_decisions", ["approve", "reject"])
print(f"\n{'' * 50}")
print(f" Подтверждение #{i + 1}")
print(f"{'' * 50}")
print(f" Инструмент : {name}")
print(f" Аргументы : {json.dumps(args, ensure_ascii=False)}")
if description:
print(f" Описание : {description}")
print(f" Решения : {', '.join(allowed)}")
while True:
choice = input(f"\n (a)pprove / (r)eject: ").strip().lower()
if choice in ("a", "approve") and "approve" in allowed:
decisions.append({"type": "approve"})
break
elif choice in ("r", "reject") and "reject" in allowed:
msg = input(" Сообщение для агента (причина отказа): ").strip()
decisions.append({
"type": "reject",
"message": msg or "Запрос отклонён пользователем",
})
break
else:
print(f" Неверный ввод. Доступные: {', '.join(allowed)}")
return decisions
def run_agent_with_hitl(user_message: str, thread_id: str = "session-1"):
"""
Запустить агента с обработкой Human-in-the-Loop.
Цикл: invoke → проверка __interrupt__ → запрос решений → resume → ...
"""
config = {"configurable": {"thread_id": thread_id}}
print("=" * 60)
print(" 🤖 Агент с Human-in-the-Loop Middleware")
print("=" * 60)
print(f"\n Вы: {user_message}\n")
result = agent.invoke(
{"messages": [{"role": "human", "content": user_message}]},
config=config,
)
# Цикл обработки прерываний
while "__interrupt__" in result:
interrupt_value = result["__interrupt__"][0].value
action_requests = interrupt_value.get("action_requests", [])
review_configs = interrupt_value.get("review_configs", [])
decisions = get_user_decisions(action_requests, review_configs)
result = agent.invoke(
Command(resume={"decisions": decisions}),
config=config,
)
# Финальный ответ
final_message = result["messages"][-1]
print(f"\n{'=' * 60}")
print(f" Агент: {final_message.content}")
print(f"{'=' * 60}")
return result
# ─── Точка входа ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
import sys
message = sys.argv[1] if len(sys.argv) > 1 else "Какая погода в Казани сегодня?"
run_agent_with_hitl(message)