Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Conversation

sjrl
Copy link
Contributor

@sjrl sjrl commented Sep 18, 2025

Related Issues

Proposed Changes:

This PR introduces a human-in-the-loop (HITL) mechanism for tool execution in Agent, allowing users to confirm, reject, or modify tool invocations interactively.

TODOs

  • Add unit and integration tests
    • Double check tools that expect no params work as expected
  • Update README with all new classes/features

Key Changes

  • haystack_experimental/components/agents/human_in_the_loop/

    • dataclasses.py

      • Adds ConfirmationUIResult and ToolExecutionDecision dataclasses to standardize user feedback and tool execution decisions
    • policies.py

      • Implements ConfirmationPolicy base class and concrete policies: AlwaysAskPolicy, NeverAskPolicy, and AskOncePolicy
    • user_interfaces.py

      • Provides ConfirmationUI base class and two implementations: RichConsoleUI (using Rich library) and SimpleConsoleUI (using standard input), supporting interactive user feedback and parameter modification.
      • These UI's are thread locked so in the case of parallel tool execution that the confirmations are still run sequentially. Look at hitl_multi_agent_example.py to see a case where this occurs.
    • protocol.py

      • Defines the ConfirmationStrategy protocol. I'm not 100% if this should be a protocol or a base class.
    • strategies.py

      • Introduces HumanInTheLoopStrategy to orchestrate the confirmation policy and the confirmation UI, returning a ToolExecutionDecision.
  • haystack_experimental/components/tools/tool_invoker.py

    • Extends ToolInvoker to support per-tool confirmation strategies.
    • Integrates HITL logic before tool execution, handling all user feedback and parameter modification before calling tools.
  • haystack_experimental/components/agents/agent.py

    • Modified the init method of Agent use the new experimental ToolInvoker and added the confirmation_strategies explicitly.

How did you test it?

Provided an example script called hitl_intro_example.py which shows how to use these new utility functions to convert existing tools into ones that ask for confirmation. I've also reproduced it here

Single Agent Example highlighting different Confirmation Policies and Confirmation UIs
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0

from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.dataclasses import ChatMessage
from haystack.tools import create_tool_from_function
from rich.console import Console

from haystack_experimental.components.agents.agent import Agent
from haystack_experimental.components.agents.human_in_the_loop.policies import (
    AlwaysAskPolicy,
    AskOncePolicy,
    NeverAskPolicy,
)
from haystack_experimental.components.agents.human_in_the_loop.user_interfaces import (
    RichConsoleUI,
    SimpleConsoleUI,
)
from haystack_experimental.components.agents.human_in_the_loop.strategies import HumanInTheLoopStrategy


def addition(a: float, b: float) -> float:
    """
    A simple addition function.

    :param a: First float.
    :param b: Second float.
    :returns:
        Sum of a and b.
    """
    return a + b


addition_tool = create_tool_from_function(
    function=addition,
    name="addition",
    description="Add two floats together.",
)


def get_bank_balance(account_id: str) -> str:
    """
    Simulate fetching a bank balance for a given account ID.

    :param account_id: The ID of the bank account.
    :returns:
        A string representing the bank balance.
    """
    return f"Balance for account {account_id} is $1,234.56"


balance_tool = create_tool_from_function(
    function=get_bank_balance,
    name="get_bank_balance",
    description="Get the bank balance for a given account ID.",
)


def get_phone_number(name: str) -> str:
    """
    Simulate fetching a phone number for a given name.

    :param name: The name of the person.
    :returns:
        A string representing the phone number.
    """
    return f"The phone number for {name} is (123) 456-7890"


phone_tool = create_tool_from_function(
    function=get_phone_number,
    name="get_phone_number",
    description="Get the phone number for a given name.",
)

# Define shared console
cons = Console()

# Define Main Agent with multiple tools and different confirmation strategies
agent = Agent(
    chat_generator=OpenAIChatGenerator(model="gpt-4.1"),
    tools=[balance_tool, addition_tool, phone_tool],
    system_prompt="You are a helpful financial assistant. Use the provided tool to get bank balances when needed.",
    confirmation_strategies={
        balance_tool.name: HumanInTheLoopStrategy(
            confirmation_policy=AlwaysAskPolicy(), confirmation_ui=RichConsoleUI(console=cons)
        ),
        addition_tool.name: HumanInTheLoopStrategy(
            confirmation_policy=NeverAskPolicy(), confirmation_ui=SimpleConsoleUI()
        ),
        phone_tool.name: HumanInTheLoopStrategy(
            confirmation_policy=AskOncePolicy(), confirmation_ui=SimpleConsoleUI()
        ),
    },
)

# Call bank tool with confirmation (Always Ask) using RichConsoleUI
result = agent.run([ChatMessage.from_user("What's the balance of account 56789?")])
last_message = result["last_message"]
cons.print(f"\n[bold green]Agent Result:[/bold green] {last_message.text}")

# Call addition tool with confirmation (Never Ask)
result = agent.run([ChatMessage.from_user("What is 5.5 + 3.2?")])
last_message = result["last_message"]
print(f"\nAgent Result: {last_message.text}")

# Call phone tool with confirmation (Ask Once) using SimpleConsoleUI
result = agent.run([ChatMessage.from_user("What is the phone number of Alice?")])
last_message = result["last_message"]
print(f"\nAgent Result: {last_message.text}")

# Call phone tool again to see that it doesn't ask for confirmation the second time
result = agent.run([ChatMessage.from_user("What is the phone number of Alice?")])
last_message = result["last_message"]
print(f"\nAgent Result: {last_message.text}")
Multi-Agent Example: Tools within Sub-Agents use HiTL Confirmation
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0

from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.dataclasses import ChatMessage
from haystack.tools import ComponentTool, create_tool_from_function
from rich.console import Console

from haystack_experimental.components.agents.agent import Agent
from haystack_experimental.components.agents.human_in_the_loop.policies import AlwaysAskPolicy
from haystack_experimental.components.agents.human_in_the_loop.strategies import HumanInTheLoopStrategy
from haystack_experimental.components.agents.human_in_the_loop.user_interfaces import RichConsoleUI


def addition(a: float, b: float) -> float:
    """
    A simple addition function.

    :param a: First float.
    :param b: Second float.
    :returns:
        Sum of a and b.
    """
    return a + b


addition_tool = create_tool_from_function(
    function=addition,
    name="addition",
    description="Add two floats together.",
)


def get_bank_balance(account_id: str) -> str:
    """
    Simulate fetching a bank balance for a given account ID.

    :param account_id: The ID of the bank account.
    :returns:
        A string representing the bank balance.
    """
    return f"Balance for account {account_id} is $1,234.56"


balance_tool = create_tool_from_function(
    function=get_bank_balance,
    name="get_bank_balance",
    description="Get the bank balance for a given account ID.",
)

# Define shared console for all UIs
cons = Console()

# Define Bank Sub-Agent
bank_agent = Agent(
    chat_generator=OpenAIChatGenerator(model="gpt-4.1"),
    tools=[balance_tool],
    system_prompt="You are a helpful financial assistant. Use the provided tool to get bank balances when needed.",
    confirmation_strategies={
        balance_tool.name: HumanInTheLoopStrategy(
            confirmation_policy=AlwaysAskPolicy(), confirmation_ui=RichConsoleUI(console=cons)
        ),
    },
)
bank_agent_tool = ComponentTool(
    component=bank_agent,
    name="bank_agent_tool",
    description="A bank agent that can get bank balances.",
)

# Define Math Sub-Agent
math_agent = Agent(
    chat_generator=OpenAIChatGenerator(model="gpt-4.1"),
    tools=[addition_tool],
    system_prompt="You are a helpful math assistant. Use the provided tool to perform addition when needed.",
    confirmation_strategies={
        addition_tool.name: HumanInTheLoopStrategy(
            # We use AlwaysAskPolicy here for demonstration; in real scenarios, you might choose NeverAskPolicy
            confirmation_policy=AlwaysAskPolicy(),
            confirmation_ui=RichConsoleUI(console=cons),
        ),
    },
)
math_agent_tool = ComponentTool(
    component=math_agent,
    name="math_agent_tool",
    description="A math agent that can perform addition.",
)

# Define Main Agent with Sub-Agents as tools
planner_agent = Agent(
    chat_generator=OpenAIChatGenerator(model="gpt-4.1"),
    tools=[bank_agent_tool, math_agent_tool],
    system_prompt="""You are a master agent that can delegate tasks to sub-agents based on the user's request.
Available sub-agents:
- bank_agent_tool: A bank agent that can get bank balances.
- math_agent_tool: A math agent that can perform addition.
Use the appropriate sub-agent to handle the user's request.
""",
)

# Make bank balance request to planner agent
result = planner_agent.run([ChatMessage.from_user("What's the balance of account 56789?")])
last_message = result["last_message"]
cons.print(f"\n[bold green]Agent Result:[/bold green] {last_message.text}")

# Make addition request to planner agent
result = planner_agent.run([ChatMessage.from_user("What is 5.5 + 3.2?")])
last_message = result["last_message"]
print(f"\nAgent Result: {last_message.text}")

# Make bank balance request and addition request to planner agent
# NOTE: This will try and invoke both sub-agents in parallel requiring a thread-safe UI
result = planner_agent.run([ChatMessage.from_user("What's the balance of account 56789 and what is 5.5 + 3.2?")])
last_message = result["last_message"]
print(f"\nAgent Result: {last_message.text}")

Notes for the reviewer

@julian-risch this now follows more closely your idea of passing the tool confirmation strategies to the Agent (or ToolInvoker) rather than applying it at the tool level. I think I like this design better since it has better separation and it makes the various policies easy to understand.

Checklist

Some renaming and example how I'd like the interface to look

Alternative implementation

formatting

Continue work on alternative

Fixes

Add more examples

Change name of file
@coveralls
Copy link

coveralls commented Sep 18, 2025

Pull Request Test Coverage Report for Build 18378714213

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+1.0%) to 70.439%

Totals Coverage Status
Change from base Build 18361480556: 1.0%
Covered Lines: 1058
Relevant Lines: 1502

💛 - Coveralls

haystack_experimental/components/tools/tool_invoker.py Outdated Show resolved Hide resolved
haystack_experimental/components/tools/tool_invoker.py Outdated Show resolved Hide resolved
haystack_experimental/components/agents/agent.py Outdated Show resolved Hide resolved
Copy link
Member

@anakin87 anakin87 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some high-level comments:

  • Skimming other frameworks, it doesn't seem like there's a single right way to do HITL (Tool level or Agent level?). If this approach also helps with serialization headaches, it could be a good direction.

  • I see a design tension between giving users freedom to provide their own ConfirmationStrategy (there is a protocol for this and ConfirmationResult.action is a string) and requiring a specific stricter strategy (the current implementation of _handle_confirmation_strategies only expects confirm/reject/modify). If we want to let users free to implement their strategies, I would suggest delegating most of the logic in _handle_confirmation_strategies to the ConfirmationStrategy implementation. (Otherwise, we could be stricter in defining ConfirmationResult and removing the protocol...)

  • about names: maybe we can rename UserInterfaceConfirmationUI but if we go with the approach proposed in this PR, I recommend checking in with Bilge.

hitl_intro_example.py Show resolved Hide resolved
haystack_experimental/components/tools/tool_invoker.py Outdated Show resolved Hide resolved
@sjrl sjrl changed the title feat: Add Human in the Loop utilities for tool execution in Agent feat: Add Human in the Loop confirmation strategies for tool execution in Agent Oct 6, 2025
)

* Exploring supporting hitl using breakpoints

* Add example script

* Update example so using only break points to stop and start execution works

* Keep working on the examples

* Updating example

* Making progress, the confirm, and modify both work now. The rejection option still has a problem.

* Get example to work

* Work on some of the TODOs

* Add tool_id to ToolExecutionDecision

* Use the serialized chat messages instead of just the tool calls

* Minor formatting update

* Refactoring

* Refactor to move confirmation strategy logic to Agent instead of ToolInvoker

* More refactoring and prep for getting BreakpointConfirmationStrategy to work

* Add missing run_async to Agent and remove unused test file

* Add missing import

* First version of HiTL + Breakpoints is working!

* Some refactoring and adding a TODO

* Refactoring example script to be a bit more robust

* Fixed some bugs

* Some cleanup

* Rename file

* Fix a bug to get multiple sequential tool calls to work

* Cleanup

* Cleanup and formatting

* Cleanup and refactoring

* Refactoring

* More refactoring

* More refactoring and updated _get_tool_calls_and_descriptions properly creates the final set of tool arguments

* Refactoring based on comments

* More cleanup

* Update example

* Formatting

* fix license header

* Fix sede bug

* PR comments and simplification

* Do more monkey patching

* PR comments

* PR comments

* Refactoring and cleanup of utilities in strategies.py

* Fix issue. Use tool name as fall back to match tool execution decision to tool call

* ignore pylint import issues

* Resolve todo

* Two tool call question now works as expected

* Fixing typing and formatting

* Formatting

* Update readme and docs

* remove tool invoker docs since not needed anymore

* Add some integration tests for Agent using confirmation strategies

* Add more unit tests

* Update haystack_experimental/components/agents/human_in_the_loop/breakpoint.py

Co-authored-by: Stefano Fiorucci <stefanofiorucci@gmail.com>

* Refactoring based on PR comments

* formatting and types

---------

Co-authored-by: Stefano Fiorucci <stefanofiorucci@gmail.com>
@sjrl sjrl requested a review from anakin87 October 8, 2025 13:16
@anakin87
Copy link
Member

anakin87 commented Oct 8, 2025

  • Can we increase test coverage?
  • I'd appreciate it if also @julian-risch could take a look...

@@ -0,0 +1,186 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mpangrazzi as we discussed, I'd appreciate it if you could take a look at this script (from the point of view of integrate HiTL in Hayhooks)

@anakin87 anakin87 requested a review from mpangrazzi October 8, 2025 15:24
@sjrl sjrl marked this pull request as ready for review October 9, 2025 13:58
@sjrl sjrl requested review from a team as code owners October 9, 2025 13:58
@sjrl sjrl requested review from dfokina and removed request for a team October 9, 2025 13:58
@sjrl
Copy link
Contributor Author

sjrl commented Oct 9, 2025

  • Can we increase test coverage?
  • I'd appreciate it if also @julian-risch could take a look...

@anakin87 I have increased the test coverage specifically focusing on the human_in_the_loop folder. I've also added integration tests for Agent using confirmation strategies (both blocking and breakpoint confirmation strategies).

Copy link
Member

@anakin87 anakin87 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my point of view, this PR looks good. Thanks for adding the tests!

I'd still appreciate reviews from others, especially to confirm that the BreakpointConfirmationStrategy (which introduces much complexity) truly unlocks the Hayhooks/platform use cases.

@sjrl
Copy link
Contributor Author

sjrl commented Oct 10, 2025

From my point of view, this PR looks good. Thanks for adding the tests!

I'd still appreciate reviews from others, especially to confirm that the BreakpointConfirmationStrategy (which introduces much complexity) truly unlocks the Hayhooks/platform use cases.

From my conversation with @mpangrazzi the ability to pass a callback to be triggered by a tool call (basically what the ConfirmationStrategy is) that is capable of pausing the execution of an Agent is necessary to get this working in a hayhooks context. Whether BreakpointConfirmationStrategy can be directly used by hayhooks is another question and it is possible that will need to be updated/changed to fully integrate.

@mpangrazzi
Copy link
Contributor

@sjrl @anakin87 It should be possible, but IMHO the best way to be sure that this works would be to try it out and check how clean is the output implementation.

We may want to try to e.g.:

  1. Execute an agent in a pipeline wrapper
  2. Catch and handle breakpoint exceptions
  3. Send (via websockets using AG-UI?) tool approval data to frontend and receive result
  4. Based on received feedback, resume the execution

We can also skip using AG-UI on point 3, but we still need to have websockets support on pipeline wrappers (to be able to do a bi-directional communication). I'll try to find some time next week to try it out.

@sjrl
Copy link
Contributor Author

sjrl commented Oct 10, 2025

@sjrl @anakin87 It should be possible, but IMHO the best way to be sure that this works would be to try it out and check how clean is the output implementation.

We may want to try to e.g.:

  1. Execute an agent in a pipeline wrapper
  2. Catch and handle breakpoint exceptions
  3. Send (via websockets using AG-UI?) tool approval data to frontend and receive result
  4. Based on received feedback, resume the execution

We can also skip using AG-UI on point 3, but we still need to have websockets support on pipeline wrappers (to be able to do a bi-directional communication). I'll try to find some time next week to try it out.

Sounds good! @mpangrazzi and @anakin87 I'll go ahead and merge this then and we can create follow-up PRs/issues if we find that we need to make changes to get this to work with hayhooks

@anakin87
Copy link
Member

I'll go ahead and merge this then and we can create follow-up PRs/issues if we find that we need to make changes to get this to work with hayhooks

👍

@sjrl sjrl merged commit 5bc99c2 into main Oct 10, 2025
10 checks passed
@sjrl sjrl deleted the hitl-alternative branch October 10, 2025 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.