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
79 changes: 79 additions & 0 deletions examples/functionality/return_direct/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Return Direct

This example demonstrates the `return_direct` feature for tool functions in AgentScope.

## What is Return Direct?

When a tool has `return_direct=True`, its output will be returned directly to the user without further LLM processing. The agent will stop the reasoning-acting loop immediately after the tool execution.

This is useful when:
- The tool output is already user-friendly and doesn't need LLM interpretation
- You want to reduce latency by skipping the LLM reasoning step
- The tool performs a final action that should end the conversation loop

## Usage

### Method 1: Set at Registration Time

```python
from agentscope.tool import Toolkit, ToolResponse
from agentscope.message import TextBlock

def get_weather(city: str) -> ToolResponse:
"""Get weather for a city."""
return ToolResponse(
content=[TextBlock(type="text", text=f"Weather in {city}: Sunny, 25°C")],
)

toolkit = Toolkit()
toolkit.register_tool_function(
get_weather,
return_direct=True, # Output returns directly to user
)
```

### Method 2: Set Dynamically in ToolResponse

```python
def search_database(query: str, return_immediately: bool = False) -> ToolResponse:
"""Search database with optional direct return."""
results = f"Found results for '{query}'"
return ToolResponse(
content=[TextBlock(type="text", text=results)],
return_direct=return_immediately, # Dynamic control
)
```

### Method 3: For MCP Clients

```python
await toolkit.register_mcp_client(
mcp_client,
return_direct=True, # Apply to all tools from this client
)

# Or specify per-tool
await toolkit.register_mcp_client(
mcp_client,
return_direct={
"tool_a": True,
"tool_b": False,
},
)
```

## How to Run

```bash
export DASHSCOPE_API_KEY="your_api_key"
python main.py
```

## Example Interaction

```
User: What's the weather in Beijing?
Assistant: Beijing: Sunny, 25°C, Humidity 40%
```

When `return_direct=True`, the weather result is returned directly without the LLM adding extra commentary.
170 changes: 170 additions & 0 deletions examples/functionality/return_direct/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
"""Example demonstrating the return_direct feature for tool functions.

The `return_direct` feature allows tool functions to return their output
directly to the user without further LLM processing. This is useful when:
- The tool output is already user-friendly and doesn't need LLM interpretation
- You want to reduce latency by skipping the LLM reasoning step
- The tool performs a final action that should end the conversation loop
"""
import asyncio
import os

from agentscope.agent import ReActAgent, UserAgent
from agentscope.formatter import DashScopeChatFormatter
from agentscope.memory import InMemoryMemory
from agentscope.model import DashScopeChatModel
from agentscope.tool import Toolkit, ToolResponse
from agentscope.message import TextBlock


# Example 1: Tool with return_direct=True at registration time
def get_weather(city: str) -> ToolResponse:
"""Get the current weather for a city.

This tool returns weather information directly to the user without
further LLM processing.

Args:
city: The name of the city to get weather for.

Returns:
ToolResponse with weather information.
"""
# Simulated weather data
weather_data = {
"beijing": "Beijing: Sunny, 25°C, Humidity 40%",
"shanghai": "Shanghai: Cloudy, 22°C, Humidity 65%",
"guangzhou": "Guangzhou: Rainy, 28°C, Humidity 85%",
"shenzhen": "Shenzhen: Partly Cloudy, 27°C, Humidity 70%",
}

city_lower = city.lower()
if city_lower in weather_data:
result = weather_data[city_lower]
else:
result = f"Weather data not available for {city}"

return ToolResponse(
content=[
TextBlock(
type="text",
text=result,
),
],
)


# Example 2: Tool that dynamically sets return_direct in ToolResponse
def search_database(query: str, return_immediately: bool = False) -> ToolResponse:
"""Search the database for information.

This tool can optionally return results directly to the user by setting
return_immediately=True.

Args:
query: The search query.
return_immediately: If True, return results directly without LLM
processing.

Returns:
ToolResponse with search results.
"""
# Simulated database search
results = f"Found 3 results for '{query}':\n1. Result A\n2. Result B\n3. Result C"

return ToolResponse(
content=[
TextBlock(
type="text",
text=results,
),
],
# Dynamically set return_direct based on the parameter
return_direct=return_immediately,
)


# Example 3: Regular tool without return_direct (for comparison)
def calculate(expression: str) -> ToolResponse:
"""Calculate a mathematical expression.

This tool returns the result to the LLM for further processing/explanation.

Args:
expression: The mathematical expression to evaluate.

Returns:
ToolResponse with calculation result.
"""
try:
# Note: In production, use a safer evaluation method
result = eval(expression) # noqa: S307
text = f"Result: {expression} = {result}"
except Exception as e:
text = f"Error calculating '{expression}': {e}"

return ToolResponse(
content=[
TextBlock(
type="text",
text=text,
),
],
)


async def main() -> None:
"""The main entry point demonstrating return_direct feature."""
toolkit = Toolkit()

# Register tool with return_direct=True at registration time
# When this tool is called, its output will be returned directly to user
toolkit.register_tool_function(
get_weather,
return_direct=True, # <-- Key parameter!
)

# Register tool without return_direct at registration,
# but it can set return_direct dynamically in ToolResponse
toolkit.register_tool_function(search_database)

# Register regular tool for comparison
toolkit.register_tool_function(calculate)

agent = ReActAgent(
name="Friday",
sys_prompt="You are a helpful assistant named Friday.",
model=DashScopeChatModel(
api_key=os.environ.get("DASHSCOPE_API_KEY"),
model_name="qwen-max",
enable_thinking=False,
stream=True,
),
formatter=DashScopeChatFormatter(),
toolkit=toolkit,
memory=InMemoryMemory(),
)

user = UserAgent("User")

print("=" * 60)
print("Return Direct Feature Demo")
print("=" * 60)
print("\nTry these commands:")
print(" - 'What's the weather in Beijing?' (return_direct=True)")
print(" - 'Search for python tutorials' (normal flow)")
print(" - 'Calculate 2 + 3 * 4' (normal flow with LLM explanation)")
print(" - Type 'exit' to quit")
print("=" * 60)

msg = None
while True:
msg = await user(msg)
if msg.get_text_content() == "exit":
break
msg = await agent(msg)


if __name__ == "__main__":
asyncio.run(main())
56 changes: 52 additions & 4 deletions src/agentscope/agent/_react_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,11 +324,35 @@ async def reply( # pylint: disable=too-many-branches
# Sequential tool calls
structured_outputs = [await _ for _ in futures]

# -------------- Check for return_direct --------------
# Check if any tool returned with return_direct flag
for output in structured_outputs:
if (
isinstance(output, dict)
and output.get("_return_direct", False)
):
# Return the tool result directly to the user
reply_msg = Msg(
self.name,
output.get("_return_direct_content", []),
"assistant",
)
break

# Exit the loop if return_direct was triggered
if reply_msg is not None:
break

# -------------- Check for exit condition --------------
# If structured output is still not satisfied
if self._required_structured_model:
# Remove None results
structured_outputs = [_ for _ in structured_outputs if _]
# Remove None results and return_direct results
structured_outputs = [
_ for _ in structured_outputs
if _ is not None and not (
isinstance(_, dict) and _.get("_return_direct", False)
)
]

msg_hint = None
# If the acting step generates structured outputs
Expand Down Expand Up @@ -522,7 +546,9 @@ async def _reasoning(

async def _acting(self, tool_call: ToolUseBlock) -> dict | None:
"""Perform the acting process, and return the structured output if
it's generated and verified in the finish function call.
it's generated and verified in the finish function call, or return
a special dict with '_return_direct' key if the tool has return_direct
enabled.

Args:
tool_call (`ToolUseBlock`):
Expand All @@ -531,7 +557,9 @@ async def _acting(self, tool_call: ToolUseBlock) -> dict | None:
Returns:
`Union[dict, None]`:
Return the structured output if it's verified in the finish
function call, otherwise return None.
function call, or a dict with '_return_direct' and
'_return_direct_content' keys if return_direct is enabled,
otherwise return None.
"""

tool_res_msg = Msg(
Expand All @@ -546,12 +574,21 @@ async def _acting(self, tool_call: ToolUseBlock) -> dict | None:
],
"system",
)

# Check if the tool has return_direct enabled
tool_name = tool_call["name"]
return_direct = False
if tool_name in self.toolkit.tools:
return_direct = self.toolkit.tools[tool_name].return_direct

try:
# Execute the tool call
tool_res = await self.toolkit.call_tool_function(tool_call)

# Async generator handling
last_chunk = None
async for chunk in tool_res:
last_chunk = chunk
# Turn into a tool result block
tool_res_msg.content[0][ # type: ignore[index]
"output"
Expand All @@ -573,6 +610,17 @@ async def _acting(self, tool_call: ToolUseBlock) -> dict | None:
# Only return the structured output
return chunk.metadata.get("structured_output")

# Check return_direct from tool registration or from ToolResponse
should_return_direct = return_direct or (
last_chunk is not None and last_chunk.return_direct
)

if should_return_direct and last_chunk is not None:
return {
"_return_direct": True,
"_return_direct_content": last_chunk.content,
}

return None

finally:
Expand Down
5 changes: 5 additions & 0 deletions src/agentscope/tool/_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,10 @@ class ToolResponse:
is_interrupted: bool = False
"""Whether the tool execution is interrupted."""

return_direct: bool = False
"""Whether to return the tool's output directly to the user without
further LLM processing. When set to True, the agent will stop the
reasoning-acting loop and return the tool result as the final response."""

id: str = field(default_factory=lambda: _get_timestamp(True))
"""The identity of the tool response."""
Loading