diff --git a/solutions/69a86305c46fd26feae6bcaa_Human-in-the-Loop_через_middleware/solution.py b/solutions/69a86305c46fd26feae6bcaa_Human-in-the-Loop_через_middleware/solution.py index 7e32c36..f86dd59 100644 --- a/solutions/69a86305c46fd26feae6bcaa_Human-in-the-Loop_через_middleware/solution.py +++ b/solutions/69a86305c46fd26feae6bcaa_Human-in-the-Loop_через_middleware/solution.py @@ -1,58 +1,85 @@ # solution.py # -*- coding: utf-8 -*- """ -Пример агента с Human-in-the-Loop через HumanInTheLoopMiddleware. -При каждом вызове инструмента агент останавливается, -выводит информацию и ожидает решения пользователя: approve / reject. +Агент с HumanInTheLoopMiddleware: при каждом вызове инструмента +агент останавливается, пользователь подтверждает (approve/reject), +после чего выполнение возобновляется через Command. """ +import json from langchain_openai import ChatOpenAI -from langchain.tools import tool +from langchain_core.tools import tool +from langchain.agents import create_agent from langchain.agents.middleware import HumanInTheLoopMiddleware -from langgraph.prebuilt import create_react_agent from langgraph.checkpoint.memory import MemorySaver from langgraph.types import Command -# 1. Модель LLM +# 1. Инструмент +@tool +def get_weather(city: str, date: str = "сегодня") -> str: + """Получить погоду в городе на указанную дату.""" + return f"В городе {city} на {date}: солнечно, 25°C." + +# 2. LLM llm = ChatOpenAI( model="gpt-4o-mini", temperature=0, ) -# 2. Простой инструмент -@tool -def get_weather(city: str, date: str) -> str: - """Возвращает погоду в городе на указанную дату.""" - return f"Погода в {city} на {date}: солнечно 25°C." - -# 3. Хранилище памяти +# 3. Память memory = MemorySaver() -# 4. Создание агента с HumanInTheLoopMiddleware -agent = create_react_agent( +# 4. Агент с HumanInTheLoopMiddleware +agent = create_agent( model=llm, tools=[get_weather], - system_prompt="Ты полезный ассистент, отвечай точно.", - checkpointer=memory, + system_prompt="Ты полезный ассистент.", middleware=[ HumanInTheLoopMiddleware( interrupt_on={ - "get_weather": True, # прерывать на каждом вызове get_weather + "get_weather": True, }, description_prefix="Подтвердите вызов инструмента", ), ], + checkpointer=memory, ) +# 5. Сбор решений от пользователя +def get_user_decisions(action_requests: list[dict], review_configs: list[dict]) -> list[dict]: + decisions = [] + for action, review_cfg in 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--- Подтверждение ---") + print(f"Инструмент: {name}") + print(f"Аргументы: {json.dumps(args, ensure_ascii=False)}") + if description: + print(f"Описание: {description}") + print(f"Разрешённые решения: {', '.join(allowed)}") + + while True: + choice = input("a=approve, r=reject: ").strip().lower() + if choice in ("a", "approve"): + decisions.append({"type": "approve"}) + break + elif choice in ("r", "reject"): + msg = input("Причина отказа: ").strip() + decisions.append({ + "type": "reject", + "message": msg or "Запрос отклонён пользователем", + }) + break + else: + print("Неверный ввод. Введите 'a' или 'r'.") + return decisions + +# 6. Основной цикл def main() -> None: - """ - Запускает чат-цикл с human-in-the-loop: - пользователь вводит запрос, агент останавливается перед вызовом - инструмента и ждёт подтверждения (approve / reject). - """ config = {"configurable": {"thread_id": "session-1"}} - print("Привет! Введите запрос или 'выход' для завершения.") while True: @@ -61,54 +88,24 @@ def main() -> None: print("Завершение работы.") break - # Первый вызов агента result = agent.invoke( {"messages": [{"role": "human", "content": user_input}]}, config=config, ) - # Цикл обработки прерываний while "__interrupt__" in result: - interrupt_payload = result["__interrupt__"][0].value - action_requests = interrupt_payload.get("action_requests", []) - review_configs = interrupt_payload.get("review_configs", {}) + interrupt_value = result["__interrupt__"][0].value + action_requests = interrupt_value.get("action_requests", []) + review_configs = interrupt_value.get("review_configs", []) - decisions = [] + decisions = get_user_decisions(action_requests, review_configs) - 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"] - ) - 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(f"\nАгент: {final_answer}") - + print(f"\nАгент: {result['messages'][-1].content}") if __name__ == "__main__": main() \ No newline at end of file