diff --git a/MIGRATION.md b/MIGRATION.md index 1993b37f71c..c4abca65e33 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -639,6 +639,8 @@ agent = Agent( **Why:** Confirmation was a one-off, before-tool interception bolted onto the Agent. Hooks generalize that seam, so HITL becomes one application of a single, uniform extension point instead of a parallel concept with its own serialization and run plumbing. +The Human-in-the-Loop module has also moved from `haystack.human_in_the_loop` to `haystack.hooks.human_in_the_loop`, so that it lives alongside the other built-in hooks (such as tool result offloading). Update your imports to the new location. + **How to migrate:** Before (v2.x): @@ -661,7 +663,7 @@ agent.run(messages=[...], confirmation_strategy_context={"websocket": ws}) After (v3.0): ```python from haystack.components.agents import Agent -from haystack.human_in_the_loop import ( +from haystack.hooks.human_in_the_loop import ( BlockingConfirmationStrategy, AlwaysAskPolicy, ConfirmationHook, diff --git a/docs-website/docs/pipeline-components/agents-1/hooks.mdx b/docs-website/docs/pipeline-components/agents-1/hooks.mdx index b586078fb84..6abb7e54b5e 100644 --- a/docs-website/docs/pipeline-components/agents-1/hooks.mdx +++ b/docs-website/docs/pipeline-components/agents-1/hooks.mdx @@ -196,7 +196,7 @@ print(result["last_message"].text) ## Ready-made hooks -Haystack ships two ready-made hooks: +Haystack ships two ready-made hooks, each in its own submodule of `haystack.hooks`: -- `ConfirmationHook`: A `before_tool` hook that applies Human-in-the-Loop confirmation strategies to pending tool calls — a human can confirm, modify, or reject the tool calls the model requested before they run. See [Human in the Loop](./human-in-the-loop.mdx). -- `ToolResultOffloadHook`: An `after_tool` hook that offloads tool results to a `ToolResultStore` (such as `FileSystemToolResultStore`) and replaces them in the conversation with a compact pointer, so the next LLM call sees a reference instead of the full result. Per-tool policies (`AlwaysOffload`, `NeverOffload`, `OffloadOverChars`) control which results are offloaded. See [Tool Result Offloading](./tool-result-offloading.mdx). +- `ConfirmationHook` (from `haystack.hooks.human_in_the_loop`): A `before_tool` hook that applies Human-in-the-Loop confirmation strategies to pending tool calls — a human can confirm, modify, or reject the tool calls the model requested before they run. See [Human in the Loop](./human-in-the-loop.mdx). +- `ToolResultOffloadHook` (from `haystack.hooks.tool_result_offloading`): An `after_tool` hook that offloads tool results to a `ToolResultStore` (such as `FileSystemToolResultStore`) and replaces them in the conversation with a compact pointer, so the next LLM call sees a reference instead of the full result. Per-tool policies (`AlwaysOffload`, `NeverOffload`, `OffloadOverChars`) control which results are offloaded. See [Tool Result Offloading](./tool-result-offloading.mdx). diff --git a/docs-website/docs/pipeline-components/agents-1/human-in-the-loop.mdx b/docs-website/docs/pipeline-components/agents-1/human-in-the-loop.mdx index cc8c02b36cd..ee56a5f8f5d 100644 --- a/docs-website/docs/pipeline-components/agents-1/human-in-the-loop.mdx +++ b/docs-website/docs/pipeline-components/agents-1/human-in-the-loop.mdx @@ -17,8 +17,8 @@ This is useful for high-stakes operations - such as sending emails, modifying da | --- | --- | | **Configured on** | The [`Agent`](./agent.mdx) component, as a `ConfirmationHook` registered under the `before_tool` [hook point](./hooks.mdx) | | **Key classes** | `ConfirmationHook`, `BlockingConfirmationStrategy`, `AlwaysAskPolicy`, `AskOncePolicy`, `NeverAskPolicy`, `RichConsoleUI`, `SimpleConsoleUI` | -| **Import path** | `haystack.human_in_the_loop` | -| **GitHub link** | https://github.com/deepset-ai/haystack/blob/main/haystack/human_in_the_loop/ | +| **Import path** | `haystack.hooks.human_in_the_loop` | +| **GitHub link** | https://github.com/deepset-ai/haystack/blob/main/haystack/hooks/human_in_the_loop/ | | **Package name** | `haystack-ai` | @@ -56,7 +56,7 @@ from typing import Annotated from haystack.components.agents import Agent from haystack.components.generators.chat import OpenAIChatGenerator from haystack.dataclasses import ChatMessage -from haystack.human_in_the_loop import ( +from haystack.hooks.human_in_the_loop import ( AlwaysAskPolicy, BlockingConfirmationStrategy, ConfirmationHook, @@ -118,7 +118,7 @@ pip install rich ``` ```python -from haystack.human_in_the_loop import RichConsoleUI +from haystack.hooks.human_in_the_loop import RichConsoleUI strategy = BlockingConfirmationStrategy( confirmation_policy=AlwaysAskPolicy(), @@ -202,10 +202,13 @@ Policies control *when* the human is asked. ### Custom policy -You can implement your own policy by subclassing `ConfirmationPolicy` from `haystack.human_in_the_loop.types`: +You can implement your own policy by subclassing `ConfirmationPolicy` from `haystack.hooks.human_in_the_loop.types`: ```python -from haystack.human_in_the_loop.types import ConfirmationPolicy, ConfirmationUIResult +from haystack.hooks.human_in_the_loop.types import ( + ConfirmationPolicy, + ConfirmationUIResult, +) from typing import Any @@ -227,8 +230,8 @@ It is called after the user responds and receives the full `ConfirmationUIResult The following policy asks once per tool name and skips re-asking for any tool the user has already confirmed: ```python -from haystack.human_in_the_loop.types import ConfirmationPolicy -from haystack.human_in_the_loop import ConfirmationUIResult +from haystack.hooks.human_in_the_loop.types import ConfirmationPolicy +from haystack.hooks.human_in_the_loop import ConfirmationUIResult from typing import Any @@ -295,11 +298,11 @@ This is a good reference if you need non-blocking HITL in a web or server enviro ## Custom UI -Implement `ConfirmationUI` from `haystack.human_in_the_loop.types` to build your own interface - for example, a web-based approval queue: +Implement `ConfirmationUI` from `haystack.hooks.human_in_the_loop.types` to build your own interface - for example, a web-based approval queue: ```python -from haystack.human_in_the_loop.types import ConfirmationUI -from haystack.human_in_the_loop import ConfirmationUIResult +from haystack.hooks.human_in_the_loop.types import ConfirmationUI +from haystack.hooks.human_in_the_loop import ConfirmationUIResult from typing import Any diff --git a/docs-website/reference/haystack-api/human_in_the_loop_api.md b/docs-website/reference/haystack-api/human_in_the_loop_api.md deleted file mode 100644 index 97b39925a47..00000000000 --- a/docs-website/reference/haystack-api/human_in_the_loop_api.md +++ /dev/null @@ -1,471 +0,0 @@ ---- -title: "Human-in-the-Loop" -id: human-in-the-loop-api -description: "Abstractions for integrating human feedback and interaction into Agent workflows." -slug: "/human-in-the-loop-api" ---- - - -## dataclasses - -### ConfirmationUIResult - -Result of the confirmation UI interaction. - -**Parameters:** - -- **action** (str) – The action taken by the user such as "confirm", "reject", or "modify". - This action type is not enforced to allow for custom actions to be implemented. -- **feedback** (str | None) – Optional feedback message from the user. For example, if the user rejects the tool execution, - they might provide a reason for the rejection. -- **new_tool_params** (dict\[str, Any\] | None) – Optional set of new parameters for the tool. For example, if the user chooses to modify the tool parameters, - they can provide a new set of parameters here. - -### ToolExecutionDecision - -Decision made regarding tool execution. - -**Parameters:** - -- **tool_name** (str) – The name of the tool to be executed. -- **execute** (bool) – A boolean indicating whether to execute the tool with the provided parameters. -- **tool_call_id** (str | None) – Optional unique identifier for the tool call. This can be used to track and correlate the decision with a - specific tool invocation. -- **feedback** (str | None) – Optional feedback message. - For example, if the tool execution is rejected, this can contain the reason. Or if the tool parameters were - modified, this can contain the modification details. -- **final_tool_params** (dict\[str, Any\] | None) – Optional final parameters for the tool if execution is confirmed or modified. - -#### to_dict - -```python -to_dict() -> dict[str, Any] -``` - -Convert the ToolExecutionDecision to a dictionary representation. - -**Returns:** - -- dict\[str, Any\] – A dictionary containing the tool execution decision details. - -#### from_dict - -```python -from_dict(data: dict[str, Any]) -> ToolExecutionDecision -``` - -Populate the ToolExecutionDecision from a dictionary representation. - -**Parameters:** - -- **data** (dict\[str, Any\]) – A dictionary containing the tool execution decision details. - -**Returns:** - -- ToolExecutionDecision – An instance of ToolExecutionDecision. - -## hooks - -### ConfirmationHook - -A `before_tool` Agent hook that applies Human-in-the-Loop confirmation strategies to pending tool calls. - -Register it on an `Agent` to confirm, modify, or reject tool calls before they run: - -```python -from haystack.components.agents import Agent -from haystack.human_in_the_loop import ( - AlwaysAskPolicy, - BlockingConfirmationStrategy, - ConfirmationHook, - NeverAskPolicy, - RichConsoleUI, - SimpleConsoleUI, -) - -hook = ConfirmationHook( - confirmation_strategies={ - "my_tool": BlockingConfirmationStrategy( - confirmation_policy=NeverAskPolicy(), confirmation_ui=SimpleConsoleUI() - ) - } -) -agent = Agent(chat_generator=..., tools=[...], hooks={"before_tool": [hook]}) -``` - -A key may 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. More specific keys win, so you can set a default for all tools and -override individual ones: - -```python -hook = ConfirmationHook( - confirmation_strategies={ - "delete_file": BlockingConfirmationStrategy( - confirmation_policy=AlwaysAskPolicy(), confirmation_ui=RichConsoleUI() - ), - "*": BlockingConfirmationStrategy( - confirmation_policy=NeverAskPolicy(), confirmation_ui=SimpleConsoleUI() - ), - } -) -``` - -Request-scoped resources for the strategies (e.g. a WebSocket or queue) are passed per run via the Agent's -`hook_context` argument (`agent.run(messages=[...], hook_context={...})`) and read by the hook with -`state.data.get("hook_context")`. - -This hook only makes sense at the `before_tool` hook point, where the pending tool calls exist (between the model -requesting tools and those tools running); the Agent enforces this and raises if it is registered elsewhere. Use a -single ConfirmationHook with one entry per tool (or per tuple of tools) in `confirmation_strategies` rather than -registering several hooks. - -#### __init__ - -```python -__init__( - confirmation_strategies: dict[str | tuple[str, ...], ConfirmationStrategy], -) -> None -``` - -Initialize the hook with its per-tool confirmation strategies. - -**Parameters:** - -- **confirmation_strategies** (dict\[str | tuple\[str, ...\], ConfirmationStrategy\]) – Mapping of tool name (or a tuple of tool names) to its `ConfirmationStrategy`. - The wildcard key `"*"` applies to any tool without a more specific entry. - -#### run - -```python -run(state: State) -> None -``` - -Confirm the pending tool calls, rewriting the `messages` in `state` to reflect modifications and rejections. - -**Parameters:** - -- **state** (State) – The Agent's live `State`. Reads the available tools (`state.data.get("tools")`) and the per-run - context (`state.data.get("hook_context")`), and the pending tool calls from the last message; writes the - updated conversation back to `messages`. Reads go through `state.data` rather than `state.get`, which - deep-copies and would break non-copyable resources (e.g. a WebSocket or client) in `hook_context`. - -#### run_async - -```python -run_async(state: State) -> None -``` - -Async version of `run`. - -#### to_dict - -```python -to_dict() -> dict[str, Any] -``` - -Serialize the hook, including its confirmation strategies (tuple keys become JSON-array strings). - -#### from_dict - -```python -from_dict(data: dict[str, Any]) -> ConfirmationHook -``` - -Deserialize the hook, reconstructing its confirmation strategies. - -## policies - -### AlwaysAskPolicy - -Bases: ConfirmationPolicy - -Always ask for confirmation. - -#### should_ask - -```python -should_ask( - tool_name: str, tool_description: str, tool_params: dict[str, Any] -) -> bool -``` - -Always ask for confirmation before executing the tool. - -**Parameters:** - -- **tool_name** (str) – The name of the tool to be executed. -- **tool_description** (str) – The description of the tool. -- **tool_params** (dict\[str, Any\]) – The parameters to be passed to the tool. - -**Returns:** - -- bool – Always returns True, indicating confirmation is needed. - -### NeverAskPolicy - -Bases: ConfirmationPolicy - -Never ask for confirmation. - -#### should_ask - -```python -should_ask( - tool_name: str, tool_description: str, tool_params: dict[str, Any] -) -> bool -``` - -Never ask for confirmation, always proceed with tool execution. - -**Parameters:** - -- **tool_name** (str) – The name of the tool to be executed. -- **tool_description** (str) – The description of the tool. -- **tool_params** (dict\[str, Any\]) – The parameters to be passed to the tool. - -**Returns:** - -- bool – Always returns False, indicating no confirmation is needed. - -### AskOncePolicy - -Bases: ConfirmationPolicy - -Ask only once per tool with specific parameters. - -#### __init__ - -```python -__init__() -> None -``` - -Creates an instance of AskOncePolicy. - -#### should_ask - -```python -should_ask( - tool_name: str, tool_description: str, tool_params: dict[str, Any] -) -> bool -``` - -Ask for confirmation only once per tool with specific parameters. - -**Parameters:** - -- **tool_name** (str) – The name of the tool to be executed. -- **tool_description** (str) – The description of the tool. -- **tool_params** (dict\[str, Any\]) – The parameters to be passed to the tool. - -**Returns:** - -- bool – True if confirmation is needed, False if already asked with the same parameters. - -#### update_after_confirmation - -```python -update_after_confirmation( - tool_name: str, - tool_description: str, - tool_params: dict[str, Any], - confirmation_result: ConfirmationUIResult, -) -> None -``` - -Store the tool and parameters if the action was "confirm" to avoid asking again. - -This method updates the internal state to remember that the user has already confirmed the execution of the -tool with the given parameters. - -**Parameters:** - -- **tool_name** (str) – The name of the tool that was executed. -- **tool_description** (str) – The description of the tool. -- **tool_params** (dict\[str, Any\]) – The parameters that were passed to the tool. -- **confirmation_result** (ConfirmationUIResult) – The result from the confirmation UI. - -## strategies - -### BlockingConfirmationStrategy - -Confirmation strategy that blocks execution to gather user feedback. - -#### __init__ - -```python -__init__( - *, - confirmation_policy: ConfirmationPolicy, - confirmation_ui: ConfirmationUI, - reject_template: str = REJECTION_FEEDBACK_TEMPLATE, - modify_template: str = MODIFICATION_FEEDBACK_TEMPLATE, - user_feedback_template: str = USER_FEEDBACK_TEMPLATE -) -> None -``` - -Initialize the BlockingConfirmationStrategy with a confirmation policy and UI. - -**Parameters:** - -- **confirmation_policy** (ConfirmationPolicy) – The confirmation policy to determine when to ask for user confirmation. -- **confirmation_ui** (ConfirmationUI) – The user interface to interact with the user for confirmation. -- **reject_template** (str) – Template for rejection feedback messages. It should include a `{tool_name}` placeholder. -- **modify_template** (str) – Template for modification feedback messages. It should include `{tool_name}` and `{final_tool_params}` - placeholders. -- **user_feedback_template** (str) – Template for user feedback messages. It should include a `{feedback}` placeholder. - -#### run - -```python -run( - *, - tool_name: str, - tool_description: str, - tool_params: dict[str, Any], - tool_call_id: str | None = None, - confirmation_strategy_context: dict[str, Any] | None = None -) -> ToolExecutionDecision -``` - -Run the human-in-the-loop strategy for a given tool and its parameters. - -**Parameters:** - -- **tool_name** (str) – The name of the tool to be executed. -- **tool_description** (str) – The description of the tool. -- **tool_params** (dict\[str, Any\]) – The parameters to be passed to the tool. -- **tool_call_id** (str | None) – Optional unique identifier for the tool call. This can be used to track and correlate the decision with a - specific tool invocation. -- **confirmation_strategy_context** (dict\[str, Any\] | None) – Optional dictionary for passing request-scoped resources. Useful in web/server environments - to provide per-request objects (e.g., WebSocket connections, async queues, Redis pub/sub clients) - that strategies can use for non-blocking user interaction. - -**Returns:** - -- ToolExecutionDecision – A ToolExecutionDecision indicating whether to execute the tool with the given parameters, or a - feedback message if rejected. - -#### run_async - -```python -run_async( - *, - tool_name: str, - tool_description: str, - tool_params: dict[str, Any], - tool_call_id: str | None = None, - confirmation_strategy_context: dict[str, Any] | None = None -) -> ToolExecutionDecision -``` - -Async version of run. Calls the sync run() method by default. - -**Parameters:** - -- **tool_name** (str) – The name of the tool to be executed. -- **tool_description** (str) – The description of the tool. -- **tool_params** (dict\[str, Any\]) – The parameters to be passed to the tool. -- **tool_call_id** (str | None) – Optional unique identifier for the tool call. -- **confirmation_strategy_context** (dict\[str, Any\] | None) – Optional dictionary for passing request-scoped resources. - -**Returns:** - -- ToolExecutionDecision – A ToolExecutionDecision indicating whether to execute the tool with the given parameters. - -#### to_dict - -```python -to_dict() -> dict[str, Any] -``` - -Serializes the BlockingConfirmationStrategy to a dictionary. - -**Returns:** - -- dict\[str, Any\] – Dictionary with serialized data. - -#### from_dict - -```python -from_dict(data: dict[str, Any]) -> BlockingConfirmationStrategy -``` - -Deserializes the BlockingConfirmationStrategy from a dictionary. - -**Parameters:** - -- **data** (dict\[str, Any\]) – Dictionary to deserialize from. - -**Returns:** - -- BlockingConfirmationStrategy – Deserialized BlockingConfirmationStrategy. - -## user_interfaces - -### RichConsoleUI - -Bases: ConfirmationUI - -Rich console interface for user interaction. - -#### __init__ - -```python -__init__(console: Console | None = None) -> None -``` - -Creates an instance of RichConsoleUI. - -#### get_user_confirmation - -```python -get_user_confirmation( - tool_name: str, tool_description: str, tool_params: dict[str, Any] -) -> ConfirmationUIResult -``` - -Get user confirmation for tool execution via rich console prompts. - -**Parameters:** - -- **tool_name** (str) – The name of the tool to be executed. -- **tool_description** (str) – The description of the tool. -- **tool_params** (dict\[str, Any\]) – The parameters to be passed to the tool. - -**Returns:** - -- ConfirmationUIResult – ConfirmationUIResult based on user input. - -#### to_dict - -```python -to_dict() -> dict[str, Any] -``` - -Serializes the RichConsoleConfirmationUI to a dictionary. - -**Returns:** - -- dict\[str, Any\] – Dictionary with serialized data. - -### SimpleConsoleUI - -Bases: ConfirmationUI - -Simple console interface using standard input/output. - -#### get_user_confirmation - -```python -get_user_confirmation( - tool_name: str, tool_description: str, tool_params: dict[str, Any] -) -> ConfirmationUIResult -``` - -Get user confirmation for tool execution via simple console prompts. - -**Parameters:** - -- **tool_name** (str) – The name of the tool to be executed. -- **tool_description** (str) – The description of the tool. -- **tool_params** (dict\[str, Any\]) – The parameters to be passed to the tool. diff --git a/docs-website/sidebars.js b/docs-website/sidebars.js index 2c5dfff479d..83bfdf4e814 100644 --- a/docs-website/sidebars.js +++ b/docs-website/sidebars.js @@ -156,9 +156,18 @@ export default { label: 'Agents', items: [ 'pipeline-components/agents-1/agent', - 'pipeline-components/agents-1/hooks', - 'pipeline-components/agents-1/human-in-the-loop', - 'pipeline-components/agents-1/tool-result-offloading', + { + type: 'category', + label: 'Hooks', + link: { + type: 'doc', + id: 'pipeline-components/agents-1/hooks', + }, + items: [ + 'pipeline-components/agents-1/human-in-the-loop', + 'pipeline-components/agents-1/tool-result-offloading', + ], + }, 'pipeline-components/agents-1/state', ], }, diff --git a/haystack/human_in_the_loop/__init__.py b/haystack/hooks/human_in_the_loop/__init__.py similarity index 100% rename from haystack/human_in_the_loop/__init__.py rename to haystack/hooks/human_in_the_loop/__init__.py diff --git a/haystack/human_in_the_loop/dataclasses.py b/haystack/hooks/human_in_the_loop/dataclasses.py similarity index 100% rename from haystack/human_in_the_loop/dataclasses.py rename to haystack/hooks/human_in_the_loop/dataclasses.py diff --git a/haystack/human_in_the_loop/hooks.py b/haystack/hooks/human_in_the_loop/hooks.py similarity index 97% rename from haystack/human_in_the_loop/hooks.py rename to haystack/hooks/human_in_the_loop/hooks.py index 9cc567810c4..10cb8b8f734 100644 --- a/haystack/human_in_the_loop/hooks.py +++ b/haystack/hooks/human_in_the_loop/hooks.py @@ -7,13 +7,13 @@ from haystack.components.agents.state.state import State from haystack.components.agents.state.state_utils import replace_values from haystack.core.serialization import default_from_dict, default_to_dict -from haystack.human_in_the_loop.strategies import ( +from haystack.hooks.human_in_the_loop.strategies import ( _deserialize_confirmation_strategies, _process_confirmation_strategies, _process_confirmation_strategies_async, _serialize_confirmation_strategies, ) -from haystack.human_in_the_loop.types import ConfirmationStrategy +from haystack.hooks.human_in_the_loop.types import ConfirmationStrategy class ConfirmationHook: @@ -24,7 +24,7 @@ class ConfirmationHook: ```python from haystack.components.agents import Agent - from haystack.human_in_the_loop import ( + from haystack.hooks.human_in_the_loop import ( AlwaysAskPolicy, BlockingConfirmationStrategy, ConfirmationHook, diff --git a/haystack/human_in_the_loop/policies.py b/haystack/hooks/human_in_the_loop/policies.py similarity index 95% rename from haystack/human_in_the_loop/policies.py rename to haystack/hooks/human_in_the_loop/policies.py index 46d2b002d39..55efd478851 100644 --- a/haystack/human_in_the_loop/policies.py +++ b/haystack/hooks/human_in_the_loop/policies.py @@ -4,8 +4,8 @@ from typing import Any -from haystack.human_in_the_loop import ConfirmationUIResult -from haystack.human_in_the_loop.types import ConfirmationPolicy +from haystack.hooks.human_in_the_loop import ConfirmationUIResult +from haystack.hooks.human_in_the_loop.types import ConfirmationPolicy class AlwaysAskPolicy(ConfirmationPolicy): diff --git a/haystack/human_in_the_loop/strategies.py b/haystack/hooks/human_in_the_loop/strategies.py similarity index 99% rename from haystack/human_in_the_loop/strategies.py rename to haystack/hooks/human_in_the_loop/strategies.py index a9172a2280d..12a6b0dfd49 100644 --- a/haystack/human_in_the_loop/strategies.py +++ b/haystack/hooks/human_in_the_loop/strategies.py @@ -9,8 +9,8 @@ from haystack.components.agents.state.state import State from haystack.core.serialization import component_to_dict, default_from_dict, default_to_dict from haystack.dataclasses import ChatMessage, ToolCall -from haystack.human_in_the_loop import ToolExecutionDecision -from haystack.human_in_the_loop.types import ConfirmationPolicy, ConfirmationStrategy, ConfirmationUI +from haystack.hooks.human_in_the_loop import ToolExecutionDecision +from haystack.hooks.human_in_the_loop.types import ConfirmationPolicy, ConfirmationStrategy, ConfirmationUI from haystack.tools import Tool from haystack.utils.deserialization import deserialize_component_inplace diff --git a/haystack/human_in_the_loop/types/__init__.py b/haystack/hooks/human_in_the_loop/types/__init__.py similarity index 100% rename from haystack/human_in_the_loop/types/__init__.py rename to haystack/hooks/human_in_the_loop/types/__init__.py diff --git a/haystack/human_in_the_loop/types/protocol.py b/haystack/hooks/human_in_the_loop/types/protocol.py similarity index 97% rename from haystack/human_in_the_loop/types/protocol.py rename to haystack/hooks/human_in_the_loop/types/protocol.py index 2cafa96d03a..fbda306d8e9 100644 --- a/haystack/human_in_the_loop/types/protocol.py +++ b/haystack/hooks/human_in_the_loop/types/protocol.py @@ -5,7 +5,7 @@ from typing import Any, Protocol from haystack.core.serialization import default_from_dict, default_to_dict -from haystack.human_in_the_loop.dataclasses import ConfirmationUIResult, ToolExecutionDecision +from haystack.hooks.human_in_the_loop.dataclasses import ConfirmationUIResult, ToolExecutionDecision class ConfirmationUI(Protocol): diff --git a/haystack/human_in_the_loop/user_interfaces.py b/haystack/hooks/human_in_the_loop/user_interfaces.py similarity index 98% rename from haystack/human_in_the_loop/user_interfaces.py rename to haystack/hooks/human_in_the_loop/user_interfaces.py index 06c80d82464..31c07c5d47a 100644 --- a/haystack/human_in_the_loop/user_interfaces.py +++ b/haystack/hooks/human_in_the_loop/user_interfaces.py @@ -7,8 +7,8 @@ from typing import Any from haystack.core.serialization import default_to_dict -from haystack.human_in_the_loop import ConfirmationUIResult -from haystack.human_in_the_loop.types import ConfirmationUI +from haystack.hooks.human_in_the_loop import ConfirmationUIResult +from haystack.hooks.human_in_the_loop.types import ConfirmationUI from haystack.lazy_imports import LazyImport with LazyImport(message="Run 'pip install rich'") as rich_import: diff --git a/pydoc/hooks_api.yml b/pydoc/hooks_api.yml index 6555c46941f..7d79faba95d 100644 --- a/pydoc/hooks_api.yml +++ b/pydoc/hooks_api.yml @@ -1,7 +1,9 @@ loaders: - search_path: [../haystack/hooks] - modules: ["protocol", "from_function", "tool_result_offloading/hooks", "tool_result_offloading/policies", - "tool_result_offloading/stores", "tool_result_offloading/types/protocol"] + modules: ["protocol", "from_function", "human_in_the_loop/dataclasses", "human_in_the_loop/hooks", + "human_in_the_loop/policies", "human_in_the_loop/strategies", "human_in_the_loop/user_interfaces", + "tool_result_offloading/hooks", "tool_result_offloading/policies", "tool_result_offloading/stores", + "tool_result_offloading/types/protocol"] processors: - type: filter documented_only: true @@ -10,5 +12,5 @@ renderer: title: Hooks id: hooks-api description: Hooks that run at points in the Agent's run loop and influence it by mutating State, including - built-in tool result offloading. + built-in tool result offloading and Human-in-the-Loop tool confirmation. filename: hooks_api.md diff --git a/pydoc/human_in_the_loop_api.yml b/pydoc/human_in_the_loop_api.yml deleted file mode 100644 index 9b388942175..00000000000 --- a/pydoc/human_in_the_loop_api.yml +++ /dev/null @@ -1,13 +0,0 @@ -loaders: - - search_path: [../haystack/human_in_the_loop] - modules: ["dataclasses", "hooks", "policies", "strategies", "user_interfaces"] -processors: - - type: filter - documented_only: true - skip_empty_modules: true -renderer: - title: Human-in-the-Loop - id: human-in-the-loop-api - description: Abstractions for integrating human feedback and interaction into Agent - workflows. - filename: human_in_the_loop_api.md diff --git a/pyproject.toml b/pyproject.toml index de0fb3b2958..a6579bdc1a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -149,7 +149,7 @@ all = 'pytest {args:test}' e2e = 'pytest {args:e2e}' # TODO We want to eventually type the whole test folder -types = "mypy --install-types --non-interactive --cache-dir=.mypy_cache/ {args:haystack test/core/ test/marshal/ test/testing/ test/tracing/ test/tools/ test/human_in_the_loop test/evaluation test/document_stores test/dataclasses test/utils/ test/components/embedders/}" +types = "mypy --install-types --non-interactive --cache-dir=.mypy_cache/ {args:haystack test/core/ test/marshal/ test/testing/ test/tracing/ test/tools/ test/hooks/human_in_the_loop test/evaluation test/document_stores test/dataclasses test/utils/ test/components/embedders/}" [project.urls] "CI: GitHub" = "https://github.com/deepset-ai/haystack/actions" diff --git a/releasenotes/notes/recast-hitl-as-before-tool-hook-0ef6ba1502cf97ad.yaml b/releasenotes/notes/recast-hitl-as-before-tool-hook-0ef6ba1502cf97ad.yaml index e421ec2e0e3..709ea3c8fc2 100644 --- a/releasenotes/notes/recast-hitl-as-before-tool-hook-0ef6ba1502cf97ad.yaml +++ b/releasenotes/notes/recast-hitl-as-before-tool-hook-0ef6ba1502cf97ad.yaml @@ -6,12 +6,14 @@ upgrade: removed from ``Agent.__init__``, ``Agent.run``, and ``Agent.run_async``. To migrate, wrap your confirmation strategies in the new ``ConfirmationHook``, register it under the ``before_tool`` hook point, and pass any request-scoped resources through the generic ``hook_context`` run argument instead of - ``confirmation_strategy_context``: + ``confirmation_strategy_context``. The Human-in-the-Loop module has also moved from + ``haystack.human_in_the_loop`` to ``haystack.hooks.human_in_the_loop``, so that it lives alongside the other + built-in hooks (such as tool result offloading); update your imports to the new location: .. code-block:: python from haystack.components.agents import Agent - from haystack.human_in_the_loop import ( + from haystack.hooks.human_in_the_loop import ( AlwaysAskPolicy, BlockingConfirmationStrategy, ConfirmationHook, diff --git a/test/components/agents/test_agent_hitl.py b/test/components/agents/test_agent_hitl.py index 898c85d6997..e9dcaac1e69 100644 --- a/test/components/agents/test_agent_hitl.py +++ b/test/components/agents/test_agent_hitl.py @@ -12,7 +12,7 @@ from haystack.components.agents import Agent from haystack.components.generators.chat import OpenAIChatGenerator from haystack.dataclasses import ChatMessage, ToolCall -from haystack.human_in_the_loop import ( +from haystack.hooks.human_in_the_loop import ( AlwaysAskPolicy, BlockingConfirmationStrategy, ConfirmationHook, @@ -20,7 +20,7 @@ NeverAskPolicy, SimpleConsoleUI, ) -from haystack.human_in_the_loop.types import ConfirmationStrategy, ConfirmationUI +from haystack.hooks.human_in_the_loop.types import ConfirmationStrategy, ConfirmationUI from haystack.tools import Tool, Toolset, create_tool_from_function @@ -125,18 +125,20 @@ def test_to_dict(self, tools, confirmation_hook, monkeypatch): "hooks": { "before_tool": [ { - "type": "haystack.human_in_the_loop.hooks.ConfirmationHook", + "type": "haystack.hooks.human_in_the_loop.hooks.ConfirmationHook", "init_parameters": { "confirmation_strategies": { "addition_tool": { - "type": "haystack.human_in_the_loop.strategies.BlockingConfirmationStrategy", + "type": "haystack.hooks.human_in_the_loop.strategies." + "BlockingConfirmationStrategy", "init_parameters": { "confirmation_policy": { - "type": "haystack.human_in_the_loop.policies.NeverAskPolicy", + "type": "haystack.hooks.human_in_the_loop.policies.NeverAskPolicy", "init_parameters": {}, }, "confirmation_ui": { - "type": "haystack.human_in_the_loop.user_interfaces.SimpleConsoleUI", + "type": "haystack.hooks.human_in_the_loop.user_interfaces." + "SimpleConsoleUI", "init_parameters": {}, }, "reject_template": "Tool execution for '{tool_name}' was rejected by " diff --git a/test/hooks/human_in_the_loop/__init__.py b/test/hooks/human_in_the_loop/__init__.py new file mode 100644 index 00000000000..c1764a6e039 --- /dev/null +++ b/test/hooks/human_in_the_loop/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/test/human_in_the_loop/test_dataclasses.py b/test/hooks/human_in_the_loop/test_dataclasses.py similarity index 95% rename from test/human_in_the_loop/test_dataclasses.py rename to test/hooks/human_in_the_loop/test_dataclasses.py index daaabe1c032..21a2439106b 100644 --- a/test/human_in_the_loop/test_dataclasses.py +++ b/test/hooks/human_in_the_loop/test_dataclasses.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 -from haystack.human_in_the_loop import ConfirmationUIResult, ToolExecutionDecision +from haystack.hooks.human_in_the_loop import ConfirmationUIResult, ToolExecutionDecision class TestConfirmationUIResult: diff --git a/test/human_in_the_loop/test_hooks.py b/test/hooks/human_in_the_loop/test_hooks.py similarity index 96% rename from test/human_in_the_loop/test_hooks.py rename to test/hooks/human_in_the_loop/test_hooks.py index d7ddb4fc00d..97f6aff3042 100644 --- a/test/human_in_the_loop/test_hooks.py +++ b/test/hooks/human_in_the_loop/test_hooks.py @@ -10,7 +10,7 @@ from haystack.components.agents.state.state import State from haystack.components.agents.state.state_utils import merge_lists, replace_values from haystack.dataclasses import ChatMessage, ToolCall -from haystack.human_in_the_loop import ( +from haystack.hooks.human_in_the_loop import ( AlwaysAskPolicy, BlockingConfirmationStrategy, ConfirmationHook, @@ -19,7 +19,7 @@ SimpleConsoleUI, ToolExecutionDecision, ) -from haystack.human_in_the_loop.types import ConfirmationUI +from haystack.hooks.human_in_the_loop.types import ConfirmationUI from haystack.tools import Tool, create_tool_from_function @@ -159,18 +159,18 @@ def test_to_dict(self): } ) assert hook.to_dict() == { - "type": "haystack.human_in_the_loop.hooks.ConfirmationHook", + "type": "haystack.hooks.human_in_the_loop.hooks.ConfirmationHook", "init_parameters": { "confirmation_strategies": { "addition_tool": { - "type": "haystack.human_in_the_loop.strategies.BlockingConfirmationStrategy", + "type": "haystack.hooks.human_in_the_loop.strategies.BlockingConfirmationStrategy", "init_parameters": { "confirmation_policy": { - "type": "haystack.human_in_the_loop.policies.NeverAskPolicy", + "type": "haystack.hooks.human_in_the_loop.policies.NeverAskPolicy", "init_parameters": {}, }, "confirmation_ui": { - "type": "haystack.human_in_the_loop.user_interfaces.SimpleConsoleUI", + "type": "haystack.hooks.human_in_the_loop.user_interfaces.SimpleConsoleUI", "init_parameters": {}, }, "reject_template": "Tool execution for '{tool_name}' was rejected by the user.", diff --git a/test/human_in_the_loop/test_policies.py b/test/hooks/human_in_the_loop/test_policies.py similarity index 81% rename from test/human_in_the_loop/test_policies.py rename to test/hooks/human_in_the_loop/test_policies.py index 3865540fc3f..37cf65db861 100644 --- a/test/human_in_the_loop/test_policies.py +++ b/test/hooks/human_in_the_loop/test_policies.py @@ -4,7 +4,7 @@ import pytest -from haystack.human_in_the_loop import AlwaysAskPolicy, AskOncePolicy, ConfirmationUIResult, NeverAskPolicy +from haystack.hooks.human_in_the_loop import AlwaysAskPolicy, AskOncePolicy, ConfirmationUIResult, NeverAskPolicy from haystack.tools import Tool, create_tool_from_function @@ -25,11 +25,11 @@ def test_should_ask_always_true(self, addition_tool): def test_to_dict(self): policy = AlwaysAskPolicy() policy_dict = policy.to_dict() - assert policy_dict["type"] == "haystack.human_in_the_loop.policies.AlwaysAskPolicy" + assert policy_dict["type"] == "haystack.hooks.human_in_the_loop.policies.AlwaysAskPolicy" assert policy_dict["init_parameters"] == {} def test_from_dict(self): - policy_dict = {"type": "haystack.human_in_the_loop.policies.AlwaysAskPolicy", "init_parameters": {}} + policy_dict = {"type": "haystack.hooks.human_in_the_loop.policies.AlwaysAskPolicy", "init_parameters": {}} policy = AlwaysAskPolicy.from_dict(policy_dict) assert isinstance(policy, AlwaysAskPolicy) @@ -66,11 +66,11 @@ def test_should_ask_different_params_true(self, addition_tool): def test_to_dict(self): policy = AskOncePolicy() policy_dict = policy.to_dict() - assert policy_dict["type"] == "haystack.human_in_the_loop.policies.AskOncePolicy" + assert policy_dict["type"] == "haystack.hooks.human_in_the_loop.policies.AskOncePolicy" assert policy_dict["init_parameters"] == {} def test_from_dict(self): - policy_dict = {"type": "haystack.human_in_the_loop.policies.AskOncePolicy", "init_parameters": {}} + policy_dict = {"type": "haystack.hooks.human_in_the_loop.policies.AskOncePolicy", "init_parameters": {}} policy = AskOncePolicy.from_dict(policy_dict) assert isinstance(policy, AskOncePolicy) @@ -83,10 +83,10 @@ def test_should_ask_always_false(self, addition_tool): def test_to_dict(self): policy = NeverAskPolicy() policy_dict = policy.to_dict() - assert policy_dict["type"] == "haystack.human_in_the_loop.policies.NeverAskPolicy" + assert policy_dict["type"] == "haystack.hooks.human_in_the_loop.policies.NeverAskPolicy" assert policy_dict["init_parameters"] == {} def test_from_dict(self): - policy_dict = {"type": "haystack.human_in_the_loop.policies.NeverAskPolicy", "init_parameters": {}} + policy_dict = {"type": "haystack.hooks.human_in_the_loop.policies.NeverAskPolicy", "init_parameters": {}} policy = NeverAskPolicy.from_dict(policy_dict) assert isinstance(policy, NeverAskPolicy) diff --git a/test/human_in_the_loop/test_strategies.py b/test/hooks/human_in_the_loop/test_strategies.py similarity index 97% rename from test/human_in_the_loop/test_strategies.py rename to test/hooks/human_in_the_loop/test_strategies.py index a2ba260e42e..ef467249f07 100644 --- a/test/human_in_the_loop/test_strategies.py +++ b/test/hooks/human_in_the_loop/test_strategies.py @@ -10,7 +10,7 @@ from haystack.components.agents.state.state import State from haystack.components.agents.tool_calling import ToolNotFoundException, _run_tool from haystack.dataclasses import ChatMessage, ToolCall -from haystack.human_in_the_loop import ( +from haystack.hooks.human_in_the_loop import ( AlwaysAskPolicy, AskOncePolicy, BlockingConfirmationStrategy, @@ -19,7 +19,7 @@ SimpleConsoleUI, ToolExecutionDecision, ) -from haystack.human_in_the_loop.strategies import ( +from haystack.hooks.human_in_the_loop.strategies import ( _apply_tool_execution_decisions, _process_confirmation_strategies, _run_confirmation_strategies, @@ -60,14 +60,14 @@ def test_to_dict(self): strategy = BlockingConfirmationStrategy(confirmation_policy=AskOncePolicy(), confirmation_ui=SimpleConsoleUI()) strategy_dict = strategy.to_dict() assert strategy_dict == { - "type": "haystack.human_in_the_loop.strategies.BlockingConfirmationStrategy", + "type": "haystack.hooks.human_in_the_loop.strategies.BlockingConfirmationStrategy", "init_parameters": { "confirmation_policy": { - "type": "haystack.human_in_the_loop.policies.AskOncePolicy", + "type": "haystack.hooks.human_in_the_loop.policies.AskOncePolicy", "init_parameters": {}, }, "confirmation_ui": { - "type": "haystack.human_in_the_loop.user_interfaces.SimpleConsoleUI", + "type": "haystack.hooks.human_in_the_loop.user_interfaces.SimpleConsoleUI", "init_parameters": {}, }, "reject_template": "Tool execution for '{tool_name}' was rejected by the user.", @@ -79,14 +79,14 @@ def test_to_dict(self): def test_from_dict(self): strategy_dict = { - "type": "haystack.human_in_the_loop.strategies.BlockingConfirmationStrategy", + "type": "haystack.hooks.human_in_the_loop.strategies.BlockingConfirmationStrategy", "init_parameters": { "confirmation_policy": { - "type": "haystack.human_in_the_loop.policies.AskOncePolicy", + "type": "haystack.hooks.human_in_the_loop.policies.AskOncePolicy", "init_parameters": {}, }, "confirmation_ui": { - "type": "haystack.human_in_the_loop.user_interfaces.SimpleConsoleUI", + "type": "haystack.hooks.human_in_the_loop.user_interfaces.SimpleConsoleUI", "init_parameters": {}, }, }, diff --git a/test/human_in_the_loop/test_user_interfaces.py b/test/hooks/human_in_the_loop/test_user_interfaces.py similarity index 89% rename from test/human_in_the_loop/test_user_interfaces.py rename to test/hooks/human_in_the_loop/test_user_interfaces.py index d44c6e5f247..bb082d0bef6 100644 --- a/test/human_in_the_loop/test_user_interfaces.py +++ b/test/hooks/human_in_the_loop/test_user_interfaces.py @@ -6,7 +6,7 @@ import pytest -from haystack.human_in_the_loop import ConfirmationUIResult, RichConsoleUI, SimpleConsoleUI +from haystack.hooks.human_in_the_loop import ConfirmationUIResult, RichConsoleUI, SimpleConsoleUI from haystack.tools import create_tool_from_function @@ -26,7 +26,7 @@ class TestRichConsoleUI: def test_process_choice_confirm(self, tool, choice): ui = RichConsoleUI(console=MagicMock()) - with patch("haystack.human_in_the_loop.user_interfaces.Prompt.ask", side_effect=[choice, "feedback"]): + with patch("haystack.hooks.human_in_the_loop.user_interfaces.Prompt.ask", side_effect=[choice, "feedback"]): result = ui.get_user_confirmation(tool.name, tool.description, {"x": 1}) assert isinstance(result, ConfirmationUIResult) @@ -38,7 +38,7 @@ def test_process_choice_confirm(self, tool, choice): def test_process_choice_modify(self, tool, choice): ui = RichConsoleUI(console=MagicMock()) - with patch("haystack.human_in_the_loop.user_interfaces.Prompt.ask", side_effect=["m", "2"]): + with patch("haystack.hooks.human_in_the_loop.user_interfaces.Prompt.ask", side_effect=["m", "2"]): result = ui.get_user_confirmation(tool.name, tool.description, {"x": 1}) assert isinstance(result, ConfirmationUIResult) @@ -48,7 +48,9 @@ def test_process_choice_modify(self, tool, choice): def test_process_choice_modify_dict_param(self, tool): ui = RichConsoleUI(console=MagicMock()) - with patch("haystack.human_in_the_loop.user_interfaces.Prompt.ask", side_effect=["m", '{"key": "value"}']): + with patch( + "haystack.hooks.human_in_the_loop.user_interfaces.Prompt.ask", side_effect=["m", '{"key": "value"}'] + ): result = ui.get_user_confirmation(tool.name, tool.description, {"param1": {"old_key": "old_value"}}) assert isinstance(result, ConfirmationUIResult) @@ -59,7 +61,7 @@ def test_process_choice_modify_dict_param_invalid_json(self, tool): ui = RichConsoleUI(console=MagicMock()) with patch( - "haystack.human_in_the_loop.user_interfaces.Prompt.ask", + "haystack.hooks.human_in_the_loop.user_interfaces.Prompt.ask", side_effect=["m", "invalid_json", '{"key": "value"}'], ): result = ui.get_user_confirmation(tool.name, tool.description, {"param1": {"old_key": "old_value"}}) @@ -72,7 +74,7 @@ def test_process_choice_modify_dict_param_invalid_json(self, tool): def test_process_choice_reject(self, tool, choice): ui = RichConsoleUI(console=MagicMock()) - with patch("haystack.human_in_the_loop.user_interfaces.Prompt.ask", side_effect=["n", "Changed my mind"]): + with patch("haystack.hooks.human_in_the_loop.user_interfaces.Prompt.ask", side_effect=["n", "Changed my mind"]): result = ui.get_user_confirmation(tool.name, tool.description, {"x": 1}) assert isinstance(result, ConfirmationUIResult) @@ -82,7 +84,7 @@ def test_process_choice_reject(self, tool, choice): def test_to_dict(self): ui = RichConsoleUI() data = ui.to_dict() - assert data["type"] == ("haystack.human_in_the_loop.user_interfaces.RichConsoleUI") + assert data["type"] == ("haystack.hooks.human_in_the_loop.user_interfaces.RichConsoleUI") assert data["init_parameters"]["console"] is None def test_from_dict(self): @@ -181,7 +183,7 @@ def test_process_choice_no_tool_params_reject(self, tool): def test_to_dict(self): ui = SimpleConsoleUI() data = ui.to_dict() - assert data["type"] == ("haystack.human_in_the_loop.user_interfaces.SimpleConsoleUI") + assert data["type"] == ("haystack.hooks.human_in_the_loop.user_interfaces.SimpleConsoleUI") assert data["init_parameters"] == {} def test_from_dict(self):