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):