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:
50
examples/integration/alibabacloud_api_mcp/README.md
Normal file
50
examples/integration/alibabacloud_api_mcp/README.md
Normal 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.
|
||||
101
examples/integration/alibabacloud_api_mcp/main.py
Normal file
101
examples/integration/alibabacloud_api_mcp/main.py
Normal 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())
|
||||
266
examples/integration/alibabacloud_api_mcp/oauth_handler.py
Normal file
266
examples/integration/alibabacloud_api_mcp/oauth_handler.py
Normal 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
|
||||
22
examples/integration/qwen_deep_research_model/README.md
Normal file
22
examples/integration/qwen_deep_research_model/README.md
Normal 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
|
||||
```
|
||||
51
examples/integration/qwen_deep_research_model/main.py
Normal file
51
examples/integration/qwen_deep_research_model/main.py
Normal 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)
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user