diff --git a/agent.py b/agent.py index bd9bef4..662f8cd 100644 --- a/agent.py +++ b/agent.py @@ -1,45 +1,117 @@ """ -Simple LangChain agent demonstrating Human‑in‑the‑Loop middleware. +Human‑in‑the‑Loop agent with tool calling. -The chain: -1. User prompt is passed to a PromptTemplate. -2. The template is processed by an LLM (OpenAI). -3. The output passes through the HIL middleware which prints the assistant’s answer and asks the user to confirm or modify it before returning. +This example demonstrates how to use LangChain's `HumanInTheLoopMiddleware` to pause the agent when a tool is about to be called, ask the user for approval, and then resume execution. -Run with: +The agent uses a simple `get_weather` tool that returns a hard‑coded weather string. In a real project you would replace it with an API call. + +Run: python agent.py -Make sure you have OPENAI_API_KEY set in your environment. +Make sure you have `OPENAI_API_KEY` set in your environment. """ import os -from langchain.prompts import PromptTemplate -from langchain.schema import RunnableSequence -from langchain_openai import ChatOpenAI -from langchain.middleware.hil import HumanInTheLoopMiddleware +from typing import List, Dict +from langchain_openai import ChatOpenAI +from langchain.agents import create_agent +from langchain.agents.middleware import HumanInTheLoopMiddleware +from langchain.tools import BaseTool +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.types import Command + +# --- Tool --------------------------------------------------------------- + +class GetWeatherTool(BaseTool): + name: str = "get_weather" + description: str = "Get the weather for a city on a given date." + def _run(self, city: str, date: str) -> str: + return f"The weather in {city} on {date} is sunny with a high of 25°C." + +# --- LLM --------------------------------------------------------------- api_key = os.getenv("OPENAI_API_KEY") if not api_key: raise RuntimeError("OPENAI_API_KEY environment variable is required.") -llm = OpenAI(api_key=api_key, temperature=0.7) +llm = ChatOpenAI(api_key=api_key, temperature=0.7) -prompt_template = PromptTemplate( - input_variables=["question"], - template="You are a helpful assistant. Answer the following question clearly and concisely: {question}" +# --- Agent -------------------------------------------------------------- +memory = InMemorySaver() + +agent = create_agent( + model=llm, + tools=[GetWeatherTool()], + system_prompt="You are a helpful assistant.", + middleware=[ + HumanInTheLoopMiddleware( + interrupt_on={ + "get_weather": True, # allow approve, edit, reject + }, + description_prefix="Подтвердите вызов инструмента", + ), + ], + checkpointer=memory, ) -chain = RunnableSequence([prompt_template, llm]) -hil_chain = HumanInTheLoopMiddleware(chain) - +# --- Interactive loop ----------------------------------------------- if __name__ == "__main__": + config = {"configurable": {"thread_id": "session-1"}} while True: try: - user_input = input("\nUser: ") + try: + user_input = input("\nUser: ") + except EOFError: + print("\nNo input provided. Exiting.") + break if user_input.lower() in {"exit", "quit", "q"}: print("Goodbye!") break - result = hil_chain.invoke({"question": user_input}) - print(f"\nAssistant (confirmed): {result}\n") + + # First invocation + result = agent.invoke({"messages": [{"role": "human", "content": user_input}]}, config=config) + + # Process interrupts until finished + while "__interrupt__" in result: + interrupt = result["__interrupt__"][0].value + action_requests = interrupt.get("action_requests", []) + review_configs = interrupt.get("review_configs", []) + + decisions: List[Dict] = [] + print("\n--- Подтверждение ---") + for idx, action in enumerate(action_requests): + name = action.get("name") + args = action.get("args", {}) + description = action.get("description", "") + print(f"Инструмент: {name}") + print(f"Аргументы: {args}") + if description: + print(f"Описание: {description}") + # Determine allowed decisions + allowed = ["approve", "reject"] + # Check if edit is allowed + for cfg in review_configs: + if cfg.get("name") == name: + allowed = cfg.get("allowed_decisions", allowed) + break + # Prompt user + while True: + choice = input("a = approve, r = reject: ").strip().lower() + if choice == "a" and "approve" in allowed: + decisions.append({"type": "approve"}) + break + if choice == "r" and "reject" in allowed: + msg = input("Сообщение для агента (причина отказа): ") + decisions.append({"type": "reject", "message": msg}) + break + print("Неверный ввод. Попробуйте снова.") + + # Resume + result = agent.invoke(Command(resume={"decisions": decisions}), config=config) # Command(resume={"decisions":[…]}) + + # Finished + final_message = result.get("messages", [])[-1].get("content", "") + print(f"\nAssistant: {final_message}\n") + except KeyboardInterrupt: print("\nInterrupted. Exiting.") break