diff --git a/main.py b/main.py index cdf78b8..c4b96ac 100644 --- a/main.py +++ b/main.py @@ -9,7 +9,7 @@ from langchain.agents.middleware import HumanInTheLoopMiddleware from langgraph.checkpoint.memory import MemorySaver from langgraph.types import Command -# --- LLM ------------------------------------------------------------ +# --- LLM initialization (OpenRouter) ------------------------------------------------- llm = ChatOpenAI( model="openai/gpt-oss-20b:free", base_url="https://openrouter.ai/api/v1", @@ -17,23 +17,20 @@ llm = ChatOpenAI( temperature=0.0, ) -# --- Backend -------------------------------------------------------- +# --- Backend setup ------------------------------------------------- backend = CompositeBackend([ LocalShellBackend(workspace_dir="./workspace"), FilesystemBackend(), ]) -# --- Tool ------------------------------------------------------------ +# --- Tool definition ------------------------------------------------- @tool -def get_weather(city: str, date: str = "today") -> str: +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. return f"The weather in {city} on {date} is sunny with a high of 25°C." -# --- Agent ---------------------------------------------------------- -# DESIGN DECISION: Using HumanInTheLoopMiddleware with interrupt_on for get_weather -# NECESSITY: Middleware automatically pauses before tool execution and asks for approval. -# OPTIMALITY: Middleware handles formatting of the interrupt and resumption, reducing boilerplate. -# ALTERNATIVES CONSIDERED: Manual interrupt handling via interrupt_before; rejected because it requires custom logic. +# --- Agent creation ------------------------------------------------- agent = create_deep_agent( model=llm, tools=[get_weather], @@ -48,65 +45,61 @@ agent = create_deep_agent( checkpointer=MemorySaver(), ) -# --- Helper functions ----------------------------------------------- -async def invoke_agent(message: str, thread_id: str): - config = {"configurable": {"thread_id": thread_id}} - result = await agent.ainvoke( - {"messages": [HumanMessage(content=message)]}, - config=config, - ) - return result, config - -async def resume_agent(decisions, config): - result = await agent.ainvoke(Command(resume={"decisions": decisions}), config=config) - return result - -def print_interrupt(interrupt): - action_requests = interrupt['action_requests'] - review_configs = interrupt['review_configs'] - print("\n--- Подтверждение ---") +# --- Helper functions ------------------------------------------------- +async def prompt_user_for_decisions(action_requests, review_configs): + decisions = [] for idx, action in enumerate(action_requests): - name = action.get("name") - args = action.get("args") - description = action.get("description", "") - print(f"{idx+1}. Инструмент: {name}") - print(f" Аргументы: {args}") - if description: - print(f" Описание: {description}") - return action_requests, review_configs + 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 +# --- Main interaction loop ------------------------------------------------- async def main(): - thread_id = "session-1" - while True: - user_input = input("Вы: ") - if not user_input: - continue - result, config = await invoke_agent(user_input, thread_id) - # Loop until no interrupt - while "__interrupt__" in result: - interrupt_value = result["__interrupt__"][0].value - action_requests, review_configs = print_interrupt(interrupt_value) - decisions = [] - for idx, action in enumerate(action_requests): - while True: - choice = input("a = approve, r = reject: ").strip().lower() - if choice == "a": - decisions.append({"type": "approve"}) - break - elif choice == "r": - msg = input("Сообщение для агента (причина отказа): ") - decisions.append({"type": "reject", "message": msg}) - break - else: - print("Неверный ввод. Попробуйте снова.") - result = await resume_agent(decisions, config) - # No more interrupts – print final answer - final_message = result["messages"][-1].content - print(f"\nАгент: {final_message}\n") - # Ask if user wants another query in the same session - again = input("Хотите задать ещё вопрос? (y/n): ").strip().lower() - if again != "y": - break + config = {"configurable": {"thread_id": "session-1"}} + # Initial user message + user_input = input("Вы: ") + result = await agent.ainvoke( + {"messages": [HumanMessage(content=user_input)]}, + 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) + + # Final answer + final_message = result["messages"][-1].content + print(f"\nАгент: {final_message}") if __name__ == "__main__": asyncio.run(main())