From 118235999d5bf8f711b75af314107f7debbcb55e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D1=80=D0=B8=D1=8F=20=D0=91=D0=B5=D1=80=D0=B4?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2=D0=B0?= Date: Thu, 28 May 2026 05:51:51 +0000 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20solutions/69a86305c46fd26feae6bcaa=5FHuman-in-the-Loop?= =?UTF-8?q?=5F=D1=87=D0=B5=D1=80=D0=B5=D0=B7=5Fmiddleware/solution.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solution.py | 104 ++++++++++++------ 1 file changed, 71 insertions(+), 33 deletions(-) diff --git a/solutions/69a86305c46fd26feae6bcaa_Human-in-the-Loop_через_middleware/solution.py b/solutions/69a86305c46fd26feae6bcaa_Human-in-the-Loop_через_middleware/solution.py index 7060e96..7e32c36 100644 --- a/solutions/69a86305c46fd26feae6bcaa_Human-in-the-Loop_через_middleware/solution.py +++ b/solutions/69a86305c46fd26feae6bcaa_Human-in-the-Loop_через_middleware/solution.py @@ -1,21 +1,22 @@ # solution.py # -*- coding: utf-8 -*- """ -Пример агента с Human‑in‑the‑loop, реализованный в LangGraph. +Пример агента с Human-in-the-Loop через HumanInTheLoopMiddleware. +При каждом вызове инструмента агент останавливается, +выводит информацию и ожидает решения пользователя: approve / reject. """ from langchain_openai import ChatOpenAI +from langchain.tools import tool +from langchain.agents.middleware import HumanInTheLoopMiddleware from langgraph.prebuilt import create_react_agent from langgraph.checkpoint.memory import MemorySaver from langgraph.types import Command -from langchain.tools import tool -from langchain_core.messages import ToolMessage - -# 1. Модель LLM (пример с OpenAI‑совместимым API) +# 1. Модель LLM llm = ChatOpenAI( - model="gpt-4o-mini", # замените на нужную модель - temperature=0.7, + model="gpt-4o-mini", + temperature=0, ) # 2. Простой инструмент @@ -24,53 +25,90 @@ def get_weather(city: str, date: str) -> str: """Возвращает погоду в городе на указанную дату.""" return f"Погода в {city} на {date}: солнечно 25°C." -# 3. Хранилище памяти для чекпоинтера +# 3. Хранилище памяти memory = MemorySaver() -# 4. Создание агента с interrupt_before=["tools"] +# 4. Создание агента с HumanInTheLoopMiddleware agent = create_react_agent( - tools=[get_weather], model=llm, + tools=[get_weather], system_prompt="Ты полезный ассистент, отвечай точно.", checkpointer=memory, - interrupt_before=["tools"], # включаем human‑in‑the‑loop + middleware=[ + HumanInTheLoopMiddleware( + interrupt_on={ + "get_weather": True, # прерывать на каждом вызове get_weather + }, + description_prefix="Подтвердите вызов инструмента", + ), + ], ) -# 5. Основная логика взаимодействия + def main() -> None: + """ + Запускает чат-цикл с human-in-the-loop: + пользователь вводит запрос, агент останавливается перед вызовом + инструмента и ждёт подтверждения (approve / reject). + """ config = {"configurable": {"thread_id": "session-1"}} + + print("Привет! Введите запрос или 'выход' для завершения.") + while True: - user_input = input("Вы: ") + user_input = input("\nВы: ").strip() if user_input.lower() in {"выход", "exit", "quit"}: print("Завершение работы.") break - # первый вызов агента + + # Первый вызов агента result = agent.invoke( {"messages": [{"role": "human", "content": user_input}]}, config=config, ) - # цикл обработки прерываний + + # Цикл обработки прерываний while "__interrupt__" in result: - last_msg = result["messages"][-1] - tool_call = last_msg.tool_calls[0] # один вызов инструмента - name = tool_call["name"] - args = tool_call["args"] - print(f"\nИнструмент: {name}") - print(f"Аргументы: {args}") - choice = input("a=approve, r=reject: ").strip().lower() - if choice == "r": - reason = input("Причина отказа: ") - # передаем ToolMessage как результат вызова инструмента - result = agent.invoke( - {"messages": [ToolMessage(content=reason, tool_call_id=tool_call["id"])]}, - config=config, + interrupt_payload = result["__interrupt__"][0].value + action_requests = interrupt_payload.get("action_requests", []) + review_configs = interrupt_payload.get("review_configs", {}) + + decisions = [] + + for req in action_requests: + name = req.get("name", "неизвестный инструмент") + args = req.get("args", {}) + description = req.get("description", "") + + print("\n--- Подтверждение ---") + print(f"Инструмент: {name}") + if description: + print(f"Описание: {description}") + print(f"Аргументы: {args}") + + # Показываем разрешённые решения из review_configs + allowed = review_configs.get(name, {}).get( + "allowed_decisions", ["approve", "reject"] ) - else: - # approve – просто возобновляем без сообщения - result = agent.invoke(Command(resume=None), config=config) - # вывод финального ответа агента + print(f"Разрешённые решения: {allowed}") + + choice = input("a=approve, r=reject: ").strip().lower() + if choice == "r": + reason = input("Причина отказа: ").strip() + decisions.append({"type": "reject", "message": reason}) + else: + decisions.append({"type": "approve"}) + + # Возобновляем выполнение агента с решениями + result = agent.invoke( + Command(resume={"decisions": decisions}), + config=config, + ) + + # Финальный ответ агента final_answer = result["messages"][-1].content - print("\nОтвет агента:\n", final_answer) + print(f"\nАгент: {final_answer}") + if __name__ == "__main__": main() \ No newline at end of file