diff --git a/index.toml b/index.toml index 5a290264..e6b4ff9a 100644 --- a/index.toml +++ b/index.toml @@ -234,7 +234,7 @@ notebook = "47_Human_in_the_Loop_Agent.ipynb" aliases = [] completion_time = "20 min" created_at = 2025-10-30 -dependencies = ["haystack-ai>=2.23.0", "rich"] +dependencies = ["haystack-ai>=3.0.0", "rich"] featured = true [[tutorial]] diff --git a/tutorials/47_Human_in_the_Loop_Agent.ipynb b/tutorials/47_Human_in_the_Loop_Agent.ipynb index 87fe88af..07dbddb1 100644 --- a/tutorials/47_Human_in_the_Loop_Agent.ipynb +++ b/tutorials/47_Human_in_the_Loop_Agent.ipynb @@ -109,36 +109,17 @@ "metadata": { "id": "7b2579dc-2c59-4c3b-9691-73f5803d2add" }, - "source": [ - "## Setup and Imports\n", - "\n", - "We begin by importing Haystack classes, UI helpers, and the experimental Agent component." - ] + "source": "## Setup and Imports\n\nWe begin by importing Haystack classes, UI helpers, the `Agent` component, and the human-in-the-loop confirmation tools." }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "541cf667-a6b6-4e5b-beb1-65745c3f6ac2", "metadata": { "id": "541cf667-a6b6-4e5b-beb1-65745c3f6ac2" }, "outputs": [], - "source": [ - "from haystack.components.generators.chat import OpenAIChatGenerator\n", - "from haystack.dataclasses import ChatMessage\n", - "from haystack.tools import create_tool_from_function\n", - "from rich.console import Console\n", - "\n", - "from haystack.components.agents.agent import Agent\n", - "from haystack.human_in_the_loop import (\n", - " AlwaysAskPolicy,\n", - " AskOncePolicy,\n", - " BlockingConfirmationStrategy,\n", - " NeverAskPolicy,\n", - " RichConsoleUI,\n", - " SimpleConsoleUI,\n", - ")" - ] + "source": "from haystack.components.generators.chat import OpenAIChatGenerator\nfrom haystack.dataclasses import ChatMessage\nfrom haystack.tools import create_tool_from_function\nfrom rich.console import Console\n\nfrom haystack.components.agents.agent import Agent\nfrom haystack.hooks.human_in_the_loop import (\n AlwaysAskPolicy,\n AskOncePolicy,\n BlockingConfirmationStrategy,\n ConfirmationHook,\n NeverAskPolicy,\n RichConsoleUI,\n SimpleConsoleUI,\n)" }, { "cell_type": "markdown", @@ -195,40 +176,17 @@ "metadata": { "id": "5c2e21f4-b84f-4411-a5f1-7312afd44188" }, - "source": [ - "## Instantiate the Agent\n", - "\n", - "Instantiate the experimental [`Agent`](https://github.com/deepset-ai/haystack-experimental/blob/b2abe5486ee4ad03b6f3c136ca54aee482b9ed01/haystack_experimental/components/agents/agent.py#L69) with multiple tools and assign each tool a **confirmation strategy**." - ] + "source": "## Instantiate the Agent\n\nInstantiate the [`Agent`](https://docs.haystack.deepset.ai/docs/agent) with multiple tools. To add human-in-the-loop behavior, create a [`ConfirmationHook`](https://docs.haystack.deepset.ai/docs/confirmationhook) that assigns each tool a **confirmation strategy**, and register it on the Agent's `before_tool` hook point." }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "b6c81b8b-437e-4234-a6e8-8704708f5df9", "metadata": { "id": "b6c81b8b-437e-4234-a6e8-8704708f5df9" }, "outputs": [], - "source": [ - "cons = Console()\n", - "\n", - "agent = Agent(\n", - " chat_generator=OpenAIChatGenerator(model=\"gpt-4.1-mini\"),\n", - " tools=[balance_tool, addition_tool, phone_tool],\n", - " system_prompt=\"You are a helpful financial assistant. Use the provided tools to answer user questions.\",\n", - " confirmation_strategies={\n", - " balance_tool.name: BlockingConfirmationStrategy(\n", - " confirmation_policy=AlwaysAskPolicy(), confirmation_ui=RichConsoleUI(console=cons)\n", - " ),\n", - " addition_tool.name: BlockingConfirmationStrategy(\n", - " confirmation_policy=NeverAskPolicy(), confirmation_ui=RichConsoleUI(console=cons)\n", - " ),\n", - " phone_tool.name: BlockingConfirmationStrategy(\n", - " confirmation_policy=AskOncePolicy(), confirmation_ui=RichConsoleUI(console=cons)\n", - " ),\n", - " },\n", - ")" - ] + "source": "cons = Console()\n\nconfirmation_hook = ConfirmationHook(\n confirmation_strategies={\n balance_tool.name: BlockingConfirmationStrategy(\n confirmation_policy=AlwaysAskPolicy(), confirmation_ui=RichConsoleUI(console=cons)\n ),\n addition_tool.name: BlockingConfirmationStrategy(\n confirmation_policy=NeverAskPolicy(), confirmation_ui=RichConsoleUI(console=cons)\n ),\n phone_tool.name: BlockingConfirmationStrategy(\n confirmation_policy=AskOncePolicy(), confirmation_ui=RichConsoleUI(console=cons)\n ),\n }\n)\n\nagent = Agent(\n chat_generator=OpenAIChatGenerator(model=\"gpt-4.1-mini\"),\n tools=[balance_tool, addition_tool, phone_tool],\n system_prompt=\"You are a helpful financial assistant. Use the provided tools to answer user questions.\",\n hooks={\"before_tool\": [confirmation_hook]},\n)" }, { "cell_type": "markdown", @@ -236,29 +194,7 @@ "metadata": { "id": "4a1e1fc9-bf38-4207-a50f-dded827c4ae6" }, - "source": [ - "### Explanation\n", - "\n", - "Each [`BlockingConfirmationStrategy`](https://github.com/deepset-ai/haystack-experimental/blob/b2abe5486ee4ad03b6f3c136ca54aee482b9ed01/haystack_experimental/components/agents/human_in_the_loop/strategies.py#L32) defines when and how the agent asks for confirmation before executing a tool:\n", - "\n", - "* **`AlwaysAskPolicy`** – Always asks for approval before running.\n", - "* **`NeverAskPolicy`** – Executes automatically without user confirmation.\n", - "* **`AskOncePolicy`** – Asks once per tool, remembers approval for future runs.\n", - "\n", - "The UI can be either:\n", - "\n", - "* `RichConsoleUI` for colorized and more aesthetic prompts.\n", - "* `SimpleConsoleUI` for basic prompts.\n", - "\n", - "\n", - "Both UIs will present three options:\n", - "\n", - "- **y**: Proceed with the tool execution as planned\n", - "- **n**: Cancel the tool execution; agent will try alternative approaches\n", - "- **m**: Edit the tool parameters before execution\n", - "\n", - "**NOTE**: Custom UIs and Policies can also be implemented by following the respective `ConfirmationUI` and `ConfirmationPolicy` protocols. This allows full flexibility for domain-specific workflows." - ] + "source": "### Explanation\n\nThe `ConfirmationHook` runs at the Agent's `before_tool` hook point, after the model requests a tool call but before the tool actually runs. For each requested tool, it looks up the matching entry in `confirmation_strategies`. A key can be a single tool name, a tuple of tool names sharing one strategy, or the wildcard `\"*\"` which applies to any tool without a more specific entry.\n\nEach [`BlockingConfirmationStrategy`](https://docs.haystack.deepset.ai/docs/blockingconfirmationstrategy) defines when and how the agent asks for confirmation before executing a tool. The `confirmation_policy` decides *when* to ask:\n\n* **`AlwaysAskPolicy`** – Always asks for approval before running.\n* **`NeverAskPolicy`** – Executes automatically without user confirmation.\n* **`AskOncePolicy`** – Asks once per tool, remembers approval for future runs.\n\nThe `confirmation_ui` decides *how* to ask:\n\n* `RichConsoleUI` for colorized and more aesthetic prompts.\n* `SimpleConsoleUI` for basic prompts.\n\n\nBoth UIs will present three options:\n\n- **y**: Proceed with the tool execution as planned\n- **n**: Cancel the tool execution; agent will try alternative approaches\n- **m**: Edit the tool parameters before execution\n\n**NOTE**: Custom UIs and Policies can also be implemented by following the respective `ConfirmationUI` and `ConfirmationPolicy` protocols. This allows full flexibility for domain-specific workflows." }, { "cell_type": "markdown", @@ -572,32 +508,13 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "2c650eb7-244d-4a58-b024-e79a1b79e4cd", "metadata": { "id": "2c650eb7-244d-4a58-b024-e79a1b79e4cd" }, "outputs": [], - "source": [ - "from haystack.human_in_the_loop.types import ConfirmationPolicy\n", - "from typing import Any\n", - "\n", - "\n", - "class BudgetBasedPolicy(ConfirmationPolicy):\n", - " \"\"\"Ask for confirmation when operations exceed a cost threshold.\"\"\"\n", - "\n", - " def __init__(self, cost_threshold: float = 10.0):\n", - " self.cost_threshold = cost_threshold\n", - "\n", - " def should_ask(self, tool_name: str, tool_description: str, tool_params: dict[str, Any]) -> bool:\n", - " \"\"\"Ask if the operation cost exceeds the threshold.\"\"\"\n", - " # Check for cost-related parameters\n", - " cost = tool_params.get(\"cost\", 0.0)\n", - " amount = tool_params.get(\"amount\", 0.0)\n", - " price = tool_params.get(\"price\", 0.0)\n", - "\n", - " return max(cost, amount, price) > self.cost_threshold" - ] + "source": "from haystack.hooks.human_in_the_loop.types import ConfirmationPolicy\nfrom typing import Any\n\n\nclass BudgetBasedPolicy(ConfirmationPolicy):\n \"\"\"Ask for confirmation when operations exceed a cost threshold.\"\"\"\n\n def __init__(self, cost_threshold: float = 10.0):\n self.cost_threshold = cost_threshold\n\n def should_ask(self, tool_name: str, tool_description: str, tool_params: dict[str, Any]) -> bool:\n \"\"\"Ask if the operation cost exceeds the threshold.\"\"\"\n # Check for cost-related parameters\n cost = tool_params.get(\"cost\", 0.0)\n amount = tool_params.get(\"amount\", 0.0)\n price = tool_params.get(\"price\", 0.0)\n\n return max(cost, amount, price) > self.cost_threshold" }, { "cell_type": "markdown", @@ -613,43 +530,13 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "7810bdd3-7592-412e-a15b-106b5d4af44e", "metadata": { "id": "7810bdd3-7592-412e-a15b-106b5d4af44e" }, "outputs": [], - "source": [ - "from haystack.components.generators.utils import print_streaming_chunk\n", - "\n", - "\n", - "def expense(cost: float, description: str) -> float:\n", - " \"\"\"Submit an expense report that has a `cost` and `description`\"\"\"\n", - " # This is where we would add a real submission request\n", - " return \"Expense report submitted successfully!\"\n", - "\n", - "\n", - "expense_tool = create_tool_from_function(\n", - " function=expense, name=\"expense\", description=\"Submit an expense report that has a `cost` and `description`\"\n", - ")\n", - "\n", - "cons = Console()\n", - "\n", - "agent = Agent(\n", - " chat_generator=OpenAIChatGenerator(model=\"gpt-4.1\"),\n", - " tools=[expense_tool],\n", - " system_prompt=(\n", - " \"You are a helpful financial assistant that can submit expense reports for users. \"\n", - " \"Use the `expense` tool which only requires a cost amount and short description of the expense (e.g. 'busines lunch'). \"\n", - " \"Only respond with whether the report was submitted successfully. \"\n", - " ),\n", - " confirmation_strategies={\n", - " expense_tool.name: BlockingConfirmationStrategy(\n", - " confirmation_policy=BudgetBasedPolicy(cost_threshold=100.0), confirmation_ui=RichConsoleUI(console=cons)\n", - " )\n", - " },\n", - ")" - ] + "source": "from haystack.components.generators.utils import print_streaming_chunk\n\n\ndef expense(cost: float, description: str) -> float:\n \"\"\"Submit an expense report that has a `cost` and `description`\"\"\"\n # This is where we would add a real submission request\n return \"Expense report submitted successfully!\"\n\n\nexpense_tool = create_tool_from_function(\n function=expense, name=\"expense\", description=\"Submit an expense report that has a `cost` and `description`\"\n)\n\ncons = Console()\n\nconfirmation_hook = ConfirmationHook(\n confirmation_strategies={\n expense_tool.name: BlockingConfirmationStrategy(\n confirmation_policy=BudgetBasedPolicy(cost_threshold=100.0), confirmation_ui=RichConsoleUI(console=cons)\n )\n }\n)\n\nagent = Agent(\n chat_generator=OpenAIChatGenerator(model=\"gpt-4.1\"),\n tools=[expense_tool],\n system_prompt=(\n \"You are a helpful financial assistant that can submit expense reports for users. \"\n \"Use the `expense` tool which only requires a cost amount and short description of the expense (e.g. 'busines lunch'). \"\n \"Only respond with whether the report was submitted successfully. \"\n ),\n hooks={\"before_tool\": [confirmation_hook]},\n)" }, { "cell_type": "markdown", @@ -857,4 +744,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file