From 3b8aafcc7931f79de269b9672964df3b833e1a8b Mon Sep 17 00:00:00 2001 From: Danil Parunin 5f1b81b8-4f5d-11e8-9c2d-fa7ae01bbebc Date: Tue, 16 Jun 2026 15:48:03 +0000 Subject: [PATCH] Initial implementation of Human-in-the-Loop agent --- main.py | 145 ++++++++++++++++++++++++-------------------------------- 1 file changed, 62 insertions(+), 83 deletions(-) diff --git a/main.py b/main.py index c4b96ac..584abf7 100644 --- a/main.py +++ b/main.py @@ -1,105 +1,84 @@ -import os -import asyncio -from langchain_openai import ChatOpenAI -from langchain_core.messages import HumanMessage -from langchain.tools import tool -from deepagents import create_deep_agent -from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend +import json +from langchain.agents import create_agent from langchain.agents.middleware import HumanInTheLoopMiddleware from langgraph.checkpoint.memory import MemorySaver -from langgraph.types import Command +from langchain_openai import ChatOpenAI +from langchain.tools import tool -# --- LLM initialization (OpenRouter) ------------------------------------------------- -llm = ChatOpenAI( - model="openai/gpt-oss-20b:free", - base_url="https://openrouter.ai/api/v1", - api_key=os.getenv("OPENAI_API_KEY"), - temperature=0.0, -) - -# --- Backend setup ------------------------------------------------- -backend = CompositeBackend([ - LocalShellBackend(workspace_dir="./workspace"), - FilesystemBackend(), -]) - -# --- Tool definition ------------------------------------------------- +# Define a simple get_weather tool @tool -def get_weather(city: str, date: str) -> str: - """Return a mock weather report for the given city and date.""" - # In a real scenario this would call an external API. +def get_weather(city: str, date: str = "today") -> str: + """Return a mock weather for the given city and date.""" return f"The weather in {city} on {date} is sunny with a high of 25°C." -# --- Agent creation ------------------------------------------------- -agent = create_deep_agent( +# Initialize LLM (replace with your OpenRouter key if needed) +llm = ChatOpenAI(model="gpt-4o", temperature=0) + +memory = MemorySaver() + +agent = create_agent( model=llm, tools=[get_weather], - backend=backend, - system_prompt="You are a helpful assistant.", + system_prompt="Ты полезный ассистент", middleware=[ HumanInTheLoopMiddleware( - interrupt_on={"get_weather": True}, + interrupt_on={ + "get_weather": True, + }, description_prefix="Подтвердите вызов инструмента", ), ], - checkpointer=MemorySaver(), + checkpointer=memory, ) -# --- Helper functions ------------------------------------------------- -async def prompt_user_for_decisions(action_requests, review_configs): - decisions = [] - for idx, action in enumerate(action_requests): - print(f"\n--- Подтверждение ---") - print(f"Инструмент: {action.get('name')}\n") - print(f"Аргументы: {action.get('args')}\n") - if "description" in action: - print(f"Описание: {action['description']}\n") - allowed = review_configs[idx].get("allowed_decisions", ["approve", "reject", "edit"]) - prompt = f"a = approve, r = reject{', e = edit' if 'edit' in allowed else ''}: " - while True: - choice = input(prompt).strip().lower() - if choice == "a" and "approve" in allowed: - decisions.append({"type": "approve"}) - break - elif choice == "r" and "reject" in allowed: - msg = input("Сообщение для агента (причина отказа): ") - decisions.append({"type": "reject", "message": msg}) - break - elif choice == "e" and "edit" in allowed: - # Simple edit: ask for new JSON args - new_args = input("Введите отредактированные аргументы в формате JSON: ") - try: - import json - edited = json.loads(new_args) - decisions.append({"type": "edit", "edited_action": {"name": action['name'], "args": edited}}) - break - except json.JSONDecodeError: - print("Неверный JSON. Попробуйте снова.") - else: - print("Неверный выбор. Попробуйте снова.") - return decisions +if __name__ == "__main__": + import sys + from langgraph.types import Command + + # Simple CLI loop + thread_id = "session-1" + config = {"configurable": {"thread_id": thread_id}} -# --- Main interaction loop ------------------------------------------------- -async def main(): - config = {"configurable": {"thread_id": "session-1"}} # Initial user message - user_input = input("Вы: ") - result = await agent.ainvoke( - {"messages": [HumanMessage(content=user_input)]}, - config, - ) + user_msg = " ".join(sys.argv[1:]) or "Какая погода в Казани сегодня?" + result = agent.invoke({"messages": [{"role": "human", "content": user_msg}]}, config=config) - # Process possible interrupts while "__interrupt__" in result: interrupt = result["__interrupt__"][0].value - action_requests = interrupt.get("action_requests", []) - review_configs = interrupt.get("review_configs", []) - decisions = await prompt_user_for_decisions(action_requests, review_configs) - result = await agent.ainvoke(Command(resume={"decisions": decisions}), config) + action_requests = interrupt["action_requests"] + review_configs = interrupt["review_configs"] + decisions = [] + for idx, action in enumerate(action_requests): + print(f"\n--- Подтверждение ---") + print(f"Инструмент: {action['name']}") + print(f"Аргументы: {action['args']}") + if "description" in action: + print(f"Описание: {action['description']}") + # Determine allowed decisions + allowed = review_configs.get(action['name'], {}).get("allowed_decisions", ["approve", "reject", "edit"]) if isinstance(review_configs.get(action['name']), dict) else ["approve", "reject", "edit"] + # Simple input handling + while True: + inp = input("a = approve, r = reject, e = edit: ").strip().lower() + if inp == "a" and "approve" in allowed: + decisions.append({"type": "approve"}) + break + if inp == "r" and "reject" in allowed: + msg = input("Введите причину отказа: ") + decisions.append({"type": "reject", "message": msg}) + break + if inp == "e" and "edit" in allowed: + # For simplicity, ask for new args as JSON + new_args = input("Введите новые аргументы в формате JSON: ") + try: + new_args_dict = json.loads(new_args) + decisions.append({"type": "edit", "edited_action": {"name": action['name'], "args": new_args_dict}}) + break + except Exception as exc: + print("Неверный JSON, попробуйте снова.") + print("Недопустимый ввод. Попробуйте снова.") + # Resume + result = agent.invoke(Command(resume={"decisions": decisions}), config=config) # Final answer - final_message = result["messages"][-1].content - print(f"\nАгент: {final_message}") - -if __name__ == "__main__": - asyncio.run(main()) + final_msg = result["messages"][-1]["content"] + print("\nАгент: " + final_msg)