Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions docs/tutorial/en/src/task_human_in_the_loop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# -*- coding: utf-8 -*-
"""
.. _human-in-the-loop:

Human-in-the-Loop Tool Calling
==============================

In many production scenarios, we want a human to **review or edit tool calls
before execution**, e.g. running shell commands, executing arbitrary Python
code, or invoking external MCP services.

AgentScope provides a Human-in-the-Loop mechanism for tools via the
``human_permit_func`` callback in ``Toolkit``, which allows you to:

- add human review for local tools (e.g. ``execute_shell_command``,
``execute_python_code``),
- add human review for MCP tools,
- let the user **edit tool name and parameters** before execution.

.. tip::
The full runnable example is available in
``examples/functionality/human_in_the_loop/``.

Basic concept
-------------------------

Both ``Toolkit.register_tool_function`` and ``Toolkit.register_mcp_client``
accept an optional ``human_permit_func`` parameter with the following signature:

.. code-block:: python

from agentscope.message import ToolUseBlock

def human_permit_function(tool_call: ToolUseBlock) -> bool:
\"\"\"This function is called right before a tool is executed.

Returns:
- True: permit the tool call
- False: reject the tool call

It can also modify ``tool_call['name']`` and ``tool_call['input']`` in
place to implement pre-editing of the tool name and arguments.
Note that ``tool_call['id']`` **must not** be modified, as it is used
internally to track the tool call.
\"\"\"
...

When an agent decides to call a tool, the corresponding ``ToolUseBlock`` is
first passed into ``human_permit_function``. The tool will be executed only if
the function returns ``True``.

The following implementation is consistent with the example:

.. code-block:: python

from agentscope.message import ToolUseBlock

def human_permit_function(tool_call: ToolUseBlock) -> bool:
arg_name_dict = {
"execute_python_code": "code",
"execute_shell_command": "command",
"add_one": "a",
}
option = None
while option not in ["y", "n", "e"]:
option = (
input(
"Enter 'y' for agreement, 'n' for refusal, "
"'e' to modify execution parameters: ",
)
.strip()
.lower()
)

if option == "y": # execute as-is
return True
if option == "n": # refuse
return False

# edit mode: allow user to change tool name and arguments
expected_tool_name = ""
while expected_tool_name not in [
"execute_python_code",
"execute_shell_command",
"add_one",
]:
expected_tool_name = input(
"Enter the expected tool name registered in the toolkit, "
"available options: "
"execute_python_code, execute_shell_command, add_one: ",
).strip()

expected_tool_args = input(
f"Enter {arg_name_dict[expected_tool_name]} "
f"for {expected_tool_name}: ",
) # your code or command

# modify the tool_call in place
tool_call["name"] = expected_tool_name
tool_call["input"].clear()
tool_call["input"][arg_name_dict[expected_tool_name]] = expected_tool_args
return True

With local tools
-------------------------

For local tools such as ``execute_python_code`` and ``execute_shell_command``,
you can pass the same ``human_permit_function`` when registering them so that
all tools share a unified Human-in-the-Loop review process:

.. code-block:: python

from agentscope.tool import (
Toolkit,
execute_shell_command,
execute_python_code,
)

toolkit = Toolkit()
toolkit.register_tool_function(
execute_shell_command,
human_permit_func=human_permit_function,
)
toolkit.register_tool_function(
execute_python_code,
human_permit_func=human_permit_function,
)

With MCP tools
-------------------------

``Toolkit`` also supports specifying ``human_permit_func`` for MCP tools.
In ``examples/functionality/human_in_the_loop/main.py``, a local MCP server is
connected via ``HttpStatefulClient`` and its tools are registered into
``Toolkit``:

.. code-block:: python

from agentscope.mcp import HttpStatefulClient

add_mcp_client = HttpStatefulClient(
name="mcp_add_one",
transport="sse",
url="http://127.0.0.1:8001/sse",
)

await add_mcp_client.connect()
await toolkit.register_mcp_client(
add_mcp_client,
human_permit_func=human_permit_function,
)

In this way, **both local tools and MCP tools** will go through the same
``human_permit_function`` before execution, implementing a unified
Human-in-the-Loop policy.
"""
150 changes: 150 additions & 0 deletions docs/tutorial/zh_CN/src/task_human_in_the_loop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
"""
.. _human-in-the-loop:

Human-in-the-Loop 工具调用
=========================

在许多生产场景中,我们希望在 **执行工具之前** 让人类先审核或修改调用参数,
例如运行 shell 命令、执行任意 Python 代码、调用外部 MCP 服务等。

AgentScope 通过 ``Toolkit`` 的 ``human_permit_func`` 回调,提供了「人类在环」
(Human-in-the-Loop)的工具调用能力,包括:

- 对本地工具(如 ``execute_shell_command``、``execute_python_code``)加审
- 对 MCP 工具统一加审
- 让用户在运行前 **修改工具名和参数**

.. tip::
完整可运行示例见仓库中的
``examples/functionality/human_in_the_loop/`` 目录。

基本概念
-------------------------

在 ``Toolkit.register_tool_function`` 与 ``Toolkit.register_mcp_client`` 中可以传入
可选参数 ``human_permit_func``,其签名形如:

.. code-block:: python

from agentscope.message import ToolUseBlock

def human_permit_function(tool_call: ToolUseBlock) -> bool:
\"\"\"在工具实际执行前被调用。

返回值:
- True: 允许执行工具
- False: 拒绝执行

也可以在函数内部原地修改 ``tool_call['name']`` 和 ``tool_call['input']``,
从而实现「预编辑」工具名与参数。
注意 ``tool_call['id']`` 字段 **不能** 被修改,它在内部用于标识工具调用。
\"\"\"
...

当智能体计划调用某个工具时,对应的 ``ToolUseBlock`` 会先被传入
``human_permit_function``,只有当它返回 ``True`` 时,工具才会真正执行。

与示例代码保持一致的一个实现如下:

.. code-block:: python

from agentscope.message import ToolUseBlock

def human_permit_function(tool_call: ToolUseBlock) -> bool:
arg_name_dict = {
"execute_python_code": "code",
"execute_shell_command": "command",
"add_one": "a",
}
option = None
while option not in ["y", "n", "e"]:
option = (
input(
"Enter 'y' for agreement, 'n' for refusal, "
"'e' to modify execution parameters: ",
)
.strip()
.lower()
)

if option == "y": # 正常执行
return True
if option == "n": # 拒绝执行
return False

# 进入编辑模式,允许用户修改工具名和参数
expected_tool_name = ""
while expected_tool_name not in [
"execute_python_code",
"execute_shell_command",
"add_one",
]:
expected_tool_name = input(
"Enter the expected tool name registered in the toolkit, "
"available options: "
"execute_python_code, execute_shell_command, add_one: ",
).strip()

expected_tool_args = input(
f"Enter {arg_name_dict[expected_tool_name]} "
f"for {expected_tool_name}: ",
) # your code or command

# 原地修改 tool_call
tool_call["name"] = expected_tool_name
tool_call["input"].clear()
tool_call["input"][arg_name_dict[expected_tool_name]] = expected_tool_args
return True

结合本地工具
-------------------------

对于本地工具(如 ``execute_python_code``、``execute_shell_command``),只需在
注册时传入相同的 ``human_permit_function`` 即可让所有工具统一走「人类在环」
审核流程:

.. code-block:: python

from agentscope.tool import (
Toolkit,
execute_shell_command,
execute_python_code,
)

toolkit = Toolkit()
toolkit.register_tool_function(
execute_shell_command,
human_permit_func=human_permit_function,
)
toolkit.register_tool_function(
execute_python_code,
human_permit_func=human_permit_function,
)

结合 MCP 工具
-------------------------

``Toolkit`` 同样支持为 MCP 工具指定 ``human_permit_func``。例如,在
``examples/functionality/human_in_the_loop/main.py`` 中,通过
``HttpStatefulClient`` 连接本地 MCP 服务器,并将其工具注册到 ``Toolkit``:

.. code-block:: python

from agentscope.mcp import HttpStatefulClient

add_mcp_client = HttpStatefulClient(
name="mcp_add_one",
transport="sse",
url="http://127.0.0.1:8001/sse",
)

await add_mcp_client.connect()
await toolkit.register_mcp_client(
add_mcp_client,
human_permit_func=human_permit_function,
)

此时,无论是本地工具还是 MCP 工具,它们的每一次调用都会先经过
``human_permit_function`` 审核,从而实现统一的「人类在环」策略。
"""
88 changes: 88 additions & 0 deletions examples/functionality/human_in_the_loop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Human-in-the-Loop MCP in AgentScope

This example demonstrates how to:

- create a ReAct agent with tools that require **human approval** before execution,
- connect to an **MCP (Model Context Protocol)** server via SSE and register its tools into a `Toolkit`,
- use a **human-in-the-loop permit function** to approve, deny, or modify tool calls (including MCP tools) at runtime.

The agent will use:

- local tools: `execute_shell_command`, `execute_python_code`
- an MCP tool: `add_one` (provided by the `mcp_add_one.py` server)

For every tool call, the user is asked whether to:

- run the tool as-is,
- refuse the call, or
- edit the tool name and/or parameters before running.

## Prerequisites

- Python 3.10 or higher
- DashScope API key from Alibaba Cloud (`DASHSCOPE_API_KEY` in your environment)

## Installation

### Install AgentScope

```bash
# Install from source
cd {PATH_TO_AGENTSCOPE}
pip install -e .
```

Or install from PyPI:

```bash
pip install agentscope
```

## QuickStart

1. **Set your DashScope API key** in the environment:

```bash
# Linux / macOS
export DASHSCOPE_API_KEY=YOUR_API_KEY

# Windows (PowerShell)
setx DASHSCOPE_API_KEY "YOUR_API_KEY"
```

2. **Start the MCP server** (SSE transport) in one terminal:

```bash
cd {PATH_TO_AGENTSCOPE}/examples/functionality/human_in_the_loop
python mcp_add_one.py
```

This will start an MCP server on `http://127.0.0.1:8001` exposing the `add_one` tool.

3. **Run the human-in-the-loop agent** in another terminal:

```bash
cd {PATH_TO_AGENTSCOPE}/examples/functionality/human_in_the_loop
python main.py
```

## What the Example Does

When you run `main.py`, the example will:

1. **Create a `Toolkit`** and register:
- `execute_shell_command` (local tool, guarded by human approval),
- `execute_python_code` (local tool, guarded by human approval),
- `add_one` from the MCP server via a stateful `HttpStatefulClient` (also guarded by human approval).
2. **Create a ReAct agent** named `Friday` using the DashScope chat model (`qwen-max`) with streaming output.
3. **Send two user messages** to the agent:
- Ask for the version of AgentScope (using Python execution),
- Request: “add 11 and 1 using add_one tool”.
4. **For each tool call**, prompt you in the console to:
- enter `y` to approve and run the tool as-is,
- enter `n` to refuse the tool call,
- enter `e` to edit the tool name and/or its parameters before execution.

This showcases how to keep a human in the loop for **all tool executions**, including tools coming from an external MCP server.


Loading