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

This commit is contained in:
codex-bot
2026-03-02 22:32:27 +08:00
commit a64378956a
584 changed files with 93604 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
# Connect AlibabaCloud API MCP Server Example
## What This Example Demonstrates
This use case shows how to use OAuth login in agentscope to connect to the Alibaba Cloud API MCP server.
Alibaba Cloud is a world-leading cloud computing and artificial intelligence technology company, committed to providing one-stop cloud computing services and middleware for enterprises and developers.
Alibaba Cloud API MCP Server provides MCP-based access to nearly all of Alibaba Cloud's OpenAPIs. You can create and optimize them without coding at <https://api.aliyun.com/mcp>.
For example, you can add the ECS service's price query interfaces DescribePrice and CreateInstance, DescribeImages to a custom MCP service. This allows you to obtain a remote MCP address without any code configuration. Using the agent scope, you can query prices and place orders from the agent.In addition to supporting atomic OpenAPI, it also supports encapsulating Terraform HCL as a remote tool to achieve deterministic orchestration.
After adding the sample MCP, you can use queries similar to the following:
1. Find the lowest-priced ECS instance in the Hangzhou region;
2. Create an instance with the lowest price and lowest specifications in Hangzhou.
## Prerequisites
- Python 3.10 or higher
- Python package asyncio, webbrowser
- Node.js and npm (for the MCP server)
- AlibabaCloud API MCP Server connect address [Alibaba Cloud API MCP Server console](https://api.aliyun.com/mcp)
## How to Run This Example
**Edit main.py**
```python
# openai base
# read from .env
load_dotenv()
server_url = "https://openapi-mcp.cn-hangzhou.aliyuncs.com/accounts/14******/custom/****/id/KXy******/mcp"
```
You need to create your own MCP SERVER from https://api.aliyun.com/mcp and replace the link here. Please choose an address that uses the streamable HTTP protocol.
**Run the script**:
```bash
python main.py
```
## Video example
<https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250911/otcfsk/AgentScope+%E9%9B%86%E6%88%90+OpenAPI+MCP+Server%28%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E5%88%9B%E5%BB%BA+ECS%29.mp4>
This video demonstrates how to complete the configuration in the agent scope using the Alibaba Cloud API MCP SERVER service. After logging in through OAuth, users can create an ECS instance using natural language.

View File

@@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
"""The main entry point of the ReAct agent example."""
import asyncio
import os
from dotenv import load_dotenv
from mcp.client.auth import (
OAuthClientProvider,
)
from mcp.shared.auth import (
OAuthClientMetadata,
)
from pydantic import AnyUrl
from oauth_handler import (
InMemoryTokenStorage,
handle_callback,
handle_redirect,
)
from agentscope.agent import ReActAgent, UserAgent
from agentscope.formatter import DashScopeChatFormatter
from agentscope.mcp import HttpStatelessClient
from agentscope.memory import InMemoryMemory
from agentscope.model import DashScopeChatModel
from agentscope.tool import Toolkit
load_dotenv()
# Fetch the MCP endpoint from https://api.aliyun.com/mcp after provisioning.
server_url = (
"https://openapi-mcp.cn-hangzhou.aliyuncs.com/accounts/14******/custom/"
"****/id/KXy******/mcp"
)
memory_token_storage = InMemoryTokenStorage()
oauth_provider = OAuthClientProvider(
server_url=server_url,
client_metadata=OAuthClientMetadata(
client_name="AgentScopeExampleClient",
redirect_uris=[AnyUrl("http://localhost:3000/callback")],
grant_types=["authorization_code", "refresh_token"],
response_types=["code"],
scope=None,
),
storage=memory_token_storage,
redirect_handler=handle_redirect,
callback_handler=handle_callback,
)
stateless_client = HttpStatelessClient(
# Name used to identify the MCP
name="mcp_services_stateless",
transport="streamable_http",
url=server_url,
auth=oauth_provider,
)
def require_env_var(name: str) -> str:
"""Return the value of *name* or raise a helpful error."""
value = os.environ.get(name)
if value is None:
raise RuntimeError(f"Environment variable '{name}' must be set.")
return value
async def main() -> None:
"""The main entry point for the ReAct agent example."""
toolkit = Toolkit()
await toolkit.register_mcp_client(stateless_client)
agent = ReActAgent(
name="AlibabaCloudOpsAgent",
sys_prompt=(
"You are an Alibaba Cloud operations assistant. "
"Use ECS, RDS, VPC, and other services to satisfy requests."
),
model=DashScopeChatModel(
api_key=require_env_var("DASHSCOPE_API_KEY"),
model_name="qwen3-max-preview",
enable_thinking=False,
stream=True,
),
formatter=DashScopeChatFormatter(),
toolkit=toolkit,
memory=InMemoryMemory(),
)
user = UserAgent("User")
msg = None
while True:
msg = await user(msg)
if msg.get_text_content() == "exit":
break
msg = await agent(msg)
asyncio.run(main())

View File

@@ -0,0 +1,266 @@
# -*- coding: utf-8 -*-
"""OAuth handler utilities for the Alibaba Cloud MCP example."""
from __future__ import annotations
import asyncio
import socket
import threading
import webbrowser
from functools import partial
from http.server import BaseHTTPRequestHandler, HTTPServer
from textwrap import dedent
from urllib.parse import parse_qs, urlparse
from mcp.client.auth import TokenStorage
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
SUCCESS_PAGE = dedent(
"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Authorization Complete</title>
</head>
<body>
<h1>Authorization Complete</h1>
<p>You can now return to the application.</p>
<button onclick="window.close()">Close Window</button>
</body>
</html>
""",
)
ERROR_PAGE_TEMPLATE = dedent(
"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Authorization Error</title>
</head>
<body>
<h1>Authorization Error</h1>
<p><strong>Code:</strong> {error}</p>
<p><strong>Description:</strong> {description}</p>
<button onclick="window.close()">Close Window</button>
</body>
</html>
""",
)
INTERNAL_ERROR_TEMPLATE = dedent(
"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Server Error</title>
</head>
<body>
<h1>Server Error</h1>
<p>Sorry, something went wrong while handling the callback.</p>
<pre>{details}</pre>
<button onclick="window.close()">Close Window</button>
</body>
</html>
""",
)
class InMemoryTokenStorage(TokenStorage):
"""Demo in-memory token storage implementation."""
def __init__(self) -> None:
self.tokens: OAuthToken | None = None
self.client_info: OAuthClientInformationFull | None = None
async def get_tokens(self) -> OAuthToken | None:
"""Get stored tokens."""
return self.tokens
async def set_tokens(self, tokens: OAuthToken) -> None:
"""Store tokens."""
self.tokens = tokens
async def get_client_info(self) -> OAuthClientInformationFull | None:
"""Get stored client information."""
return self.client_info
async def set_client_info(
self,
client_info: OAuthClientInformationFull,
) -> None:
"""Store client information."""
self.client_info = client_info
class CallbackHandler(BaseHTTPRequestHandler):
"""HTTP handler for OAuth callback."""
def __init__(
self,
callback_server: "CallbackServer",
request: socket.socket,
client_address: tuple[str, int],
server: HTTPServer,
) -> None:
self.callback_server: "CallbackServer" = callback_server
super().__init__(request, client_address, server)
def do_GET(self) -> None:
"""Handle GET request for OAuth callback."""
try:
parsed_url = urlparse(self.path)
params = parse_qs(parsed_url.query)
if "code" in params:
code = params["code"][0]
state = params.get("state", [None])[0]
self.callback_server.auth_code = code
self.callback_server.auth_state = state
self.callback_server.auth_received = True
self.send_response(200)
self.send_header("Content-type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(SUCCESS_PAGE.encode("utf-8"))
elif "error" in params:
error = params["error"][0]
description = params.get(
"error_description",
["Unknown error"],
)[0]
self.callback_server.auth_error = f"{error}: {description}"
self.callback_server.auth_received = True
self.send_response(400)
self.send_header("Content-type", "text/html; charset=utf-8")
self.end_headers()
page = ERROR_PAGE_TEMPLATE.format(
error=error,
description=description,
)
self.wfile.write(page.encode("utf-8"))
except Exception as exc: # pylint: disable=broad-exception-caught
self.callback_server.auth_error = str(exc)
self.callback_server.auth_received = True
self.send_response(500)
self.send_header("Content-type", "text/html; charset=utf-8")
self.end_headers()
page = INTERNAL_ERROR_TEMPLATE.format(details=exc)
self.wfile.write(page.encode("utf-8"))
class CallbackServer:
"""OAuth callback server."""
def __init__(self, port: int = 3000) -> None:
self.port = port
self.server: HTTPServer | None = None
self.thread: threading.Thread | None = None
self.auth_code: str | None = None
self.auth_state: str | None = None
self.auth_error: str | None = None
self.auth_received = False
def start(self) -> None:
"""Start callback server."""
handler = partial(CallbackHandler, self)
self.server = HTTPServer(("localhost", self.port), handler)
self.thread = threading.Thread(
target=self.server.serve_forever,
daemon=True,
)
self.thread.start()
print(f"OAuth callback server started, listening on port {self.port}")
def stop(self) -> None:
"""Stop callback server."""
if self.server:
self.server.shutdown()
self.server.server_close()
if self.thread:
self.thread.join(timeout=1)
print("OAuth callback server stopped")
async def wait_for_callback(
self,
timeout: float = 300,
) -> tuple[str, str | None]:
"""Wait for OAuth callback."""
loop = asyncio.get_running_loop()
start_time = loop.time()
while not self.auth_received:
if loop.time() - start_time > timeout:
raise TimeoutError("OAuth callback timeout")
await asyncio.sleep(0.1)
if self.auth_error:
raise RuntimeError(
f"OAuth authorization failed: {self.auth_error}",
)
if self.auth_code is None:
raise RuntimeError(
"OAuth authorization failed: missing authorization code",
)
return self.auth_code, self.auth_state
# Global callback server instance
_callback_server: CallbackServer | None = None
async def handle_redirect(auth_url: str) -> None:
"""Automatically open browser for OAuth authorization."""
global _callback_server
# Start callback server
if _callback_server is None:
_callback_server = CallbackServer(port=3000)
_callback_server.start()
print("Opening browser for OAuth authorization...")
print(f"Authorization URL: {auth_url}")
# Automatically open browser
webbrowser.open(auth_url)
async def handle_callback() -> tuple[str, str | None]:
"""Automatically handle OAuth callback."""
global _callback_server
if _callback_server is None:
raise RuntimeError("Callback server not started")
print("Waiting for OAuth authorization to complete...")
try:
# Wait for callback
code, state = await _callback_server.wait_for_callback()
print("OAuth authorization successful!")
return code, state
except Exception as e: # pylint: disable=broad-exception-caught
print(f"OAuth authorization failed: {e}")
raise
finally:
# Clean up server state but keep server running for reuse
_callback_server.auth_code = None
_callback_server.auth_state = None
_callback_server.auth_error = None
_callback_server.auth_received = False

View File

@@ -0,0 +1,22 @@
# Deep Research Agent Example with Qwen-Deep-Research Model
## What This Example Demonstrates
This example shows an Agent implementation with **Qwen-Deep-Research** model using the AgentScope framework. It can break down complex problems, uses web searches to perform analysis, and generates research reports.
Reference: https://www.alibabacloud.com/help/en/model-studio/qwen-deep-research
## Prerequisites
- Python 3.10 or higher
- DashScope API key from [Alibaba Cloud](https://dashscope.console.aliyun.com/)
## How to Run This Example
1. **Set Environment Variable**:
```bash
export DASHSCOPE_API_KEY="your_dashscope_api_key_here"
```
2. **Run the script**:
```bash
python main.py
```

View File

@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
"""The main entry point of the Qwen Deep Research agent example."""
import asyncio
from qwen_deep_research_agent import QwenDeepResearchAgent
from agentscope import logger
from agentscope.message import Msg
async def main() -> None:
"""The main entry point for the Qwen Deep Research agent example."""
# Create DeepResearch Agent
researcher = QwenDeepResearchAgent(
name="Researcher Qwen",
verbose=True,
)
# Step 1: Model follow-up question for confirmation
# The model analyzes the user's question
# and asks follow-up questions to clarify the research direction.
user_msg = Msg(
name="User",
content="Research the applications of artificial intelligence in "
"education",
role="user",
)
clarification = await researcher(user_msg)
print(f"\n{clarification.name}: {clarification.content}\n")
# Step 2: Deep research
# Based on the content of the follow-up question in Step 1,
# the model executes the complete research process.
user_response = Msg(
name="User",
content="I am mainly interested in personalized learning and "
"intelligent assessment.",
role="user",
)
research_result = await researcher(user_response)
print(f"\n{research_result.name}: {research_result.content}\n")
print("\n✅ Research complete!\n")
if __name__ == "__main__":
try:
asyncio.run(main())
except Exception as e:
logger.exception(e)

View File

@@ -0,0 +1,388 @@
# -*- coding: utf-8 -*-
"""Qwen Deep Research Agent"""
# pylint: disable=line-too-long, too-many-branches, too-many-statements
import os
from typing import Any, Optional, Union, Sequence
import dashscope
from dashscope.api_entities.dashscope_response import GenerationResponse
from agentscope import logger
from agentscope.agent import AgentBase
from agentscope.memory import MemoryBase, InMemoryMemory
from agentscope.message import Msg
class QwenDeepResearchAgent(AgentBase):
"""
Deep Research Agent based on Qwen-Deep-Research model
This agent supports a two-step research process:
1. Clarification: Analyzes the question and asks follow-up questions
2. Deep research: Executes the complete research process
Args:
name (str):
Agent name
api_key (str, optional):
DashScope API Key, defaults to environment variable
memory (MemoryBase, optional):
Memory component
verbose (bool):
Whether to display detailed process, defaults to True
"""
def __init__(
self,
name: str,
api_key: Optional[str] = None,
memory: Optional[MemoryBase] = None,
verbose: bool = False,
):
"""Initialize QwenDeepResearchAgent Agent"""
super().__init__()
self.name = name
# Configure API Key
self.api_key = api_key or os.getenv("DASHSCOPE_API_KEY")
if not self.api_key:
raise ValueError(
"The DASHSCOPE_API_KEY environment variable is not set.",
)
self.model_name = "qwen-deep-research"
self.verbose = verbose
self.memory = memory or InMemoryMemory()
async def reply(
self,
x: Optional[Union[Msg, Sequence[Msg]]] = None,
) -> Msg:
"""
Process input message and return reply (asynchronous version)
Args:
x: Input message, can be a single Msg or a list of Msg
Returns:
Msg: Agent's reply message
"""
# Process input message
if x is None:
logger.warning("Received empty message")
return Msg(name=self.name, content="", role="assistant")
# Convert to message list
if isinstance(x, Msg):
msgs = [x]
else:
msgs = list(x)
# Add to memory
for msg in msgs:
await self.memory.add(msg)
# Check if clarification is needed
memory_list = await self.memory.get_memory()
user_msgs = [m for m in memory_list if m.role == "user"]
if len(user_msgs) == 1:
# Step 1: Clarification
logger.info("[%s] Starting clarification ...", self.name)
content = await self._call_model(step_name="Clarification")
response_msg = Msg(
name=self.name,
content=content,
role="assistant",
metadata={
"phase": "clarification",
"requires_user_response": True,
},
)
else:
# Step 2: Deep Research
logger.info("[%s] Starting deep research ...", self.name)
content = await self._call_model(step_name="Deep Research")
response_msg = Msg(
name=self.name,
content=content,
role="assistant",
metadata={
"phase": "deep_research",
"requires_user_response": False,
},
)
await self.memory.add(response_msg)
return response_msg
async def _call_model(self, step_name: str) -> str:
"""
Call qwen-deep-research model
Args:
step_name: step name
Returns:
str: Model response content
"""
if self.verbose:
logger.info("\n%s", "=" * 50)
logger.info(" %s", step_name)
logger.info("%s", "=" * 50)
memory_list = await self.memory.get_memory()
messages = []
for msg in memory_list:
messages.append(
{
"role": msg.role,
"content": msg.content,
},
)
try:
responses = await dashscope.AioGeneration.call(
api_key=self.api_key,
model=self.model_name,
messages=messages,
stream=True,
request_timeout=1800, # Seconds
)
return await self._process_responses(responses)
except Exception as e:
err_msg = f"An error occurred when calling the API: {e}"
logger.error(err_msg)
return err_msg
async def _process_responses(
self,
responses: GenerationResponse,
) -> str:
"""
Process model streaming responses (asynchronous version)
Args:
responses: Model response stream
step_name: Step name
Returns:
str: Model response content
"""
current_phase = None
current_status = None
phase_content = ""
research_goal = ""
keepalive_shown = False
references = []
async for response in responses:
# Check response status
if (
hasattr(response, "status_code")
and response.status_code != 200
):
error_msg = f"HTTP status code: {response.status_code}"
if hasattr(response, "code"):
error_msg += f", Error code: {response.code}"
if hasattr(response, "message"):
error_msg += f", Error message: {response.message}"
logger.error(error_msg)
continue
if hasattr(response, "output") and response.output:
message = response.output.get("message", {})
phase = message.get("phase")
content = message.get("content", "")
status = message.get("status")
extra = message.get("extra", {})
# Phase change detection
if phase != current_phase:
if current_phase and phase_content and self.verbose:
logger.info("\n%s phase completed", current_phase)
current_phase = phase
phase_content = ""
keepalive_shown = False
if phase and phase != "KeepAlive" and self.verbose:
logger.info("\n▶ Entering %s phase", phase)
if phase == "answer":
references = extra.get("deep_research", {}).get(
"references",
[],
)
# Process WebResearch phase
if phase == "WebResearch" and self.verbose:
research_goal = self._handle_web_research_phase(
status,
extra,
research_goal,
)
if content:
phase_content += content
# Display content
if self.verbose:
print(content, end="", flush=True)
# Display status changes
if status:
if (
status != current_status
and status != "typing"
and self.verbose
):
self._log_status(status)
current_status = status
# Token usage statistics
if status == "finished":
self._log_usage(response)
if self.verbose:
logger.info("\n%s phase completed", current_phase)
if phase == "answer":
if len(references) > 0:
reference_links = []
list_links = []
for i, ref in enumerate(references):
title = ref["title"]
url = ref["url"]
reference_links.append(
f'[{i + 1}]: {url} "{title}"',
)
list_links.append(f"{i + 1}. [{title}]({url})")
phase_content = (
phase_content
+ "\n\n## References\n\n"
+ "\n".join(list_links)
+ "\n\n"
+ "\n".join(reference_links)
)
break
# Process KeepAlive
if phase == "KeepAlive":
if not keepalive_shown and self.verbose:
logger.info("\n⏳ Preparing for the next phase...")
keepalive_shown = True
continue
return phase_content
def _handle_web_research_phase(
self,
status: str,
extra: dict,
research_goal: str,
) -> str:
web_sites = []
if extra.get("deep_research", {}).get("research"):
research_info = extra["deep_research"]["research"]
# handle research goal
if status == "streamingQueries":
if "researchGoal" in research_info:
goal = research_info["researchGoal"]
if goal:
research_goal += goal
# handle web site search results
elif status == "streamingWebResult":
if research_goal != "":
logger.info("\n🎯 Research Goal: %s", research_goal)
research_goal = ""
if "webSites" in research_info:
sites = research_info["webSites"]
if sites and sites != web_sites:
# web_sites.clear()
web_sites.extend(sites)
msg = (
f"\n🔍 Found {len(sites)} relevant websites:\n"
+ "\n".join(
f" {i + 1}. {site.get('title', 'No title')}\n"
f" {site.get('url', 'No link')}"
for i, site in enumerate(sites)
)
)
logger.info(msg)
# handle finished status
elif status == "WebResultFinished":
logger.info(
"\n✓ Web search completed, found %s reference sources",
len(web_sites),
)
return research_goal
def _log_status(self, status: str) -> None:
"""log status information"""
status_desc = {
"streamingQueries": "Generating research goals and search queries "
"(WebResearch phase)",
"streamingWebResult": "Performing search, web page reading, and "
"code execution (WebResearch phase)",
"WebResultFinished": "Web search phase completed (WebResearch "
"phase)",
}
if status in status_desc:
logger.info("\n📊 %s", status_desc[status])
def _log_usage(self, response: GenerationResponse) -> None:
"""log Token usage information"""
if hasattr(response, "usage") and response.usage:
usage = response.usage
if self.verbose:
print("\n")
logger.info(
"\n📈 Token usage - input: %s output: %s",
usage.get("input_tokens", 0),
usage.get("output_tokens", 0),
)
async def observe(self, msg: Msg | list[Msg] | None) -> None:
"""Receive the given message(s) without generating a reply.
Args:
msg (`Msg | list[Msg] | None`):
The message(s) to be observed.
"""
# Simply add the message(s) to memory without generating a reply
if msg is not None:
if isinstance(msg, Msg):
await self.memory.add(msg)
else:
for m in msg:
await self.memory.add(m)
async def handle_interrupt(self, *args: Any, **kwargs: Any) -> Msg:
"""The post-processing logic when the reply is interrupted by the
user or something else.
Returns:
Msg: The interrupt message.
"""
# Return a message indicating the interruption
# pylint: disable=unused-argument
return Msg(
name=self.name,
content="Operation was interrupted.",
role="assistant",
)
async def reset_memory(self) -> None:
"""reset memory"""
await self.memory.clear()