diff --git a/main.py b/main.py index 1553d77..bebc047 100644 --- a/main.py +++ b/main.py @@ -1,150 +1,125 @@ """ -Main entry point for the agent with memory and human‑in‑the‑loop confirmation. +Main entry point for the assignment. -The agent is built on top of LangChain's `create_tool_calling_agent` API. It uses a -`MemorySaver` checkpoint to keep conversation history across calls, and it -is configured with `interrupt_before=["tools"]` so that the agent pauses just -before invoking any tool. The pause allows us to ask the user for explicit -confirmation. +This script demonstrates a deep agent that: +1. Uses `create_deep_agent` from the `deepagents` package. +2. Stores conversation history with `MemorySaver`. +3. Pauses before each tool call and asks the user for confirmation. +4. Can defend its decisions when a user contradicts the original task. -The example includes one simple tool – ``get_price`` – which pretends to -query a price service. In a real project this would be replaced with an -actual API call. - -Three usage examples are demonstrated in ``__main__``: -1. Ask the agent for weather information (uses the built‑in ``web_search`` - tool). -2. Ask for a product price – the agent will pause and ask for confirmation. -3. Continue the conversation to show that memory is preserved. - -The console output is rendered with `rich` for better readability. +The script contains three example interactions that showcase: +- Normal operation. +- Tool usage with confirmation. +- Defending the agent's choice. """ -from __future__ import annotations - import os -import json -from typing import Any, Dict +from typing import Dict, Any +# LLM configuration – BroJS only from langchain_openai import ChatOpenAI -from langchain_core.messages import HumanMessage, SystemMessage +from deepagents import create_deep_agent +from deepagents.backends import CompositeBackend, FilesystemBackend from langgraph.checkpoint.memory import MemorySaver -from langgraph.types import Command -from langchain.agents import create_tool_calling_agent -from langchain.tools import tool from rich.console import Console +from rich.markdown import Markdown # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- -console = Console() - -llm = ChatOpenAI( +LLM = ChatOpenAI( model="openai/gpt-oss-20b:free", base_url="https://platform.brojs.ru/jrnl-bh/api/inference/v1", api_key=os.getenv("JOURNAL_MCP_PAT"), temperature=0.0, ) +# Backend – virtual FS (real shell not needed for this demo). +backend = CompositeBackend( + default=FilesystemBackend(root_dir="./workspace", virtual_mode=True, inherit_env=True), + routes={}, +) + # --------------------------------------------------------------------------- -# Simple tool – in a real scenario replace with an actual API call. +# Helper tools # --------------------------------------------------------------------------- +from langchain.tools import tool + @tool -async def get_price(query: Dict[str, Any]) -> str: - """Pretend to fetch a price for a product. +def echo(text: str) -> str: + """Return the same text – useful for demonstration.""" + return f"Echo: {text}" - Parameters - ---------- - query: dict - Expected keys are ``product`` and optionally ``currency``. - - Returns - ------- - str - A human‑readable string describing the price. - """ - product = query.get("product", "unknown") - currency = query.get("currency", "USD") - # Dummy logic – in real life call an external service. - return f"The price of {product} is 42.00 {currency}." +@tool +def add(a: int, b: int) -> int: + """Add two integers.""" + return a + b # --------------------------------------------------------------------------- -# Agent definition using create_tool_calling_agent (LangChain) +# Agent creation # --------------------------------------------------------------------------- memory = MemorySaver() - -agent = create_tool_calling_agent( - llm=llm, - tools=[get_price], - system_prompt="You are a helpful assistant that can query prices.", +agent = create_deep_agent( + llm=LLM, + tools=[echo, add], + backend=backend, + system_prompt="You are an assistant that must follow the user’s instructions and can use tools. You should ask for confirmation before calling a tool.", checkpointer=memory, - interrupt_before=["tools"], # pause before any tool call + interrupt_before=["tools"], # pause before each tool call ) -# --------------------------------------------------------------------------- -# Helper to run the agent and pause before each tool call. -# --------------------------------------------------------------------------- -async def ask_and_run(user_input: Dict[str, Any], config: Dict[str, Any]): - """Run the agent and pause before each tool call. - - Parameters - ---------- - user_input: dict - Dictionary with a ``messages`` key containing a list of messages. - config: dict - Configuration dictionary that must contain ``configurable`` with - ``thread_id``. - """ - async for chunk in agent.stream(user_input, config=config, stream_mode=["messages", "updates"]): - # ``chunk`` is a tuple (type, data). - chunk_type, chunk_data = chunk - state = agent.get_state(config) - - if chunk_type == "messages": - # Stream token by token. - console.print(chunk_data.content, end="", style="cyan") - console.file.flush() - elif chunk_type == "updates": - # Tool call preview – show the user what will be executed. - console.print("\n[bold magenta]Agent wants to call a tool:[/]") - console.print(json.dumps(chunk_data, indent=2), style="magenta") - - if "__interrupt__" in chunk_data and state.next == ("tools",): - # Pause – ask for confirmation. - console.print("\n[bold yellow]Confirmation required:[/] Do you allow the tool call? (y/n)") - answer = input().strip().lower() - if answer != "y": - console.print("[red]Action cancelled by user.[/]") - break - # Resume from the same state. - await agent.ainvoke(Command(resume=None), config=config) +console = Console() # --------------------------------------------------------------------------- -# Main loop – three examples as requested. +# Interaction helpers +# --------------------------------------------------------------------------- +async def run_interaction(messages: list[Dict[str, str]], thread_id: str) -> None: + """Run a single interaction with the agent.""" + config = {"configurable": {"thread_id": thread_id}} + async for chunk in agent.astream(messages, config=config, stream_mode="messages"): + if isinstance(chunk, str): + console.print(chunk, end="", style="bold cyan") + else: + # Handle possible interrupt + if "__interrupt__" in chunk and agent.get_state(config).next == ("tools",): + console.print("\n[bold yellow]Agent wants to use a tool:[/]", style="yellow") + console.print(Markdown(str(chunk))) + confirm = input("Allow? (y/n) ").strip().lower() + if confirm == "y": + await agent.ainvoke(Command(resume=None), config) + else: + console.print("[red]Tool call rejected by user.[/]\n", style="red") + break + else: + console.print(chunk, style="green") + console.print("\n--- End of interaction ---\n", style="bold magenta") + +# --------------------------------------------------------------------------- +# Main demo loop – three examples # --------------------------------------------------------------------------- if __name__ == "__main__": - thread_id = "demo-thread" - config = {"configurable": {"thread_id": thread_id}} - - console.print("[bold green]Welcome to the agent demo![/]") - console.print("Type 'exit' to quit.") - import asyncio - # Example 1 – simple chat (no tool call). - console.print("\n[underline]Example 1: Simple question[/]") - user_msg = {"messages": [HumanMessage(content="What is the capital of France?")]} - asyncio.run(ask_and_run(user_msg, config)) + async def main(): + # Example 1: Simple echo + await run_interaction( + [{"role": "human", "content": "Say hello."}], + thread_id="demo-echo", + ) - # Example 2 – tool call with confirmation. - console.print("\n[underline]Example 2: Tool call (price query)[/]") - user_msg = {"messages": [HumanMessage(content="Get price of laptop in USD")]} - asyncio.run(ask_and_run(user_msg, config)) + # Example 2: Tool usage with confirmation + await run_interaction( + [{"role": "human", "content": "Add 7 and 5."}], + thread_id="demo-add", + ) - # Example 3 – continue conversation to show memory. - console.print("\n[underline]Example 3: Continue conversation[/]") - user_msg = {"messages": [HumanMessage(content="What about the price in EUR?")]} - asyncio.run(ask_and_run(user_msg, config)) + # Example 3: Defending the agent when user contradicts task + await run_interaction( + [ + {"role": "human", "content": "I think you should not use tools at all."}, + {"role": "assistant", "content": "But I need to add numbers. Let me call the tool."}, + ], + thread_id="demo-contradiction", + ) - console.print("\n[bold green]Demo finished.[/]" -) + asyncio.run(main())