chore: initialize sandbox and overwrite remote content
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled
This commit is contained in:
66
examples/deployment/planning_agent/README.md
Normal file
66
examples/deployment/planning_agent/README.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# High-code Deployment of a Routing Agent
|
||||
|
||||
This example demonstrates how to deploy a multi-agent system using AgentScope. The system is composed of a main
|
||||
routing agent equipped with a tool function named `create_worker` to dispatch tasks to specialized worker agents.
|
||||
|
||||
Specifically, the routing agent is deployed as a chat endpoint in server hold by the `Quart` library.
|
||||
Once receiving an input request, we
|
||||
- set up a routing agent
|
||||
- load the session state if any
|
||||
- invoke the routing agent to handle the request, and return the streaming response
|
||||
- save the session state
|
||||
|
||||
|
||||
# Example Structure
|
||||
|
||||
```
|
||||
planning_agent/
|
||||
├── main.py # Entry point to start the Quart server with routing agent
|
||||
├── tool.py # Tool function to create worker agents
|
||||
└── test_post.py # Preset test script to send requests to the server
|
||||
```
|
||||
|
||||
|
||||
## Note
|
||||
|
||||
1. The printing messages from sub-agent/worker agents is converted to the streaming response of the tool
|
||||
function `create_worker`, meaning the sub-agent won't be exposing to the user directly.
|
||||
|
||||
2. The sub-agent in `tool.py` is equipped with the following tools. For GitHub and AMap tools, they will be activated only
|
||||
if the corresponding environment variables are set.
|
||||
You can customize the toolset by modifying the `tool.py` file.
|
||||
|
||||
| Tool | Description | Required Environment Variable |
|
||||
|-----------------------|-----------------------------------------------------|-------------------------------|
|
||||
| write/view text files | Read and write text files | - |
|
||||
| Playwright MCP server | Automate browser actions using Microsoft Playwright | - |
|
||||
| GitHub MCP server | Access GitHub repositories and data | GITHUB_TOKEN |
|
||||
| AMap MCP server | Access AMap services for location-based tasks | GAODE_API_KEY |
|
||||
|
||||
|
||||
3. Optionally, you can also expose the sub-agent's response to the user by modifying the `tool.py` file.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Install the latest agentscope and Quart packages:
|
||||
|
||||
```bash
|
||||
pip install agentscope quart
|
||||
```
|
||||
|
||||
Ensure you have set `DASHSCOPE_API_KEY` in your environment for DashScope LLM API, or change the used model in
|
||||
both `main.py` and `tool.py` (Remember to change the formatter correspondingly).
|
||||
|
||||
Set the environment variables for GitHub and AMap tools if needed.
|
||||
|
||||
Run the Quart server:
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
In another terminal, run the test script to send a request to the server:
|
||||
|
||||
```bash
|
||||
python test_post.py
|
||||
```
|
||||
129
examples/deployment/planning_agent/main.py
Normal file
129
examples/deployment/planning_agent/main.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The server that holds agent service."""
|
||||
import json
|
||||
import os
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from quart import Quart, Response, request
|
||||
|
||||
from tool import create_worker
|
||||
|
||||
from agentscope.pipeline import stream_printing_messages
|
||||
from agentscope.session import JSONSession
|
||||
from agentscope.agent import ReActAgent
|
||||
from agentscope.formatter import DashScopeChatFormatter
|
||||
from agentscope.message import Msg
|
||||
from agentscope.model import DashScopeChatModel
|
||||
from agentscope.tool import Toolkit
|
||||
|
||||
app = Quart(__name__)
|
||||
|
||||
|
||||
async def handle_input(
|
||||
msg: Msg,
|
||||
user_id: str,
|
||||
session_id: str,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""Handle the input message and yield response chunks.
|
||||
|
||||
Args:
|
||||
msg (`Msg`):
|
||||
The input message from the user.
|
||||
user_id (`str`):
|
||||
The user ID.
|
||||
session_id (`str`):
|
||||
The session ID.
|
||||
|
||||
Yields:
|
||||
`str`:
|
||||
A response message in dict format by `Msg().to_dict()`.
|
||||
"""
|
||||
toolkit = Toolkit()
|
||||
toolkit.register_tool_function(
|
||||
create_worker,
|
||||
)
|
||||
|
||||
# Init JSONSession to save and load the state
|
||||
session = JSONSession(save_dir="./sessions")
|
||||
|
||||
agent = ReActAgent(
|
||||
name="Friday",
|
||||
# pylint: disable=line-too-long
|
||||
sys_prompt="""You are Friday, a multifunctional agent that can help people solving different complex tasks. You act like a meta planner to solve complicated tasks by decomposing the task and building/orchestrating different worker agents to finish the sub-tasks.
|
||||
|
||||
## Core Mission
|
||||
Your primary purpose is to break down complicated tasks into manageable subtasks (a plan), create worker agents to finish the subtask, and coordinate their execution to achieve the user's goal efficiently.
|
||||
|
||||
### Important Constraints
|
||||
1. DO NOT TRY TO SOLVE THE SUBTASKS DIRECTLY yourself.
|
||||
2. Always follow the plan sequence.
|
||||
3. DO NOT finish the plan until all subtasks are finished.
|
||||
""", # noqa: E501
|
||||
model=DashScopeChatModel(
|
||||
model_name="qwen3-max",
|
||||
api_key=os.environ.get("DASHSCOPE_API_KEY"),
|
||||
),
|
||||
formatter=DashScopeChatFormatter(),
|
||||
toolkit=toolkit,
|
||||
)
|
||||
|
||||
# Load the session state if exists
|
||||
await session.load_session_state(
|
||||
session_id=f"{user_id}-{session_id}",
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
async for msg, _ in stream_printing_messages(
|
||||
agents=[agent],
|
||||
coroutine_task=agent(msg),
|
||||
):
|
||||
# Transform the message into a dict string and yield it
|
||||
data = json.dumps(msg.to_dict(), ensure_ascii=False)
|
||||
yield f"data: {data}\n\n"
|
||||
|
||||
# Save the session state
|
||||
await session.save_session_state(
|
||||
session_id=f"{user_id}-{session_id}",
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/chat_endpoint", methods=["POST"])
|
||||
async def chat_endpoint() -> Response:
|
||||
"""A simple chat endpoint that streams responses."""
|
||||
# Parse the user_id, session_id and user message from the request body
|
||||
data = await request.get_json()
|
||||
|
||||
user_id = data.get("user_id")
|
||||
session_id = data.get("session_id")
|
||||
|
||||
# We use textual input here, you can extend it to support other types
|
||||
user_input = data.get("user_input")
|
||||
|
||||
# If the user_id or session_id is missing, return 400
|
||||
if not user_id or not session_id:
|
||||
return Response(
|
||||
f"user_id and session_id are required, got user_id: {user_id}, "
|
||||
f"session_id: {session_id}",
|
||||
status=400,
|
||||
)
|
||||
|
||||
return Response(
|
||||
handle_input(
|
||||
Msg(
|
||||
"user",
|
||||
user_input,
|
||||
"user",
|
||||
),
|
||||
user_id,
|
||||
session_id,
|
||||
),
|
||||
mimetype="text/event-stream",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(
|
||||
port=5000,
|
||||
debug=True,
|
||||
)
|
||||
35
examples/deployment/planning_agent/test_post.py
Normal file
35
examples/deployment/planning_agent/test_post.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Send the post request to get the response from the agent"""
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def send_post(user_query: str) -> None:
|
||||
"""Send the post request to the agent endpoint and print the response."""
|
||||
res = requests.post(
|
||||
url="http://127.0.0.1:5000/chat_endpoint",
|
||||
json={
|
||||
"user_id": "test_user",
|
||||
"session_id": "test_session",
|
||||
"user_input": user_query,
|
||||
},
|
||||
stream=True,
|
||||
)
|
||||
|
||||
res.raise_for_status()
|
||||
|
||||
for chunk in res.iter_content(chunk_size=None):
|
||||
if chunk:
|
||||
print(repr(chunk.decode("utf-8")))
|
||||
|
||||
|
||||
print("The first request response:")
|
||||
# We first tell who we are in the first request
|
||||
send_post("Hi, Alice!")
|
||||
|
||||
print("\n\nThe second request response:")
|
||||
# Test if the session is loaded correctly
|
||||
send_post("Do you know my name?")
|
||||
|
||||
print("\n\nThe third request response:")
|
||||
send_post("Help me to write a hello world in Python")
|
||||
226
examples/deployment/planning_agent/tool.py
Normal file
226
examples/deployment/planning_agent/tool.py
Normal file
@@ -0,0 +1,226 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The tool functions used in the planner example."""
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from agentscope.agent import ReActAgent
|
||||
from agentscope.formatter import DashScopeChatFormatter
|
||||
from agentscope.mcp import HttpStatelessClient, StdIOStatefulClient
|
||||
from agentscope.message import Msg, TextBlock
|
||||
from agentscope.model import DashScopeChatModel
|
||||
from agentscope.pipeline import stream_printing_messages
|
||||
from agentscope.tool import (
|
||||
ToolResponse,
|
||||
Toolkit,
|
||||
write_text_file,
|
||||
insert_text_file,
|
||||
view_text_file,
|
||||
)
|
||||
|
||||
|
||||
class ResultModel(BaseModel):
|
||||
"""
|
||||
The result model used for the sub worker to summarize the task result.
|
||||
"""
|
||||
|
||||
success: bool = Field(
|
||||
description="Whether the task was successful or not.",
|
||||
)
|
||||
message: str = Field(
|
||||
description=(
|
||||
"The specific task result, should include necessary details, "
|
||||
"e.g. the file path if any file is generated, the deviation, "
|
||||
"and the error message if any."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _convert_to_text_block(msgs: list[Msg]) -> list[TextBlock]:
|
||||
# Collect all the content blocks
|
||||
blocks: list = []
|
||||
# Convert tool_use block into text block for streaming tool response
|
||||
for _ in msgs:
|
||||
for block in _.get_content_blocks():
|
||||
if block["type"] == "text":
|
||||
blocks.append(block)
|
||||
|
||||
elif block["type"] == "tool_use":
|
||||
blocks.append(
|
||||
TextBlock(
|
||||
type="text",
|
||||
text=f"Calling tool {block['name']} ...",
|
||||
),
|
||||
)
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
async def create_worker(
|
||||
task_description: str,
|
||||
) -> AsyncGenerator[ToolResponse, None]:
|
||||
"""Create a sub-worker to finish the given task.
|
||||
|
||||
Args:
|
||||
task_description (`str`):
|
||||
The description of the task to be done by the sub-worker, should
|
||||
contain all the necessary information.
|
||||
|
||||
Returns:
|
||||
`AsyncGenerator[ToolResponse, None]`:
|
||||
An async generator yielding ToolResponse objects.
|
||||
"""
|
||||
toolkit = Toolkit()
|
||||
|
||||
# Gaode MCP client
|
||||
if os.getenv("GAODE_API_KEY"):
|
||||
toolkit.create_tool_group(
|
||||
group_name="amap_tools",
|
||||
description="Map-related tools, including geocoding, routing, and "
|
||||
"place search.",
|
||||
)
|
||||
client = HttpStatelessClient(
|
||||
name="amap_mcp",
|
||||
transport="streamable_http",
|
||||
url=f"https://mcp.amap.com/mcp?key={os.environ['GAODE_API_KEY']}",
|
||||
)
|
||||
await toolkit.register_mcp_client(client, group_name="amap_tools")
|
||||
else:
|
||||
print(
|
||||
"Warning: GAODE_API_KEY not set in environment, skipping Gaode "
|
||||
"MCP client registration.",
|
||||
)
|
||||
|
||||
# Browser MCP client
|
||||
toolkit.create_tool_group(
|
||||
group_name="browser_tools",
|
||||
description="Web browsing related tools.",
|
||||
)
|
||||
browser_client = StdIOStatefulClient(
|
||||
name="playwright-mcp",
|
||||
command="npx",
|
||||
args=["@playwright/mcp@latest"],
|
||||
)
|
||||
await browser_client.connect()
|
||||
await toolkit.register_mcp_client(
|
||||
browser_client,
|
||||
group_name="browser_tools",
|
||||
)
|
||||
|
||||
# GitHub MCP client
|
||||
if os.getenv("GITHUB_TOKEN"):
|
||||
toolkit.create_tool_group(
|
||||
group_name="github_tools",
|
||||
description="GitHub related tools, including repository "
|
||||
"search and code file retrieval.",
|
||||
)
|
||||
github_client = HttpStatelessClient(
|
||||
name="github",
|
||||
transport="streamable_http",
|
||||
url="https://api.githubcopilot.com/mcp/",
|
||||
headers={"Authorization": f"Bearer {os.getenv('GITHUB_TOKEN')}"},
|
||||
)
|
||||
await toolkit.register_mcp_client(
|
||||
github_client,
|
||||
group_name="github_tools",
|
||||
)
|
||||
|
||||
else:
|
||||
print(
|
||||
"Warning: GITHUB_TOKEN not set in environment, skipping GitHub "
|
||||
"MCP client registration.",
|
||||
)
|
||||
|
||||
# Basic read/write tools
|
||||
toolkit.register_tool_function(write_text_file)
|
||||
toolkit.register_tool_function(insert_text_file)
|
||||
toolkit.register_tool_function(view_text_file)
|
||||
|
||||
# Create a new sub-agent to finish the given task
|
||||
sub_agent = ReActAgent(
|
||||
name="Worker",
|
||||
sys_prompt=f"""You're an agent named Worker.
|
||||
|
||||
## Your Target
|
||||
Your target is to finish the given task with your tools.
|
||||
|
||||
## IMPORTANT
|
||||
You MUST use the `{ReActAgent.finish_function_name}` to generate the final answer after finishing the task.
|
||||
""", # noqa: E501 # pylint: disable=C0301
|
||||
model=DashScopeChatModel(
|
||||
model_name="qwen3-max",
|
||||
api_key=os.environ["DASHSCOPE_API_KEY"],
|
||||
),
|
||||
enable_meta_tool=True,
|
||||
formatter=DashScopeChatFormatter(),
|
||||
toolkit=toolkit,
|
||||
max_iters=20,
|
||||
)
|
||||
|
||||
# disable the console output of the sub-agent
|
||||
sub_agent.set_console_output_enabled(False)
|
||||
|
||||
# Collect the execution process content
|
||||
msgs = OrderedDict()
|
||||
|
||||
# Wrap the sub-agent in a coroutine task to obtain the final
|
||||
# structured output
|
||||
result = []
|
||||
|
||||
async def call_sub_agent() -> None:
|
||||
msg_res = await sub_agent(
|
||||
Msg(
|
||||
"user",
|
||||
content=task_description,
|
||||
role="user",
|
||||
),
|
||||
structured_model=ResultModel,
|
||||
)
|
||||
result.append(msg_res)
|
||||
|
||||
# Use stream_printing_message to get the streaming response as the
|
||||
# sub-agent works
|
||||
async for msg, _ in stream_printing_messages(
|
||||
agents=[sub_agent],
|
||||
coroutine_task=call_sub_agent(),
|
||||
):
|
||||
msgs[msg.id] = msg
|
||||
|
||||
# Collect all the content blocks
|
||||
yield ToolResponse(
|
||||
content=_convert_to_text_block(
|
||||
list(msgs.values()),
|
||||
),
|
||||
stream=True,
|
||||
is_last=False,
|
||||
)
|
||||
|
||||
# Expose the interruption signal to the caller
|
||||
if msg.metadata and msg.metadata.get("_is_interrupted", False):
|
||||
raise asyncio.CancelledError()
|
||||
|
||||
# Obtain the last message from the coroutine task
|
||||
if result:
|
||||
yield ToolResponse(
|
||||
content=[
|
||||
*_convert_to_text_block(
|
||||
list(msgs.values()),
|
||||
),
|
||||
TextBlock(
|
||||
type="text",
|
||||
text=json.dumps(
|
||||
result[0].metadata,
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
),
|
||||
),
|
||||
],
|
||||
stream=True,
|
||||
is_last=True,
|
||||
)
|
||||
|
||||
await browser_client.close()
|
||||
Reference in New Issue
Block a user