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

23
docs/NEWS.md Normal file
View File

@@ -0,0 +1,23 @@
<!-- This is the source of truth for all NEWS items. -->
<!-- The first 10 items are automatically synced to README.md and README_zh.md via GitHub Actions. -->
<!-- To update news in READMEs, modify this file and push to trigger the workflow. -->
- **[2026-02] `FEAT`:** Realtime Voice Agent support. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/realtime_voice_agent) | [Multi-Agent Realtime Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/workflows/multiagent_realtime) | [Tutorial](https://doc.agentscope.io/tutorial/task_realtime.html)
- **[2026-01] `COMM`:** Biweekly Meetings launched to share ecosystem updates and development plans - join us! [Details & Schedule](https://github.com/agentscope-ai/agentscope/discussions/1126)
- **[2026-01] `FEAT`:** Database support & memory compression in memory module. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/short_term_memory/memory_compression) | [Tutorial](https://doc.agentscope.io/tutorial/task_memory.html)
- **[2025-12] `INTG`:** A2A (Agent-to-Agent) protocol support. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/a2a_agent) | [Tutorial](https://doc.agentscope.io/tutorial/task_a2a.html)
- **[2025-12] `FEAT`:** TTS (Text-to-Speech) support. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/tts) | [Tutorial](https://doc.agentscope.io/tutorial/task_tts.html)
- **[2025-11] `INTG`:** Anthropic Agent Skill support. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/agent_skill) | [Tutorial](https://doc.agentscope.io/tutorial/task_agent_skill.html)
- **[2025-11] `RELS`:** Alias-Agent for diverse real-world tasks and Data-Juicer Agent for data processing open-sourced. [Alias-Agent](https://github.com/agentscope-ai/agentscope-samples/tree/main/alias) | [Data-Juicer Agent](https://github.com/agentscope-ai/agentscope-samples/tree/main/data_juicer_agent)
- **[2025-11] `INTG`:** Agentic RL via Trinity-RFT library. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/tuner/react_agent) | [Trinity-RFT](https://github.com/agentscope-ai/Trinity-RFT)
- **[2025-11] `INTG`:** ReMe for enhanced long-term memory. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/long_term_memory/reme)
- **[2025-11] `RELS`:** agentscope-samples repository launched and agentscope-runtime upgraded with Docker/K8s deployment and VNC-powered GUI sandboxes. [Samples](https://github.com/agentscope-ai/agentscope-samples) | [Runtime](https://github.com/agentscope-ai/agentscope-runtime)
- **[2025-11] `DOCS`:** Contributing Guide is online - welcome to contribute! [Guide](./CONTRIBUTING.md)
- **[2025-09] `FEAT`:** RAG module released. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/rag) | [Tutorial](https://doc.agentscope.io/tutorial/task_rag.html)
- **[2025-09] `FEAT`:** Voice agent support - ReActAgent now supports Qwen-Omni and GPT-Audio natively. [Example](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/voice_agent) | [Roadmap](https://github.com/agentscope-ai/agentscope/issues/773)
- **[2025-09] `FEAT`:** Plan module released. [Tutorial](https://doc.agentscope.io/tutorial/task_plan.html)
- **[2025-09] `RELS`:** AgentScope Runtime open-sourced - enabling effective agent deployment with sandboxed tool execution. [GitHub](https://github.com/agentscope-ai/agentscope-runtime)
- **[2025-09] `RELS`:** AgentScope Studio open-sourced. [GitHub](https://github.com/agentscope-ai/agentscope-studio)
- **[2025-08] `DOCS`:** Tutorial v1 is online. [Tutorial](https://doc.agentscope.io)
- **[2025-08] `RELS`:** AgentScope v1 released - fully embracing asynchronous execution with many new features and improvements. [Changelog](https://github.com/agentscope-ai/agentscope/blob/main/docs/changelog.md)

22
docs/NEWS_zh.md Normal file
View File

@@ -0,0 +1,22 @@
<!-- This is the source of truth for all NEWS items. -->
<!-- The first 10 items are automatically synced to README.md and README_zh.md via GitHub Actions. -->
<!-- To update news in READMEs, modify this file and push to trigger the workflow. -->
- **[2026-02] `功能`:** 支持实时语音交互。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/realtime_voice_agent) | [多智能体实时交互](https://github.com/agentscope-ai/agentscope/tree/main/examples/workflows/multiagent_realtime) | [文档](https://doc.agentscope.io/tutorial/task_realtime.html)
- **[2026-01] `社区`:** AgentScope 双周会议启动,分享生态更新和开发计划 - 欢迎加入![详情与安排](https://github.com/agentscope-ai/agentscope/discussions/1126)
- **[2026-01] `功能`:** 记忆模块新增数据库支持和记忆压缩。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/short_term_memory/memory_compression) | [教程](https://doc.agentscope.io/tutorial/task_memory.html)
- **[2025-12] `集成`:** A2A智能体间通信协议支持。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/a2a_agent) | [教程](https://doc.agentscope.io/zh_CN/tutorial/task_a2a.html)
- **[2025-12] `功能`:** TTS文本转语音支持。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/tts) | [教程](https://doc.agentscope.io/zh_CN/tutorial/task_tts.html)
- **[2025-11] `集成`:** Anthropic Agent Skill 支持。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/agent_skill) | [教程](https://doc.agentscope.io/zh_CN/tutorial/task_agent_skill.html)
- **[2025-11] `发布`:** 面向多样化真实任务的 Alias-Agent 和数据处理的 Data-Juicer Agent 开源。[Alias-Agent](https://github.com/agentscope-ai/agentscope-samples/tree/main/alias) | [Data-Juicer Agent](https://github.com/agentscope-ai/agentscope-samples/tree/main/data_juicer_agent)
- **[2025-11] `集成`:** 通过 Trinity-RFT 库实现智能体强化学习。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/tuner/react_agent) | [Trinity-RFT](https://github.com/agentscope-ai/Trinity-RFT)
- **[2025-11] `集成`:** ReMe 增强长期记忆。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/long_term_memory/reme)
- **[2025-11] `发布`:** agentscope-samples 样例库上线agentscope-runtime 升级支持 Docker/K8s 部署和 VNC 图形沙盒。[样例库](https://github.com/agentscope-ai/agentscope-samples) | [Runtime](https://github.com/agentscope-ai/agentscope-runtime)
- **[2025-11] `文档`:** 贡献指南上线 - 欢迎参与贡献![指南](./CONTRIBUTING_zh.md)
- **[2025-09] `功能`:** RAG 模块发布。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/functionality/rag) | [教程](https://doc.agentscope.io/zh_CN/tutorial/task_rag.html)
- **[2025-09] `功能`:** 语音智能体支持 - ReActAgent 原生支持 Qwen-Omni 和 GPT-Audio。[样例](https://github.com/agentscope-ai/agentscope/tree/main/examples/agent/voice_agent) | [Roadmap](https://github.com/agentscope-ai/agentscope/issues/773)
- **[2025-09] `功能`:** Plan 模块发布。[教程](https://doc.agentscope.io/zh_CN/tutorial/task_plan.html)
- **[2025-09] `发布`:** AgentScope Runtime 开源 - 支持沙盒化工具执行的高效智能体部署。[GitHub](https://github.com/agentscope-ai/agentscope-runtime)
- **[2025-09] `发布`:** AgentScope Studio 开源。[GitHub](https://github.com/agentscope-ai/agentscope-studio)
- **[2025-08] `文档`:** v1 版本教程上线。[教程](https://doc.agentscope.io/zh_CN/)
- **[2025-08] `发布`:** AgentScope v1 发布 - 全面拥抱异步执行,诸多新功能和改进。[变更日志](https://github.com/agentscope-ai/agentscope/blob/main/docs/changelog.md)

98
docs/changelog.md Normal file
View File

@@ -0,0 +1,98 @@
# CHANGELOG of v1.0.0
> ➡️ change; ✅ new feature; ❌ deprecate
The overall changes from v0.x.x to v1.0.0 are summarized below.
## Overview
- ✅ Support asynchronous execution throughout the library
- ✅ Support tools API thoroughly
## ✨Session
- ✅ Support automatic state management
- ✅ Support session/application-level state management
## ✨Tracing
- ✅ Support OpenTelemetry-based tracing
- ✅ Support third-party tracing platforms, e.g. Arize-Phoenix, Langfuse, etc.
## ✨MCP
- ✅ Support both client- and function-level control over MCP by a new MCP module
- ✅ Support both "pay-as-you-go" and persistent session management
- ✅ Support streamable HTTP, SSE and StdIO transport protocols
## ✨Memory
- ✅ Support long-term memory by providing a `LongTermMemoryBase` class
- ✅ Provide a Mem0-based long-term memory implementation
- ✅ Support both static- and agent-controlled long-term memory modes
## Formatter
- ✅ Support prompt construction/formatting with token count estimation
- ✅ Support tools API in multi-agent prompt formatting
## Model
- ❌ Deprecate model configuration, use explicit object instantiation instead
- ✅ Provide a new `ModelResponse` class for structured model responses
- ✅ Support asynchronous model invocation
- ✅ Support reasoning models
- ✅ Support any combination of streaming/non-streaming, reasoning/non-reasoning and tools API
## Agent
- ❌ Deprecate `DialogAgent`, `DictDialogAgent` and prompt-based ReAct agent class
- ➡️ Expose memory, formatter interfaces to the agent's constructor in ReActAgent
- ➡️ Unify the signature of pre- and post- agent hooks
- ✅ Support pre-/post-reasoning and pre-/post-acting hooks in ReActAgent class
- ✅ Support asynchronous agent execution
- ✅ Support interrupting agent's replying and customized interruption handling
- ✅ Support automatic state management
- ✅ Support parallel tool calls
- ✅ Support two-modes long-term memory in ReActAgent class
## Tool
- ✅ Provide a more powerful `Toolkit` class for tools management
- ✅ Provide a new `ToolResponse` class for structured and multimodal tool responses
- ✅ Support group-wise tool management
- ✅ Support agent to manage tools by itself
- ✅ Support post-processing of tool responses
- Tool function
- ✅ Support both async and sync functions
- ✅ Support both streaming and non-streaming return
## Evaluation
- ✅ Support ReAct agent-oriented evaluation
- ✅ Support Ray-based distributed and concurrent evaluation
- ✅ Support statistical analysis over evaluation results
## AgentScope Studio
- ✅ Support runtime tracing
- ✅ Provide a built-in copilot agent named Friday
## Logging
- ❌ Deprecate `loguru` and use Python native `logging` module instead
## Distribution
- ❌ Deprecate distribution functionality momentarily, a new distribution module is coming soon
## RAG
- ❌ Deprecate RAG functionality momentarily, a new RAG module is coming soon
## Parsers
- ❌ Deprecate parsers module
## WebBrowser
- ❌ Deprecate the `WebBrowser` class and shift to MCP-based web browsing

121
docs/roadmap.md Normal file
View File

@@ -0,0 +1,121 @@
# Roadmap
## Long-term Goals
Offering **agent-oriented programming (AOP)** as a new programming paradigm to organize the design and implementation of next-generation LLM-empowered applications.
## Current Focus (January 2026 - )
### 🎙️ Voice Agent
**Voice agents** are a domain we are highly focused on, and AgentScope will continue to invest in this direction.
AgentScope aims to build **production-ready** voice agents rather than demonstration prototypes. This means our voice agents will:
- Support **production-grade** deployment, including seamless frontend integration
- Support **tool invocation**, not just voice conversations
- Support **multi-agent** voice interactions
#### Development Roadmap
Our development strategy for voice agents consists of **three progressive milestones**:
1. **TTS Models** → 2. **Multimodal Models** → 3. **Real-time Multimodal Models**
---
#### Phase 1: TTS (Text-to-Speech) Models
- **Build TTS model base class infrastructure**
- Design and implement a unified TTS model base class
- Establish standardized interfaces for TTS model integration
- **Horizontal API expansion**
- Support mainstream TTS APIs (e.g., OpenAI TTS, Google TTS, Azure TTS, ElevenLabs, etc.)
- Ensure consistent behavior across different TTS providers
---
#### Phase 2: Multimodal Models (Non-Realtime)
- **Enable ReAct agents with multimodal support**
- Integrate multimodal models (e.g., qwen3-omni, gpt-audio) into existing ReAct agent framework
- Support audio input/output in non-realtime mode
- **Advanced multimodal agent capabilities**
- Enable tool invocation within multimodal conversations
- Support multi-agent workflows with multimodal communication
---
#### Phase 3: Real-time Multimodal Models
- **Beyond request-response**: Explore streaming, interrupt handling, and concurrent multimodal processing
- **New programming paradigms**: Design agent programming models specifically tailored for real-time interactions
- **Production readiness**: Ensure low-latency performance, stability, and scalability for production deployment
### 🛠️ Agent Skill
Provide **production-ready** agent skill integration solutions.
### 🌐 Ecosystem Expansion
- **A2UI (Agent-to-UI)**: Enable seamless agent-to-user interface interactions
- **A2A (Agent-to-Agent)**: Enhance agent-to-agent communication capabilities
### 🚀 Agentic RL
- Support using [Tinker](https://tinker-docs.thinkingmachines.ai/) backend to tune agent applications on devices without GPU.
- Support tuning agent applications based on their run history.
- Integrate with AgentScope Runtime to provide better environment abstraction.
- Add more tutorials and examples on how to build complex judge functions with the help of evaluation module.
- Add more tutorials and examples on data selection and augmentation.
### 📈 Code Quality
Continuous refinement and improvement of code quality and maintainability.
# Completed Milestones
### AgentScope V1.0.0 Roadmap
We are deeply grateful for the continuous support from the open-source community that has witnessed AgentScope's
growth. Throughout our journey, we have maintained **developer-centric transparency** as our core principle,
which will continue to guide our future development.
As the AI agent ecosystem rapidly evolves, we recognize the need to adapt AgentScope to meet emerging trends and
requirements. We are excited to announce the upcoming release of AgentScope v1.0.0, which marks a significant shift
towards deployment-focused and secondary development direction. This new version will provide comprehensive support for agent developers
with enhanced deployment capabilities and practical features. Specifically, the update will include:
- ✨New Features
- 🛠️ Tool/MCP
- Support both sync/async tool functions
- Support streaming tool function
- Support parallel execution of tool functions
- Provide more flexible support for the MCP server
- 💾 Memory
- Enhance the existing short-term memory
- Support long-term memory
- 🤖 Agent
- Provide powerful ReAct-based out-of-the-box agents
- 👨‍💻 Development
- Provide enhanced AgentScope Studio with visual components for developing, tracing and debugging
- Provide a built-in copilot for developing/drafting AgentScope applications
- 🔍 Evaluation
- Provide built-in benchmarking and evaluation toolkit for agents
- Support result visualization
- 🏗️ Deployment
- Support asynchronous agent execution
- Support session/state management
- Provide sandbox for tool execution
Stay tuned for our detailed release notes and beta version, which will be available soon. Follow our GitHub
repository and official channels for the latest updates. We look forward to your valuable feedback and continued
support in shaping the future of AgentScope.

View File

@@ -0,0 +1,166 @@
.sphx-glr-download-link-note.admonition.note {
display: none;
}
.sphx-glr-footer {
display: flex;
flex-direction: row;
gap: 8px;
}
.sphx-glr-download-zip {
display: none;
}
.bordered-image {
border: 1px solid #e5e5e5;
}
:root {
--item-card-width: 200px;
--item-card-margin: 10px;
--item-card-title-height: 50px;
--item-card-img-length: calc(var(--item-card-width) - 2*var(--item-card-margin));
--item-card-title-width: calc(var(--item-card-width) - 2*var(--item-card-margin));
--item-card-title-margin-top: var(--item-card-margin);
--item-card-height: calc(var(--item-card-margin) * 3 + var(--item-card-img-length) + var(--item-card-title-height));
}
td .highlight-python.notranslate {
margin-bottom: 0 !important;
}
/*cite {*/
/* background: rgba(229, 229, 229, 0.69) !important;*/
/* padding-left: 0.25rem !important;*/
/* padding-right: 0.25rem !important;*/
/* border-radius: 4px !important;*/
/* font-style: normal !important;*/
/* font-weight: 600 !important;*/
/*}*/
.sidebar-brand-text {
display: flex;
justify-content: center;
}
.sidebar-logo-container .sidebar-logo {
max-height: 170px;
width: auto;
display: block;
}
.gallery-item {
position: relative;
display: inline-block;
width: var(--item-card-width);
height: var(--item-card-height);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
margin: 7px;
}
.docutils.align-default {
white-space: normal !important;
max-width: 100% !important;
width: 100% !important;
td {
white-space: normal !important;
}
}
.sphx-glr-script-out.highlight-none.notranslate .highlight pre{
/*正常打印回车*/
white-space: pre-wrap !important;
/*white-space: normal !important;*/
max-width: 100% !important;
width: 100% !important;
}
.gallery-item-card {
position: absolute;
top: 0;
left: 0;
width: var(--item-card-width);
height: var(--item-card-height);
display: flex;
flex-direction: column;
margin: var(--item-card-margin);
}
.gallery-item-card-img {
height: var(--item-card-img-length);
width: var(--item-card-img-length);
min-width: var(--item-card-img-length);
min-height: var(--item-card-img-length);
display: block;
}
.gallery-item-card-title {
text-align: center;
margin-top: var(--item-card-margin);
font-weight: bold;
min-height: var(--item-card-title-height);
height: var(--item-card-title-height);
width: var(--item-card-title-width);
display: flex;
align-items: center;
justify-content: center;
}
.gallery-item-description {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.9);
/*background-color: #1e8449;*/
color: black;
display: none;
justify-content: center;
align-items: flex-start;
}
.gallery-item:hover .gallery-item-description {
display: flex;
padding: 10px;
border: 1px solid rgba(0, 0, 0, 0.22);
}
.language-switch-button {
background: transparent;
display: flex;
align-content: center;
justify-content: center;
font-size: 15px;
margin-top: 0;
margin-bottom: 4px;
border: none;
color: rgb(4, 4, 4);
height: 20px;
width: 20px;
border-radius: 6px;
font-weight: 325;
}
.language-switch-button:hover {
color: #2758DA;
}
.version-select {
display: flex;
align-items: center;
justify-content: center;
background: transparent;
font-size: 14px;
border: 1px solid rgb(238, 235, 238);
margin-inline: 16px;
padding: 8px;
height: fit-content;
box-sizing: border-box;
font-weight: 600;
border-radius: 6px;
fill: rgb(238, 235, 238);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="550" height="550" viewBox="0 0 550 550"><defs><linearGradient x1="0.01500389538705349" y1="0.4831196665763855" x2="0.9407116114637801" y2="0.3076102348892569" id="master_svg0_8_2390"><stop offset="0%" stop-color="#01C5FF" stop-opacity="1"/><stop offset="100%" stop-color="#019DFB" stop-opacity="1"/></linearGradient><linearGradient x1="0.21085502207279205" y1="0.38703426718711853" x2="0.9523109409029081" y2="0.390888421140005" id="master_svg1_8_1638"><stop offset="0%" stop-color="#4701EF" stop-opacity="1"/><stop offset="100%" stop-color="#395EEF" stop-opacity="1"/></linearGradient></defs><g><g></g><g><g><path d="M275.4998779296875,211.25Q275.4998779296875,279.5,373.4999779296875,310.5Q338.4998779296875,287.5,343.9998779296875,232Q344.4969779296875,227.19400000000002,345.9004779296875,222.498Q352.96577792968753,198.857,382.9998779296875,178L415.9998779296875,193.5L469.7738779296875,193.5C477.0958779296875,193.5,481.9218779296875,185.91559999999998,478.3148779296875,179.543Q441.5068779296875,114.5,372.9998779296875,114.5C343.9998779296875,114.5,275.4998779296875,143,275.4998779296875,211.25Z" fill="url(#master_svg0_8_2390)" fill-opacity="1"/></g><g><path d="M343.9999162890625,231.99999791015625Q337.9999162890625,287.5000079101562,373.5000462890625,310.5000079101562L433.4998462890625,333.00010791015626Q449.4994462890625,314.2500079101562,449.4994462890625,295.5000079101562Q449.4994462890625,276.7500079101562,433.4998462890625,258.0000079101562L345.9004862890625,222.49810791015625Q344.4970462890625,227.19412791015625,343.9999162890625,231.99999791015625Z" fill="#0064FC" fill-opacity="1"/></g><g><path d="M122.9998779296875,351.5C124.3900479296875,350.6567,125.7727179296875,349.8325,127.1479779296875,349.0269Q125.0392779296875,350.2098,122.9998779296875,351.5ZM127.1479779296875,349.0269Q150.3716779296875,336,181.9998779296875,336C186.2692779296875,335.7983,190.4211779296875,335.9808,194.4638779296875,336.502C250.5488779296875,343.7327,285.6218779296875,416.14300000000003,321.9998779296875,432Q350.1248779296875,444,377.9998779296875,444Q405.8748779296875,444,433.4998779296875,432Q489.9998779296875,400,489.9998779296875,344.5Q489.9998779296875,283,433.4998779296875,258Q449.4998779296875,276.75,449.4998779296875,295.5Q449.4998779296875,314.25,433.4998779296875,333C394.3168779296875,374.257,360.65987792968747,359.947,319.4498779296875,342.4251C286.9518779296875,328.6077,249.7568779296875,312.7935,201.4527779296875,320.6594C179.2413779296875,324.2763,154.6808779296875,332.9,127.1479779296875,349.0269Z" fill="url(#master_svg1_8_1638)" fill-opacity="1"/></g><g><path d="M61,437.99973876953123L133.8305,437.99973876953123C141.5661,437.99973876953123,148.608,433.53913876953123,151.9131,426.54513876953126L194.464,336.50202976953125C190.421,335.98083496953126,186.269,335.79829376953126,182,335.99999996953125Q147.4999,335.99999996953125,122.9999,351.5000387695313C93.5,373.5000387695313,87,387.0000387695313,61,437.99973876953123Z" fill="#0064FC" fill-opacity="1"/></g><g><path d="M61,438.00038301849366C87,387.00038301849366,93.5,373.50038301849366,122.9999,351.50038301849366C152.2215,333.77438301849367,178.132,324.45738301849366,201.453,320.65938301849366L256.597,207.04148301849364C259.081,201.92438301849364,259.26800000000003,195.99188301849364,257.11199999999997,190.72838301849367L227.948,119.52337301849366C225.653,113.91851301849366,217.805,113.67667301849366,215.169,119.12954301849365L61,438.00038301849366Z" fill="#01C8FF" fill-opacity="1"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

View File

@@ -0,0 +1,15 @@
function switchV1Language() {
if (window.location.href.includes("zh_CN")) {
window.location.href = "https://doc.agentscope.io";
} else {
window.location.href = "https://doc.agentscope.io/zh_CN";
}
}
function navigateToV0(version) {
if (version === "v0") {
const suffix = window.location.href.includes("zh_CN") ? "/zh_CN" : "/en";
window.location.href = "https://doc.agentscope.io/v0" + suffix;
}
}

View File

@@ -0,0 +1,6 @@
{# _templates/components/language-switch.html #}
<button onclick="switchV1Language()" class="language-switch-button" title="Switch to Chinese">
<script>
document.write(window.location.href.includes("zh_CN") ? "En": "中");
</script>
</button>

View File

@@ -0,0 +1,5 @@
{{ basename | heading }}
.. automodule:: {{ qualname }}
{%- for option in automodule_options %}
:{{ option }}:
{%- endfor %}

View File

@@ -0,0 +1,10 @@
{%- macro automodule(modname, options) -%}
.. automodule:: {{ modname }}
{%- for option in options %}
:{{ option }}:
{%- endfor %}
{%- endmacro %}
{{- pkgname | heading }}
{{ automodule(pkgname, automodule_options) }}

View File

@@ -0,0 +1,239 @@
{% extends "base.html" %}
{% block body -%}
{{ super() }}
{% include "partials/icons.html" %}
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
<label class="overlay sidebar-overlay" for="__navigation">
<div class="visually-hidden">Hide navigation sidebar</div>
</label>
<label class="overlay toc-overlay" for="__toc">
<div class="visually-hidden">Hide table of contents sidebar</div>
</label>
<a class="skip-to-content muted-link" href="#furo-main-content">
{%- trans -%}
Skip to content
{%- endtrans -%}
</a>
{% if theme_announcement -%}
<div class="announcement">
<aside class="announcement-content">
{% block announcement %} {{ theme_announcement }} {% endblock announcement %}
</aside>
</div>
{%- endif %}
<div class="page">
<header class="mobile-header">
<div class="header-left">
<label class="nav-overlay-icon" for="__navigation">
<div class="visually-hidden">Toggle site navigation sidebar</div>
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
</label>
</div>
<div class="header-center">
<a href="{{ pathto(master_doc) }}"><div class="brand">{{ docstitle if docstitle else project }}</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto-light"><use href="#svg-sun-with-moon"></use></svg>
<svg class="theme-icon-when-auto-dark"><use href="#svg-moon-with-sun"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-header-icon{% if furo_hide_toc %} no-toc{% endif %}" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
</header>
<aside class="sidebar-drawer">
<div class="sidebar-container">
{% block left_sidebar %}
<div class="sidebar-sticky">
{%- for sidebar_section in sidebars %}
{%- include sidebar_section %}
{%- endfor %}
</div>
{% endblock left_sidebar %}
</div>
</aside>
<div class="main">
<div class="content">
<div class="article-container">
<a href="#" class="back-to-top muted-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
</svg>
<span>{% trans %}Back to top{% endtrans %}</span>
</a>
<div class="content-icon-container">
{% if theme_top_of_page_button != "edit" -%}
{{ warning("Got configuration for 'top_of_page_button': this is deprecated.") }}
{%- endif -%}
{%- if theme_top_of_page_buttons == "" -%}
{% if theme_top_of_page_button == None -%}
{#- We respect the old configuration of disabling all the buttons -#}
{%- set theme_top_of_page_buttons = [] -%}
{% else %}
{%- set theme_top_of_page_buttons = ["view", "edit"] -%}
{%- endif -%}
{% else -%}
{% if theme_top_of_page_button != "edit" -%}
{%- set theme_top_of_page_buttons = [] -%}
{{ warning("Got configuration for both 'top_of_page_button' and 'top_of_page_buttons', ignoring both and removing all top of page buttons.") }}
{%- endif -%}
{%- endif -%}
{%- include "components/language-switch.html" with context -%}
{% for button in theme_top_of_page_buttons -%}
{% if button == "view" %}
{%- include "components/view-this-page.html" with context -%}
{% elif button == "edit" %}
{%- include "components/edit-this-page.html" with context -%}
{% else %}
{{ warning("Got an unsupported value in 'top_of_page_buttons' for theme configuration") }}
{% endif %}
{%- endfor -%}
{#- Theme toggle -#}
<div class="theme-toggle-container theme-toggle-content">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto-light"><use href="#svg-sun-with-moon"></use></svg>
<svg class="theme-icon-when-auto-dark"><use href="#svg-moon-with-sun"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-content-icon{% if furo_hide_toc %} no-toc{% endif %}" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
<article role="main" id="furo-main-content">
{% block content %}{{ body }}{% endblock %}
</article>
</div>
<footer>
{% block footer %}
<div class="related-pages">
{% if next -%}
<a class="next-page" href="{{ next.link }}">
<div class="page-info">
<div class="context">
<span>{{ _("Next") }}</span>
</div>
<div class="title">{{ next.title }}</div>
</div>
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
</a>
{%- endif %}
{% if prev -%}
<a class="prev-page" href="{{ prev.link }}">
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
<div class="page-info">
<div class="context">
<span>{{ _("Previous") }}</span>
</div>
{% if prev.link == pathto(master_doc) %}
<div class="title">{{ _("Home") }}</div>
{% else %}
<div class="title">{{ prev.title }}</div>
{% endif %}
</div>
</a>
{%- endif %}
</div>
<div class="bottom-of-page">
<div class="left-details">
{%- if show_copyright %}
<div class="copyright">
{%- if hasdoc('copyright') %}
{% trans path=pathto('copyright'), copyright=copyright|e -%}
<a href="{{ path }}">Copyright</a> &#169; {{ copyright }}
{%- endtrans %}
{%- else %}
{% trans copyright=copyright|e -%}
Copyright &#169; {{ copyright }}
{%- endtrans %}
{%- endif %}
</div>
{%- endif %}
{% trans %}Made with {% endtrans -%}
{%- if show_sphinx -%}
{% trans %}<a href="https://www.sphinx-doc.org/">Sphinx</a> and {% endtrans -%}
<a class="muted-link" href="https://pradyunsg.me">@pradyunsg</a>'s
{% endif -%}
{% trans %}
<a href="https://github.com/pradyunsg/furo">Furo</a>
{% endtrans %}
{%- if last_updated -%}
<div class="last-updated">
{% trans last_updated=last_updated|e -%}
Last updated on {{ last_updated }}
{%- endtrans -%}
</div>
{%- endif %}
</div>
<div class="right-details">
{% if theme_footer_icons or READTHEDOCS -%}
<div class="icons">
{% if theme_footer_icons -%}
{% for icon_dict in theme_footer_icons -%}
<a class="muted-link {{ icon_dict.class }}" href="{{ icon_dict.url }}" aria-label="{{ icon_dict.name }}">
{{- icon_dict.html -}}
</a>
{% endfor %}
{%- else -%}
{#- Show Read the Docs project -#}
{%- if READTHEDOCS and slug -%}
<a class="muted-link" href="https://readthedocs.org/projects/{{ slug }}" aria-label="On Read the Docs">
<svg x="0px" y="0px" viewBox="-125 217 360 360" xml:space="preserve">
<path fill="currentColor" d="M39.2,391.3c-4.2,0.6-7.1,4.4-6.5,8.5c0.4,3,2.6,5.5,5.5,6.3 c0,0,18.5,6.1,50,8.7c25.3,2.1,54-1.8,54-1.8c4.2-0.1,7.5-3.6,7.4-7.8c-0.1-4.2-3.6-7.5-7.8-7.4c-0.5,0-1,0.1-1.5,0.2 c0,0-28.1,3.5-50.9,1.6c-30.1-2.4-46.5-7.9-46.5-7.9C41.7,391.3,40.4,391.1,39.2,391.3z M39.2,353.6c-4.2,0.6-7.1,4.4-6.5,8.5 c0.4,3,2.6,5.5,5.5,6.3c0,0,18.5,6.1,50,8.7c25.3,2.1,54-1.8,54-1.8c4.2-0.1,7.5-3.6,7.4-7.8c-0.1-4.2-3.6-7.5-7.8-7.4 c-0.5,0-1,0.1-1.5,0.2c0,0-28.1,3.5-50.9,1.6c-30.1-2.4-46.5-7.9-46.5-7.9C41.7,353.6,40.4,353.4,39.2,353.6z M39.2,315.9 c-4.2,0.6-7.1,4.4-6.5,8.5c0.4,3,2.6,5.5,5.5,6.3c0,0,18.5,6.1,50,8.7c25.3,2.1,54-1.8,54-1.8c4.2-0.1,7.5-3.6,7.4-7.8 c-0.1-4.2-3.6-7.5-7.8-7.4c-0.5,0-1,0.1-1.5,0.2c0,0-28.1,3.5-50.9,1.6c-30.1-2.4-46.5-7.9-46.5-7.9 C41.7,315.9,40.4,315.8,39.2,315.9z M39.2,278.3c-4.2,0.6-7.1,4.4-6.5,8.5c0.4,3,2.6,5.5,5.5,6.3c0,0,18.5,6.1,50,8.7 c25.3,2.1,54-1.8,54-1.8c4.2-0.1,7.5-3.6,7.4-7.8c-0.1-4.2-3.6-7.5-7.8-7.4c-0.5,0-1,0.1-1.5,0.2c0,0-28.1,3.5-50.9,1.6 c-30.1-2.4-46.5-7.9-46.5-7.9C41.7,278.2,40.4,278.1,39.2,278.3z M-13.6,238.5c-39.6,0.3-54.3,12.5-54.3,12.5v295.7 c0,0,14.4-12.4,60.8-10.5s55.9,18.2,112.9,19.3s71.3-8.8,71.3-8.8l0.8-301.4c0,0-25.6,7.3-75.6,7.7c-49.9,0.4-61.9-12.7-107.7-14.2 C-8.2,238.6-10.9,238.5-13.6,238.5z M19.5,257.8c0,0,24,7.9,68.3,10.1c37.5,1.9,75-3.7,75-3.7v267.9c0,0-19,10-66.5,6.6 C59.5,536.1,19,522.1,19,522.1L19.5,257.8z M-3.6,264.8c4.2,0,7.7,3.4,7.7,7.7c0,4.2-3.4,7.7-7.7,7.7c0,0-12.4,0.1-20,0.8 c-12.7,1.3-21.4,5.9-21.4,5.9c-3.7,2-8.4,0.5-10.3-3.2c-2-3.7-0.5-8.4,3.2-10.3c0,0,0,0,0,0c0,0,11.3-6,27-7.5 C-16,264.9-3.6,264.8-3.6,264.8z M-11,302.6c4.2-0.1,7.4,0,7.4,0c4.2,0.5,7.2,4.3,6.7,8.5c-0.4,3.5-3.2,6.3-6.7,6.7 c0,0-12.4,0.1-20,0.8c-12.7,1.3-21.4,5.9-21.4,5.9c-3.7,2-8.4,0.5-10.3-3.2c-2-3.7-0.5-8.4,3.2-10.3c0,0,11.3-6,27-7.5 C-20.5,302.9-15.2,302.7-11,302.6z M-3.6,340.2c4.2,0,7.7,3.4,7.7,7.7s-3.4,7.7-7.7,7.7c0,0-12.4-0.1-20,0.7 c-12.7,1.3-21.4,5.9-21.4,5.9c-3.7,2-8.4,0.5-10.3-3.2c-2-3.7-0.5-8.4,3.2-10.3c0,0,11.3-6,27-7.5C-16,340.1-3.6,340.2-3.6,340.2z" />
</svg>
</a>
{%- endif -%}
{#- Show GitHub repository home -#}
{%- if READTHEDOCS and display_github and github_user != "None" and github_repo != "None" -%}
<a class="muted-link" href="https://github.com/{{ github_user }}/{{ github_repo }}" aria-label="On GitHub">
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
</svg>
</a>
{%- endif -%}
{%- endif %}
</div>
{%- endif %}
</div>
</div>
{% endblock footer %}
</footer>
</div>
<aside class="toc-drawer{% if furo_hide_toc %} no-toc{% endif %}">
{% block right_sidebar %}
{% if not furo_hide_toc %}
<div class="toc-sticky toc-scroll">
<div class="toc-title-container">
<span class="toc-title">
{{ _("On this page") }}
</span>
</div>
<div class="toc-tree-container">
<div class="toc-tree">
{{ toc }}
</div>
</div>
</div>
{% endif %}
{% endblock right_sidebar %}
</aside>
</div>
</div>
{%- endblock %}

View File

@@ -0,0 +1,16 @@
<div class="sidebar-tree">
<p class="caption" role="heading">
<span class="caption-text">
Version
</span>
<ul>
<li>
<select class="version-select" onchange="navigateToV0(this.value)">
<option value="v1" selected>Stable(v1.0)</option>
<option value="v0">v0.1.x</option>
</select>
</li>
</ul>
</p>
{{ furo_navigation_tree }}
</div>

20
docs/tutorial/en/Makefile Normal file
View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

18
docs/tutorial/en/build.sh Normal file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -e
# Clean old build files
rm -rf build/ doctrees/
# Build the html
sphinx-build -M html ./ build
# Remove temporary files (double insurance)
rm -rf build/html/.doctrees
rm -f build/html/.buildinfo
find build/html -name "*.pickle" -delete
find build/html -name "__pycache__" -delete
find build/html -name "*.pyc" -delete
echo "✅ English docs built successfully, temporary files cleaned"

140
docs/tutorial/en/conf.py Normal file
View File

@@ -0,0 +1,140 @@
# -*- coding: utf-8 -*-
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "AgentScope"
copyright = "2025, Alibaba"
author = "Alibaba Tongyi Lab"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
"myst_parser",
"sphinx_gallery.gen_gallery",
"sphinx.ext.autodoc",
"sphinx.ext.viewcode",
"sphinx.ext.napoleon",
]
myst_enable_extensions = [
"colon_fence",
]
sphinx_gallery_conf = {
"download_all_examples": False,
"examples_dirs": [
"src",
],
"gallery_dirs": [
"tutorial",
],
"filename_pattern": "src/.*\.py",
"example_extensions": [".py"],
}
templates_path = ["../_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
languages = ["en", "zh_CN"]
language = "en"
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "furo"
html_title = (
"<span style='font-weight: 700; color: #2196f3;'>AgentScope</span>"
)
html_logo = "../_static/images/logo.svg"
html_favicon = "../_static/images/logo.svg"
html_static_path = ["../_static"]
html_css_files = [
"css/gallery.css",
]
html_js_files = [
"language_switch.js",
]
html_theme_options = {
"footer_icons": [
{
"name": "GitHub",
"url": "https://github.com/agentscope-ai/agentscope",
"html": """
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
</svg>
""",
"class": "",
},
{
"name": "Discord",
"url": "https://discord.gg/eYMpfnkG8h",
"html": """
<svg stroke="currentColor" fill="currentColor" stroke-width="0" t="1753331148815" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5721" width="200" height="200">
<path d="M723.903423 359.138018c-69.65045-52.952793-136.256577-51.476757-136.256576-51.476757l-6.088649 7.564685c83.027027 25.738378 121.127207 62.085766 121.127207 62.085766a387.459459 387.459459 0 0 0-145.297297-46.956397 418.179459 418.179459 0 0 0-98.340901 1.752793 73.801802 73.801802 0 0 1-7.564684 1.476036 357.385225 357.385225 0 0 0-110.702703 30.258739 278.786306 278.786306 0 0 0-28.782703 13.653333S353.049369 339.488288 440.873514 313.657658l-4.612613-6.088649s-66.513874-1.476036-136.164324 51.476757A654.252973 654.252973 0 0 0 230.630631 642.167928s40.867748 71.126486 148.341621 73.801802c0 0 16.697658-22.694054 31.827027-40.867748-62.085766-18.45045-84.77982-57.565405-84.77982-57.565405a130.998198 130.998198 0 0 0 13.653334 7.564684s0 1.568288 1.476036 1.568289c1.476036 1.476036 3.044324 1.476036 4.52036 3.044324a238.748829 238.748829 0 0 0 34.779099 16.605405 513.199279 513.199279 0 0 0 71.218739 21.218018 350.558559 350.558559 0 0 0 125.555315 0 329.894054 329.894054 0 0 0 69.650451-21.218018A247.328288 247.328288 0 0 0 702.685405 618.09009s-24.262342 39.391712-87.824144 57.565405c13.653333 18.45045 31.827027 39.299459 31.827027 39.29946 107.473874-2.952072 148.341622-73.801802 146.773334-72.602523a654.990991 654.990991 0 0 0-69.558199-283.214414zM421.131532 596.77982a54.705586 54.705586 0 0 1 0-109.042162 54.705586 54.705586 0 0 1 0 109.042162z m177.124324 0a54.705586 54.705586 0 1 1 49.908468-54.521081 52.491532 52.491532 0 0 1-49.908468 54.521081z" p-id="5722"></path><path d="M512 1024A512 512 0 1 1 1024 512 512.645766 512.645766 0 0 1 512 1024z m0-972.892252a461.261261 461.261261 0 1 0 461.261261 461.261261 461.261261 461.261261 0 0 0-461.261261-461.261261z" p-id="5723"></path>
</svg>
""",
"class": "",
},
{
"name": "DingTalk",
"url": "https://qr.dingtalk.com/action/joingroup?code=v1,k1,OmDlBXpjW+I2vWjKDsjvI9dhcXjGZi3bQiojOq3dlDw=&_dt_no_comment=1&origin=11",
"html": """
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
<path d="M512 0C229.205333 0 0 229.205333 0 512s229.205333 512 512 512 512-229.205333 512-512S794.794667 0 512 0z m237.312 480.810667c-1.109333 4.48-3.712 11.093333-7.424 18.986666h0.128l-0.426667 0.682667c-21.504 46.037333-77.610667 136.106667-77.610666 136.106667l-0.298667-0.597334-16.384 28.501334h79.018667l-150.912 200.917333 34.304-136.533333h-62.208l21.589333-90.282667c-17.493333 4.224-38.101333 10.026667-62.592 17.92 0 0-33.109333 19.370667-95.317333-37.333333 0 0-41.984-36.992-17.578667-46.165334 10.410667-3.925333 50.304-8.917333 81.706667-13.226666 42.410667-5.674667 68.48-8.789333 68.48-8.789334s-130.773333 2.005333-161.792-2.901333c-30.976-4.906667-70.4-56.704-78.805334-102.186667 0 0-12.970667-25.002667 27.904-13.226666 40.917333 11.818667 210.005333 46.08 210.005334 46.08S321.109333 411.434667 306.517333 394.922667c-14.634667-16.469333-43.093333-89.770667-39.424-134.869334 0 0 1.621333-11.221333 13.098667-8.192 0 0 162.602667 74.282667 273.792 114.986667 111.104 40.704 207.786667 61.397333 195.328 114.005333z" opacity=".65" p-id="6077"></path>
</svg>
""",
"class": "",
},
],
"light_css_variables": {
"color-brand-primary": "#2196f3",
"color-brand-content": "#2196f3",
"color-admonition-background": "#f8f9fa",
},
"dark_css_variables": {
"color-link": "#2196f3",
"color-link--hover": "#2196f3",
"color-brand-primary": "#64b5f6",
"color-brand-content": "#64b5f6",
},
}
source_suffix = [".md", ".rst"]
# -- Options for API documentation -------------------------------------------
autodoc_member_order = "bysource"
autodoc_typehints = "description"
autodoc_class_signature = "separated"
autodoc_default_options = {
"special-members": "__call__",
}
add_module_names = False
python_display_short_literal_types = True
def skip_member(app, what, name, obj, skip, options):
if name in [
"__call__",
"_format",
"_format_agent_message",
"_format_tool_sequence",
]:
return False
return skip
def setup(app):
app.connect("autodoc-skip-member", skip_member)

View File

@@ -0,0 +1,76 @@
.. AgentScope Doc documentation master file, created by
sphinx-quickstart on Thu Aug 8 15:07:21 2024.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to AgentScope's documentation!
==========================================
.. toctree::
:maxdepth: 1
:caption: Tutorial
tutorial/quickstart_installation
tutorial/quickstart_key_concept
tutorial/quickstart_message
tutorial/quickstart_agent
.. toctree::
:maxdepth: 1
:caption: Workflow
tutorial/workflow_conversation
tutorial/workflow_multiagent_debate
tutorial/workflow_concurrent_agents
tutorial/workflow_routing
tutorial/workflow_handoffs
.. toctree::
:maxdepth: 1
:caption: FAQ
tutorial/faq
.. toctree::
:maxdepth: 1
:caption: Model and Context
tutorial/task_model
tutorial/task_prompt
tutorial/task_token
tutorial/task_memory
tutorial/task_long_term_memory
.. toctree::
:maxdepth: 1
:caption: Tool
tutorial/task_tool
tutorial/task_mcp
tutorial/task_agent_skill
.. toctree::
:maxdepth: 1
:caption: Agent
tutorial/task_agent
tutorial/task_state
tutorial/task_hook
tutorial/task_middleware
tutorial/task_a2a
tutorial/task_realtime
.. toctree::
:maxdepth: 1
:caption: Features
tutorial/task_pipeline
tutorial/task_plan
tutorial/task_rag
tutorial/task_studio
tutorial/task_tracing
tutorial/task_eval
tutorial/task_eval_openjudge
tutorial/task_embedding
tutorial/task_tts
tutorial/task_tuner

35
docs/tutorial/en/make.bat Normal file
View File

@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View File

View File

@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
"""
.. _faq:
FAQ
========================================
About AgentScope
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*What is AgentScope?*
AgentScope is a multi-agent framework, aiming to provide a simple yet efficient way to build LLM-empowered agent applications.
*What is the difference between AgentScope v1.0 and v0.x?*
AgentScope v1.0 is a complete refactoring of the framework, equipped with new features and improvements. Refer to for detailed changes.
About Model
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*How to integrate my own model with AgentScope?*
Create your own model by inheriting ``agentscope.model.ChatModelBase`` and implement the ``__call__`` method.
*What models are supported by AgentScope?*
Currently, AgentScope has built-in support for DashScope, Gemini, OpenAI, Anthropic, and Ollama APIs, and the ``OpenAIChatModel`` compatible with DeepSeek and vLLMs models.
*How to monitor the token usage in AgentScope?*
In AgentScope Studio, we provide visualization of token usage and tracing. Refer :ref:`studio` section for more details.
About Agent
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*How to create my own agent?*
You can choose to use the ``ReActAgent`` class directly, or create your own agent by inheriting from ``AgentBase`` or ``ReActAgentBase`` classes. Refer to the :ref:`agent` section for more details.
*How to forward the (streaming) output of agents to my own frontend or application?*
Use the pre hook of the ``print`` function to forward printing messages. Refer to the :ref:`hook` section.
About Tools
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*How many tools are provided by AgentScope?*
AgentScope provides a set of built-in tools, including ``execute_python_code``, ``execute_shell_command``, ``write_text_file`` , etc. You can find them under ``agentscope.tool`` module.
About Reporting Bugs
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*How can I report a bug in AgentScope?*
If you encounter a bug while using AgentScope, please report it by opening an issue on our GitHub repository.
*How can I report a security bug in AgentScope?*
If you discover a security issue in AgentScope, please report it to us through the `Alibaba Security Response Center (ASRC) <https://security.alibaba.com/>`_.
"""

View File

@@ -0,0 +1,245 @@
# -*- coding: utf-8 -*-
"""
.. _react-agent:
Create ReAct Agent
====================
AgentScope provides out-of-the-box ReAct agent ``ReActAgent`` under ``agentscope.agent`` that can be used directly.
It supports the following features at the same time:
- ✨ Basic features
- Support **hooks** around ``reply``, ``observe``, ``print``, ``_reasoning`` and ``_acting`` functions
- Support structured output
- ✋ Realtime Steering
- Support user **interrupt**
- Support customized **interruption handling**
- 🛠️ Tools
- Support both **sync/async** tool functions
- Support **streaming** tool response
- Support **stateful** tools management
- Support **parallel** tool calls
- Support **MCP** server
- 💾 Memory
- Support **agent-controlled** long-term memory management
- Support static long-term memory management
.. tip:: Refer to the :ref:`agent` section for more details about these
features. In quickstart, we focus on how to create a ReAct agent and run it.
"""
from agentscope.agent import ReActAgent, AgentBase
from agentscope.formatter import DashScopeChatFormatter
from agentscope.memory import InMemoryMemory
from agentscope.message import Msg
from agentscope.model import DashScopeChatModel
import asyncio
import os
from agentscope.tool import Toolkit, execute_python_code
# %%
# Creating ReAct Agent
# ------------------------------
# To improve the flexibility, the ``ReActAgent`` class exposes the following parameters in its constructor:
#
# .. list-table:: Initialization parameters of ``ReActAgent`` class
# :header-rows: 1
#
# * - Parameter
# - Further Reading
# - Description
# * - ``name`` (required)
# -
# - The name of the agent
# * - ``sys_prompt`` (required)
# -
# - The system prompt of the agent
# * - ``model`` (required)
# - :ref:`model`
# - The model used by the agent to generate responses
# * - ``formatter`` (required)
# - :ref:`prompt`
# - The prompt construction strategy, should be consisted with the model
# * - ``toolkit``
# - :ref:`tool`
# - The toolkit to register/call tool functions.
# * - ``memory``
# - :ref:`memory`
# - The short-term memory used to store the conversation history
# * - ``long_term_memory``
# - :ref:`long-term-memory`
# - The long-term memory
# * - ``long_term_memory_mode``
# - :ref:`long-term-memory`
# - The mode of the long-term memory:
#
# - ``agent_control``: allow agent to control the long-term memory by itself
# - ``static_control``: retrieving and recording from/to long-term memory will happen in the beginning/end of each reply.
# - ``both``: activate the above two modes at the same time
# * - ``enable_meta_tool``
# - :ref:`tool`
# - Whether to enable the meta tool, which allows the agent to manage tools by itself
# * - ``parallel_tool_calls``
# - :ref:`agent`
# - Whether to allow parallel tool calls
# * - ``max_iters``
# -
# - The maximum number of iterations for the agent to generate a response
# * - ``plan_notebook``
# - :ref:`plan`
# - The plan notebook to manage the plans
# * - ``print_hint_msg``
# -
# - Whether to print the hint message generated by the plan notebook at each step
#
# Taking DashScope API as example, we create an agent object as follows:
async def creating_react_agent() -> None:
"""Create a ReAct agent and run a simple task."""
# Prepare tools
toolkit = Toolkit()
toolkit.register_tool_function(execute_python_code)
jarvis = ReActAgent(
name="Jarvis",
sys_prompt="You're a helpful assistant named Jarvis",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
stream=True,
enable_thinking=False,
),
formatter=DashScopeChatFormatter(),
toolkit=toolkit,
memory=InMemoryMemory(),
)
msg = Msg(
name="user",
content="Hi! Jarvis, run Hello World in Python.",
role="user",
)
await jarvis(msg)
asyncio.run(creating_react_agent())
# %%
# Creating From Scratch
# --------------------------------
# You may want to create an agent from scratch, AgentScope provides two base classes for you to inherit from:
#
# .. list-table::
# :header-rows: 1
#
# * - Class
# - Abstract Methods
# - Description
# * - ``AgentBase``
# - | ``reply``
# | ``observe``
# | ``handle_interrupt``
# - - The base class for all agents, supporting pre- and post- hooks around ``reply``, ``observe`` and ``print`` functions.
# - Implement the realtime steering within the ``__call__`` method.
# * - ``ReActAgentBase``
# - | ``reply``
# | ``observe``
# | ``handle_interrupt``
# | ``_reasoning``
# | ``_acting``
# - Add two abstract functions ``_reasoning`` and ``_acting`` on the basis of ``AgentBase``, as well as their hooks.
#
# Please refer to the :ref:`agent` section for more details about the agent class.
#
# Taking the ``AgentBase`` class as an example, we can create a custom agent
# class by inheriting from it and implementing the ``reply`` method.
class MyAgent(AgentBase):
"""A custom agent class"""
def __init__(self) -> None:
"""Initialize the agent"""
super().__init__()
self.name = "Friday"
self.sys_prompt = "You're a helpful assistant named Friday."
self.model = DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
stream=False,
)
self.formatter = DashScopeChatFormatter()
self.memory = InMemoryMemory()
async def reply(self, msg: Msg | list[Msg] | None) -> Msg:
"""Reply to the message."""
await self.memory.add(msg)
# Prepare the prompt
prompt = await self.formatter.format(
[
Msg("system", self.sys_prompt, "system"),
*await self.memory.get_memory(),
],
)
# Call the model
response = await self.model(prompt)
msg = Msg(
name=self.name,
content=response.content,
role="assistant",
)
# Record the response in memory
await self.memory.add(msg)
# Print the message
await self.print(msg)
return msg
async def observe(self, msg: Msg | list[Msg] | None) -> None:
"""Observe the message."""
# Store the message in memory
await self.memory.add(msg)
async def handle_interrupt(self) -> Msg:
"""Postprocess the interrupt."""
# Taking a fixed response as example
return Msg(
name=self.name,
content="I noticed you interrupted me, how can I help you?",
role="assistant",
)
async def run_custom_agent() -> None:
"""Run the custom agent."""
agent = MyAgent()
msg = Msg(
name="user",
content="Who are you?",
role="user",
)
await agent(msg)
asyncio.run(run_custom_agent())
# %%
#
# Further Reading
# ---------------------
# - :ref:`agent`
# - :ref:`model`
# - :ref:`prompt`
# - :ref:`tool`
#

View File

@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
"""
.. _installation:
Installation
============================
AgentScope requires Python 3.10 or higher. You can install from source or pypi.
From PyPI
----------------
.. code-block:: bash
pip install agentscope
From Source
----------------
To install AgentScope from source, you need to clone the repository from
GitHub and install by the following commands
.. code-block:: bash
git clone -b main https://github.com/agentscope-ai/agentscope
cd agentscope
pip install -e .
To ensure AgentScope is installed successfully, check via executing the following code:
"""
import agentscope
print(agentscope.__version__)
# %%
# Extra Dependencies
# ----------------------------
#
# To satisfy the requirements of different functionalities, AgentScope provides
# extra dependencies that can be installed based on your needs.
#
# - full: Including extra dependencies for model APIs and tool functions
# - dev: Development dependencies, including testing and documentation tools
#
# For example, when installing the full dependencies, the installation command varies depending on your operating system.
#
# For Windows users:
#
# .. code-block:: bash
#
# pip install agentscope[full]
#
# For Mac and Linux users:
#
# .. code-block:: bash
#
# pip install agentscope\[full\]

View File

@@ -0,0 +1,122 @@
# -*- coding: utf-8 -*-
"""
.. key-concepts:
Key Concepts
====================================
This chapter establishes key concepts from an engineering
perspective to introduce AgentScope's design.
.. note:: The goal of introducing the key concepts in AgentScope is to claim what practical problems AgentScope addresses and how it supports developers, rather than to offer formal definitions.
State
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In AgentScope, state management is a fundamental building block that maintains snapshots of objects' runtime data.
AgentScope separates object initialization from state management, allowing
object to be restored to different states after initialization through
``load_state_dict`` and ``state_dict`` methods.
In AgentScope, agent, memory, long-term memory and toolkit are all stateful
objects. AgentScope links the state management of these objects together by supporting nested state management.
Message
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In AgentScope, message is the fundamental data structure,
used to
- exchange information between agents,
- display information in the user interface,
- store information in memory,
- act as a unified medium between AgentScope and different LLM APIs.
Tool
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A tool in AgentScope refers to callable object, no matter it's a
- function,
- partial function,
- instance method,
- class method,
- static method, or
- callable instance with ``__call__`` method.
Besides, the callable object can be either
- async or sync,
- streaming or non-streaming.
So feel free to use any callable object as a tool in AgentScope.
Agent
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In AgentScope, the agent behaviors are abstracted into three core functions in
``AgentBase`` class:
- ``reply``: Handle incoming message(s) and generate a response message.
- ``observe``: Receive message(s) from the environment or other agents without returning a response.
- ``print``: Display message(s) to the target terminal, web interface, etc.
To support realtime steering, an additional ``handle_interrupt`` function is
provided to handle user interrupts during the agent's reply process.
Additionally, ReAct agent is the most important agent in AgentScope, where
the agent's reply process is divided into two stages:
- reasoning: thinking and generating tool calls by calling the LLM
- acting: execute the tool functions.
Thus, we provide two additional core functions in ``ReActAgentBase`` class,
``_reasoning`` and ``_acting``.
Formatter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Formatter is the core component for LLM compatibility in AgentScope,
responsible for converting message objects into the required format for
LLM APIs.
Besides, additional functionality such as prompt engineering, truncation,
and message validation can also be implemented in the formatter.
Within the formatter, the "multi-agent" (or "multi-identity") concept differs
from the common multi-agent orchestration concept.
It focuses on the scenario where multiple identities are involved in the
given messages, so that the common used ``role`` field (usually "role",
"assistant" or "system") in LLM APIs cannot distinguish them.
Therefore, AgentScope provides multi-agent formatter to handle
this scenario, usually used in games, multi-person chats, and social
simulations.
.. note:: Multi-agent workflow **!=** multi-agent in formatter.
For example, even if the following code snippet may involve multiple
agents (the ``tool_agent`` and the ``tool_function`` caller), the input query
is wrapped into a **user** message, so the ``role`` field can still distinguish
between them.
.. code-block:: python
async def tool_function(query: str) -> str:
\"\"\"Tool function calling another agent\"\"\"
msg = Msg("user", query, role="user")
tool_agent = Agent(name="Programmer")
return await tool_agent(msg)
Understanding this distinction helps developers better grasp AgentScope's formatter design.
Long-Term Memory
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Although providing different base classes for short- and
long-term memory, there are no strict distinctions between them in AgentScope.
In our view, everything should be **requirement-driven**. As long as your
needs are excellently met, developers can completely use just one powerful
memory system.
For ensuring the flexibility of AgentScope, we provide a two mode long-term
memory system, allowing the agent to manage (record and retrieve) the
long-term memory by its own.
"""

View File

@@ -0,0 +1,267 @@
# -*- coding: utf-8 -*-
"""
.. _message:
Create Message
====================
Message is the core concept in AgentScope, used to support multimodal data, tools API, information storage/exchange and prompt construction.
A message consists of four fields:
- ``name``,
- ``role``,
- ``content``, and
- ``metadata``
The types and descriptions of these fields are as follows:
.. list-table:: The fields in a message object
:header-rows: 1
* - Field
- Type
- Description
* - name
- ``str``
- The name/identity of the message sender
* - role
- | ``Literal[``
| ``"system",``
| ``"assistant",``
| ``"user"``
| ``]``
- The role of the message sender, which must be one of "system", "assistant", or "user".
* - content
- ``str | list[ContentBlock]``
- The data of the message, which can be a string or a list of blocks.
* - metadata
- ``dict[str, JSONSerializableObject] | None``
- A dict containing additional metadata about the message, usually used for structured output.
.. tip:: - In application with multiple identities, the ``name`` field is used to distinguish between different identities.
- The ``metadata`` field is recommended for structured output, which won't be included in the prompt construction.
Next, we introduce the supported blocks in the ``content`` field by their corresponding scenarios.
"""
from agentscope.message import (
Msg,
Base64Source,
TextBlock,
ThinkingBlock,
ImageBlock,
AudioBlock,
VideoBlock,
ToolUseBlock,
ToolResultBlock,
)
import json
# %%
# Creating Textual Message
# -----------------------------
# Creating a message object by providing the ``name``, ``role``, and ``content`` fields.
#
msg = Msg(
name="Jarvis",
role="assistant",
content="Hi! How can I help you?",
)
print(f"The name of the sender: {msg.name}")
print(f"The role of the sender: {msg.role}")
print(f"The content of the message: {msg.content}")
# %%
# Creating Multimodal Message
# --------------------------------------
# The message class supports multimodal content by providing different content blocks:
#
# .. list-table:: Multimodal content blocks in AgentScope
# :header-rows: 1
#
# * - Class
# - Description
# - Example
# * - TextBlock
# - Pure text data
# - .. code-block:: python
#
# TextBlock(
# type="text",
# text="Hello, world!"
# )
# * - ImageBlock
# - The image data
# - .. code-block:: python
#
# ImageBlock(
# type="image",
# source=URLSource(
# type="url",
# url="https://example.com/image.jpg"
# )
# )
# * - AudioBlock
# - The audio data
# - .. code-block:: python
#
# AudioBlock(
# type="audio",
# source=URLSource(
# type="url",
# url="https://example.com/audio.mp3"
# )
# )
# * - VideoBlock
# - The video data
# - .. code-block:: python
#
# VideoBlock(
# type="video",
# source=URLSource(
# type="url",
# url="https://example.com/video.mp4"
# )
# )
#
# For ``ImageBlock``, ``AudioBlock`` and ``VideoBlock``, you can use either a base64 encoded string as the source:
#
msg = Msg(
name="Jarvis",
role="assistant",
content=[
TextBlock(
type="text",
text="This is a multimodal message with base64 encoded data.",
),
ImageBlock(
type="image",
source=Base64Source(
type="base64",
media_type="image/jpeg",
data="/9j/4AAQSkZ...",
),
),
AudioBlock(
type="audio",
source=Base64Source(
type="base64",
media_type="audio/mpeg",
data="SUQzBAAAAA...",
),
),
VideoBlock(
type="video",
source=Base64Source(
type="base64",
media_type="video/mp4",
data="AAAAIGZ0eX...",
),
),
],
)
# %%
# Creating Thinking Message
# --------------------------------------
# The ``ThinkingBlock`` is to support reasoning models, containing the thinking process of the model.
#
msg_thinking = Msg(
name="Jarvis",
role="assistant",
content=[
ThinkingBlock(
type="thinking",
thinking="I'm building an example for thinking block in AgentScope.",
),
TextBlock(
type="text",
text="This is an example for thinking block.",
),
],
)
# %%
# .. _tool-block:
#
# Creating Tool Use/Result Message
# --------------------------------------
# The ``ToolUseBlock`` and ``ToolResultBlock`` are to support tools API:
#
msg_tool_call = Msg(
name="Jarvis",
role="assistant",
content=[
ToolUseBlock(
type="tool_use",
id="343",
name="get_weather",
input={
"location": "Beijing",
},
),
],
)
msg_tool_res = Msg(
name="system",
role="system",
content=[
ToolResultBlock(
type="tool_result",
id="343",
name="get_weather",
output="The weather in Beijing is sunny with a temperature of 25°C.",
),
],
)
# %%
# .. tip:: Refer to the :ref:`tool` section for more information about tools API in AgentScope.
#
# Serialization and Deserialization
# ------------------------------------------------
# Message object can be serialized and deserialized by ``to_dict`` and ``from_dict`` methods, respectively.
serialized_msg = msg.to_dict()
print(type(serialized_msg))
print(json.dumps(serialized_msg, indent=4))
# %%
# Deserialize a message from a string in JSON format.
new_msg = Msg.from_dict(serialized_msg)
print(type(new_msg))
print(f'The sender of the message: "{new_msg.name}"')
print(f'The role of the sender: "{new_msg.role}"')
print(f'The content of the message: "{json.dumps(new_msg.content, indent=4)}"')
# %%
# Property Functions
# ------------------------------------------------
# To ease the use of message object, AgentScope provides these functions:
#
# .. list-table:: Functions of the message object
# :header-rows: 1
#
# * - Function
# - Parameters
# - Description
# * - get_text_content
# - \-
# - Gather content from all ``TextBlock`` in to a single string (separated by "\\n").
# * - get_content_blocks
# - ``block_type``
# - Return a list of content blocks of the specified type. If ``block_type`` not provided, return content in blocks format.
# * - has_content_blocks
# - ``block_type``
# - Check whether the message has content blocks of the specified type. The ``str`` content is considered as a ``TextBlock`` type.

View File

@@ -0,0 +1,212 @@
# -*- coding: utf-8 -*-
"""
.. _a2a:
A2A Agent
============================
A2A (Agent-to-Agent) is an open standard protocol for enabling interoperable communication between different AI agents.
AgentScope provides support for the A2A protocol at two levels: obtaining Agent Card information and connecting to remote agents. The related APIs are as follows:
.. list-table:: A2A Related Classes
:header-rows: 1
* - Class
- Description
* - ``A2AAgent``
- Agent class for communicating with remote A2A agents
* - ``A2AChatFormatter``
- Formatter for converting between AgentScope messages and A2A message/task formats
* - ``AgentCardResolverBase``
- Base class for Agent Card resolvers
* - ``FileAgentCardResolver``
- Resolver for loading Agent Cards from local JSON files
* - ``WellKnownAgentCardResolver``
- Resolver for fetching Agent Cards from the well-known path of a URL
* - ``NacosAgentCardResolver``
- Resolver for fetching Agent Cards from the Nacos Agent Registry
This section demonstrates how to create an ``A2AAgent`` and communicate with remote A2A agents.
.. note:: Note that A2A support is an **experimental feature** and may change in future versions. Due to limitations of the A2A protocol itself, ``A2AAgent`` cannot fully align with local agents like ``ReActAgent``, including:
- Only supports chatbot scenarios, i.e., only supports conversations between one user and one agent (does not affect handoff/router usage patterns)
- Does not support real-time interruption during conversations
- Does not support agentic structured output
- In the current implementation, messages received by the ``observe`` method are stored locally and sent to the remote agent together when the ``reply`` method is called. Therefore, if several ``observe`` calls are made without a subsequent ``reply`` call, those messages will not be seen by the remote agent
"""
from a2a.types import AgentCard, AgentCapabilities
from v2.nacos import ClientConfig
from agentscope.a2a import WellKnownAgentCardResolver, NacosAgentCardResolver
from agentscope.agent import A2AAgent, UserAgent
from agentscope.message import Msg, TextBlock
from agentscope.tool import ToolResponse
# %%
# Obtaining Agent Cards
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# First, we need to obtain an Agent Card to connect to the corresponding agent. An Agent Card contains information such as the agent's name, description, capabilities, and connection details.
#
# Manually Creating Agent Card
# --------------------------------
#
# If you know all the information of an Agent Card, you can directly create an Agent Card object from `a2a.types.AgentCard`.
#
# Create an Agent Card object
agent_card = AgentCard(
name="Friday", # Agent name
description="A fun chatting companion", # Agent description
url="http://localhost:8000", # Agent's RPC service URL
version="1.0.0", # Agent version
capabilities=AgentCapabilities( # Agent capability configuration
push_notifications=False,
state_transition_history=True,
streaming=True,
),
default_input_modes=["text/plain"], # Supported input formats
default_output_modes=["text/plain"], # Supported output formats
skills=[], # Agent skill list
)
# %%
#
# Fetching from Remote Services
# --------------------------------
# AgentScope also supports fetching from the standard path of remote services (well-known server).
# Here's an example using ``WellKnownAgentCardResolver`` to fetch an Agent Card from the standard path of a remote service:
#
async def agent_card_from_well_known_website() -> AgentCard:
"""Example of fetching an Agent Card from the well-known path of a remote service."""
# Create an Agent Card resolver
resolver = WellKnownAgentCardResolver(
base_url="http://localhost:8000",
)
# Fetch and return the Agent Card
return await resolver.get_agent_card()
# %%
# Loading Agent Cards from Local Files
# --------------------------------
#
# The ``FileAgentCardResolver`` class supports loading Agent Cards from local JSON files, suitable for configuration file management scenarios.
# An example of an Agent Card in JSON format is shown below:
#
# .. code-block:: json
# :caption: Example Agent Card JSON file content
#
# {
# "name": "RemoteAgent",
# "url": "http://localhost:8000",
# "description": "Remote A2A Agent",
# "version": "1.0.0",
# "capabilities": {},
# "default_input_modes": ["text/plain"],
# "default_output_modes": ["text/plain"],
# "skills": []
# }
#
# You can easily load this file using ``FileAgentCardResolver``:
#
async def agent_card_from_file() -> AgentCard:
"""Example of loading an Agent Card from a local JSON file."""
from agentscope.a2a import FileAgentCardResolver
# Load Agent Card from JSON file
resolver = FileAgentCardResolver(
file_path="./agent_card.json", # JSON file path
)
# Fetch and return the Agent Card
return await resolver.get_agent_card()
# %%
# Fetching Agent Cards from Nacos Registry
# --------------------------------
#
# Nacos is an open-source dynamic service discovery, configuration management, and service management platform. In version 3.1.0, it introduced the Agent Registry feature, supporting distributed registration, discovery, and version management of A2A agents.
#
# .. important:: The prerequisite for using ``NacosAgentCardResolver`` is that the user has deployed a Nacos server version 3.1.0 or higher. For deployment and registration procedures, please refer to the `official documentation <https://nacos.io/docs/latest/quickstart/quick-start>`_.
#
async def agent_card_from_nacos() -> AgentCard:
"""Example of fetching an Agent Card from the Nacos registry."""
# Create a Nacos Agent Card resolver
resolver = NacosAgentCardResolver(
remote_agent_name="my-remote-agent", # Agent name registered in Nacos
nacos_client_config=ClientConfig(
server_addresses="http://localhost:8848", # Nacos server address
# Other optional configuration items
),
)
# Fetch and return the Agent Card
return await resolver.get_agent_card()
# %%
# Building an A2A Agent
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# The ``A2AAgent`` class provided by AgentScope is used to communicate with remote A2A agents, and its usage is similar to regular agents.
agent = A2AAgent(agent_card=agent_card)
# %%
# Using ``A2AAgent``, developers can build chatbot scenario conversations, or encapsulate it as a tool function to build more complex application scenarios such as handoff/router.
# Currently, the format protocol conversion supported by ``A2AAgent`` is handled by ``agentscope.formatter.A2AChatFormatter``, which supports:
#
# - Converting AgentScope's ``Msg`` messages to A2A protocol's ``Message`` format
# - Converting A2A protocol responses back to AgentScope's ``Msg`` format
# - Converting A2A protocol's ``Task`` responses to AgentScope's ``Msg`` format
# - Supporting multiple content types such as text, images, audio, and video
#
async def a2a_in_chatbot() -> None:
"""Example of chatting using A2AAgent."""
user = UserAgent("user")
msg = None
while True:
msg = await user(msg)
if msg.get_text_content() == "exit":
break
msg = await agent(msg)
# %%
# Or encapsulate it as a tool function for invocation:
async def create_worker(query: str) -> ToolResponse:
"""Complete a given task through a sub-agent
Args:
query (`str`):
Description of the task to be completed by the sub-agent
"""
res = await agent(
Msg("user", query, "user"),
)
return ToolResponse(
content=[
TextBlock(
type="text",
text=res.get_text_content(),
),
],
)

View File

@@ -0,0 +1,426 @@
# -*- coding: utf-8 -*-
"""
.. _agent:
Agent
=========================
In this tutorial, we first focus on introducing the ReAct agent in AgentScope,
then we briefly introduce how to customize your own agent from scratch.
ReAct Agent
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In AgentScope, the ``ReActAgent`` class integrates various features into a final implementation, including
.. list-table:: Features of ``ReActAgent``
:header-rows: 1
* - Feature
- Reference
* - Support realtime steering
-
* - Support memory compression
-
* - Support parallel tool calls
-
* - Support structured output
-
* - Support fine-grained MCP control
- :ref:`mcp`
* - Support agent-controlled tools management (Meta tool)
- :ref:`tool`
* - Support self-controlled long-term memory
- :ref:`long-term-memory`
* - Support automatic state management
- :ref:`state`
Due to limited space, in this tutorial we only demonstrate the first three
features of ``ReActAgent`` class, leaving the others to the corresponding sections
listed above.
"""
import asyncio
import json
import os
from datetime import datetime
import time
from pydantic import BaseModel, Field
from agentscope.agent import ReActAgent
from agentscope.formatter import DashScopeChatFormatter
from agentscope.memory import InMemoryMemory
from agentscope.message import TextBlock, Msg
from agentscope.model import DashScopeChatModel
from agentscope.tool import Toolkit, ToolResponse
# %%
# Realtime Steering
# ---------------------------------------
#
# The realtime steering allows user to interrupt the agent's reply at any time,
# which is implemented based on the asyncio cancellation mechanism.
#
# Specifically, when calling the ``interrupt`` method of the agent, it will
# cancel the current reply task, and execute the ``handle_interrupt`` method
# for postprocessing.
#
# .. hint:: With the feature of supporting streaming tool results in
# :ref:`tool`, users can interrupt the tool execution if it takes too long or
# deviates from user expectations by Ctrl+C in the terminal or calling the
# ``interrupt`` method of the agent in your code.
#
# The interruption logic has been implemented in the ``AgentBase`` class as a
# basic feature, leaving a ``handle_interrupt`` method for users to customize the
# post-processing of interruption as follows:
#
# .. code-block:: python
#
# # code snippet of AgentBase
# class AgentBase:
# ...
# async def __call__(self, *args: Any, **kwargs: Any) -> Msg:
# ...
# reply_msg: Msg | None = None
# try:
# self._reply_task = asyncio.current_task()
# reply_msg = await self.reply(*args, **kwargs)
#
# except asyncio.CancelledError:
# # Catch the interruption and handle it by the handle_interrupt method
# reply_msg = await self.handle_interrupt(*args, **kwargs)
#
# ...
#
# @abstractmethod
# async def handle_interrupt(self, *args: Any, **kwargs: Any) -> Msg:
# pass
#
#
# In ``ReActAgent`` class, we return a fixed message "I noticed that you have
# interrupted me. What can I do for you?" as follows:
#
# .. figure:: ../../_static/images/interruption_en.gif
# :width: 100%
# :align: center
# :class: bordered-image
# :alt: Example of interruption
#
# Example of interruption
#
# You can override it with your own implementation, for example, calling the LLM
# to generate a simple response to the interruption.
#
#
# Memory Compression
# ----------------------------------------
# As conversations grow longer, the token count in memory can exceed model context
# limits or slow down inference. ``ReActAgent`` provides an automatic memory compression
# feature to address this issue.
#
# **Basic Usage**
#
# To enable memory compression, provide a ``CompressionConfig`` instance when initializing
# the ``ReActAgent``:
#
# .. code-block:: python
#
# from agentscope.agent import ReActAgent
# from agentscope.token import CharTokenCounter
#
# agent = ReActAgent(
# name="Assistant",
# sys_prompt="You are a helpful assistant.",
# model=model,
# formatter=formatter,
# compression_config=ReActAgent.CompressionConfig(
# enable=True,
# agent_token_counter=CharTokenCounter(), # The token counter for the agent
# trigger_threshold=10000, # Trigger compression when exceeding 10000 tokens
# keep_recent=3, # Keep the most recent 3 messages uncompressed
# ),
# )
#
# When memory compression is enabled, the agent monitors the token count in its memory.
# Once it exceeds the ``trigger_threshold``, the agent automatically:
#
# 1. Identifies messages that haven't been compressed yet (via ``exclude_mark``)
# 2. Keeps the most recent ``keep_recent`` messages uncompressed (to preserve recent context)
# 3. Sends older messages to an LLM to generate a structured summary
# 4. Marks the compressed messages with ``MemoryMark.COMPRESSED`` (via ``update_messages_mark``)
# 5. Stores the summary in memory (via ``update_compressed_summary``)
#
# .. important:: The compression uses a **marking mechanism** rather than replacing messages. Old messages are marked as compressed and excluded from future retrievals via ``exclude_mark=MemoryMark.COMPRESSED``, while the generated summary is stored separately and retrieved when needed. This approach preserves the original messages and allows flexible memory management. For more details about the mark functionality, please refer to :ref:`memory`.
#
# By default, the compressed summary is structured into five key fields:
#
# - **task_overview**: The user's core request and success criteria
# - **current_state**: What has been completed so far, including files and outputs
# - **important_discoveries**: Technical constraints, decisions, errors, and failed approaches
# - **next_steps**: Specific actions needed to complete the task
# - **context_to_preserve**: User preferences, domain details, and promises made
#
# **Customizing Compression**
#
# You can customize how compression works by specifying ``summary_schema``,
# ``summary_template``, and ``compression_prompt`` parameters.
#
# - **compression_prompt**: Guides the LLM on how to generate the summary
# - **summary_schema**: Defines the structure of the compressed summary using a Pydantic model
# - **summary_template**: Formats how the compressed summary is presented back to the agent
#
# Here's an example of customizing the compression:
#
# .. code-block:: python
#
# from pydantic import BaseModel, Field
#
# # Define a custom summary structure
# class CustomSummary(BaseModel):
# main_topic: str = Field(
# max_length=200,
# description="The main topic of the conversation"
# )
# key_points: str = Field(
# max_length=400,
# description="Important points discussed"
# )
# pending_tasks: str = Field(
# max_length=200,
# description="Tasks that remain to be done"
# )
#
# # Create agent with custom compression configuration
# agent = ReActAgent(
# name="Assistant",
# sys_prompt="You are a helpful assistant.",
# model=model,
# formatter=formatter,
# compression_config=ReActAgent.CompressionConfig(
# enable=True,
# agent_token_counter=CharTokenCounter(),
# trigger_threshold=10000,
# keep_recent=3,
# # Custom schema for structured summary
# summary_schema=CustomSummary,
# # Custom prompt to guide compression
# compression_prompt=(
# "<system-hint>Please summarize the above conversation "
# "focusing on the main topic, key discussion points, "
# "and any pending tasks.</system-hint>"
# ),
# # Custom template to format the summary
# summary_template=(
# "<system-info>Conversation Summary:\n"
# "Main Topic: {main_topic}\n\n"
# "Key Points:\n{key_points}\n\n"
# "Pending Tasks:\n{pending_tasks}"
# "</system-info>"
# ),
# ),
# )
#
# The ``summary_template`` uses the fields defined in ``summary_schema`` as placeholders
# (e.g., ``{main_topic}``, ``{key_points}``). After the LLM generates the structured summary,
# these placeholders will be replaced with the actual values.
#
# .. note:: The agent ensures that tool use and tool result pairs are kept together during compression to maintain the integrity of the conversation flow.
#
# .. tip:: You can use a smaller, faster model for compression by specifying a different ``compression_model`` and ``compression_formatter`` to reduce costs and latency.
#
#
#
# Parallel Tool Calls
# ----------------------------------------
# ``ReActAgent`` supports parallel tool calls by providing a ``parallel_tool_calls``
# argument in its constructor.
# When multiple tool calls are generated, and ``parallel_tool_calls`` is set to ``True``,
# they will be executed in parallel by the ``asyncio.gather`` function.
#
# .. note:: The parallel tool execution in ``ReActAgent`` is implemented based on ``asyncio.gather``. Therefore, to maximize the effect of parallel tool execution, both the tool function itself and the logic within it must be asynchronous.
#
# .. note:: When running, please ensure that parallel tool calling is supported at the model level and the corresponding parameters are set correctly (can be passed through ``generate_kwargs``). For example, for the DashScope API, you need to set ``parallel_tool_calls`` to ``True``, otherwise parallel tool calling will not be possible.
# prepare a tool function
async def example_tool_function(tag: str) -> ToolResponse:
"""A sample example tool function"""
start_time = datetime.now().strftime("%H:%M:%S.%f")
# Sleep for 3 seconds to simulate a long-running task
await asyncio.sleep(3)
end_time = datetime.now().strftime("%H:%M:%S.%f")
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Tag {tag} started at {start_time} and ended at {end_time}. ",
),
],
)
toolkit = Toolkit()
toolkit.register_tool_function(example_tool_function)
# Create an ReAct agent
agent = ReActAgent(
name="Jarvis",
sys_prompt="You're a helpful assistant named Jarvis.",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
# Preset the generation kwargs to enable parallel tool calls
generate_kwargs={
"parallel_tool_calls": True,
},
),
memory=InMemoryMemory(),
formatter=DashScopeChatFormatter(),
toolkit=toolkit,
parallel_tool_calls=True,
)
async def example_parallel_tool_calls() -> None:
"""Example of parallel tool calls"""
# prompt the agent to generate two tool calls at once
await agent(
Msg(
"user",
"Generate two tool calls of the 'example_tool_function' function with tag as 'tag1' and 'tag2' AT ONCE so that they can execute in parallel.",
"user",
),
)
asyncio.run(example_parallel_tool_calls())
# %%
# Structured Output
# ----------------------------------------
# To generate a structured output, the ``ReActAgent`` instance receives a child class
# of the ``pydantic.BaseModel`` as the ``structured_model`` argument in its ``__call__`` function.
# Then we can get the structured output from the ``metadata`` field of the returned message.
#
#
# Taking introducing Einstein as an example:
#
# Create an ReAct agent
agent = ReActAgent(
name="Jarvis",
sys_prompt="You're a helpful assistant named Jarvis.",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
# Preset the generation kwargs to enable parallel tool calls
generate_kwargs={
"parallel_tool_calls": True,
},
),
memory=InMemoryMemory(),
formatter=DashScopeChatFormatter(),
toolkit=Toolkit(),
parallel_tool_calls=True,
)
# The structured model
class Model(BaseModel):
name: str = Field(description="The name of the person")
description: str = Field(
description="A one-sentence description of the person",
)
age: int = Field(description="The age")
honor: list[str] = Field(description="A list of honors of the person")
async def example_structured_output() -> None:
"""The example structured output"""
res = await agent(
Msg(
"user",
"Introduce Einstein",
"user",
),
structured_model=Model,
)
print("\nThe structured output:")
print(json.dumps(res.metadata, indent=4))
asyncio.run(example_structured_output())
# %%
# Customizing Agent
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope provides two base classes, ``AgentBase`` and ``ReActAgentBase``, which
# differ in the abstract methods they define and the hooks they support.
# Specifically, the ``ReActAgentBase`` extends ``AgentBase`` with additional ``_reasoning`` and ``_acting``
# abstract methods, as well as their pre- and post- hooks.
#
# Developers can choose to inherit from either of these base classes based on their needs.
# We summarize the agent under ``agentscope.agent`` module as follows:
#
# .. list-table:: Agent classes in AgentScope
# :header-rows: 1
#
# * - Class
# - Abstract Method
# - Support Hooks
# - Description
# * - ``AgentBase``
# - | ``reply``
# | ``observe``
# | ``print``
# | ``handle_interrupt``
# - | pre\_/post_reply
# | pre\_/post_observe
# | pre\_/post_print
# - The base class for all agents, providing the basic interface and hooks.
# * - ``ReActAgentBase``
# - | ``reply``
# | ``observe``
# | ``print``
# | ``handle_interrupt``
# | ``_reasoning``
# | ``_acting``
# - | pre\_/post_reply
# | pre\_/post_observe
# | pre\_/post_print
# | pre\_/post_reasoning
# | pre\_/post_acting
# - The abstract class for ReAct agent, extending ``AgentBase`` with reasoning and acting abstract methods and their hooks.
# * - ``ReActAgent``
# - \-
# - | pre\_/post_reply
# | pre\_/post_observe
# | pre\_/post_print
# | pre\_/post_reasoning
# | pre\_/post_acting
# - An implementation of ``ReActAgentBase``
# * - ``UserAgent``
# -
# -
# - A special agent that represents the user, used to interact with the agent
# * - ``A2aAgent``
# - \-
# - | pre\_/post_reply
# | pre\_/post_observe
# | pre\_/post_print
# - Agent for communicating with remote A2A agents, see :ref:`a2a`
#
#
#
# Further Reading
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# - :ref:`tool`
# - :ref:`hook`
# - :ref:`a2a`
#

View File

@@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
"""
.. _agent_skill:
Agent Skill
============================
`Agent skill <https://claude.com/blog/skills>`_ is an approach proposed by
Anthropic to improve agent capabilities on specific tasks.
AgentScope provides built-in support for Agent Skills through the ``Toolkit``
class, allowing users to easily register and manage agent skills.
The related APIs are as follows:
.. list-table:: Agent skill API in ``Toolkit`` class
:header-rows: 1
* - API
- Description
* - ``register_agent_skill``
- Register agent skills from a given directory.
* - ``remove_agent_skill``
- Remove a registered agent skill by name.
* - ``get_agent_skill_prompt``
- Get the prompt for all registered agent skills, which can be
attached to the system prompt for the agent.
In this section we demonstrate how to register agent skills and use them in an
ReAct agent.
"""
import os
from agentscope.agent import ReActAgent
from agentscope.formatter import DashScopeChatFormatter
from agentscope.memory import InMemoryMemory
from agentscope.model import DashScopeChatModel
from agentscope.tool import Toolkit
# %%
# Registering Agent Skills
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# First, we need to prepare an agent skill directory, which follows the
# requirements specified in the `Anthropic blog <https://claude.com/blog/skills>`_.
#
# .. note:: The skill directory must contain a ``SKILL.md`` file containing
# YAML frontmatter and instructions.
#
# Here, we fake an example skill directory ``sample_skill`` with the following files:
#
# .. code-block:: markdown
#
# ---
# name: sample_skill
# description: A sample agent skill for demonstration.
# ---
#
# # Sample Skill
# ...
#
os.makedirs("sample_skill", exist_ok=True)
with open("sample_skill/SKILL.md", "w", encoding="utf-8") as f:
f.write(
"""---
name: sample_skill
description: A sample agent skill for demonstration.
---
# Sample Skill
...
""",
)
# %%
# Then, we can register the skill using the ``register_agent_skill`` API of
# the ``Toolkit`` class.
#
toolkit = Toolkit()
toolkit.register_agent_skill("sample_skill")
# %%
# After that, we can get the prompt for all registered agent skills using the
# ``get_agent_skill_prompt`` API
agent_skill_prompt = toolkit.get_agent_skill_prompt()
print("Agent Skill Prompt:")
print(agent_skill_prompt)
# %%
# Of course, we can customize the prompt template when creating the ``Toolkit``
# instance.
toolkit = Toolkit(
# The instruction that introduces how to use the skill to the agent/llm
agent_skill_instruction="<system-info>You're provided a collection of skills, each in a directory and described by a SKILL.md file.</system-info>\n",
# The template for formatting each skill's prompt, must contain
# {name}, {description}, and {dir} fields
agent_skill_template="- {name}({dir}): {description}",
)
toolkit.register_agent_skill("sample_skill")
agent_skill_prompt = toolkit.get_agent_skill_prompt()
print("Customized Agent Skill Prompt:")
print(agent_skill_prompt)
# %%
# Integrating Agent Skills with ReActAgent
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# The `ReActAgent` class in AgentScope will attach the agent skill prompt to
# the system prompt automatically.
#
# We can create a ReAct agent with the registered agent skills as follows:
#
# .. important:: When using agent skills, the agent must be equipped with text
# file reading or shell command tools to access the skill instructions in
# `SKILL.md` files.
#
agent = ReActAgent(
name="Friday",
sys_prompt="You are a helpful assistant named Friday.",
model=DashScopeChatModel(
model_name="qwen3-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
),
memory=InMemoryMemory(),
formatter=DashScopeChatFormatter(),
toolkit=toolkit,
)
print("Agent's System Prompt with Agent Skills:")
print(agent.sys_prompt)

View File

@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
"""
.. _embedding:
Embedding
=========================
In AgentScope, the embedding module provides a unified interface for vector representation generation, which features:
- Support **caching embeddings** to avoid redundant API calls
- Support **multiple embedding providers** with a consistent API
AgentScope has built-in embedding classes for the following API providers:
.. list-table::
:header-rows: 1
* - Provider
- Class
* - OpenAI
- ``OpenAITextEmbedding``
* - Gemini
- ``GeminiTextEmbedding``
* - DashScope
- ``DashScopeTextEmbedding``, ``DashScopeMultiModalEmbedding``
* - Ollama
- ``OllamaTextEmbedding``
All classes inherit from ``EmbeddingModelBase``, implementing the ``__call__`` method and generating ``EmbeddingResponse`` object with the embeddings and usage information.
The ``DashScopeMultiModalEmbedding`` supports multi-modal embeddings for text, images, and videos.
Taking the DashScope embedding class as an example, you can use it as follows:
"""
import asyncio
import os
import tempfile
from agentscope.embedding import DashScopeTextEmbedding, FileEmbeddingCache
async def example_dashscope_embedding() -> None:
"""Example usage of DashScope text embedding."""
texts = [
"What is the capital of France?",
"Paris is the capital city of France.",
]
# Initialize the DashScope text embedding instance
embedding_model = DashScopeTextEmbedding(
model_name="text-embedding-v2",
api_key=os.getenv("DASHSCOPE_API_KEY"),
)
# Get the embedding from the model
response = await embedding_model(texts)
print("The embedding ID: ", response.id)
print("The embedding create at: ", response.created_at)
print("The embedding usage: ", response.usage)
print("The embedding:")
print(response.embeddings)
asyncio.run(example_dashscope_embedding())
# %%
# You can customize your embedding model by subclassing ``EmbeddingModelBase`` and implementing the ``__call__`` method.
#
# Embedding Cache
# ---------------------
# AgentScope provides a base class ``EmbeddingCacheBase`` for caching embeddings, as well as a file-based implementation ``FileEmbeddingCache``.
# It works as follows in the embedding module:
#
# .. image:: ../../_static/images/embedding_cache.png
# :align: center
# :width: 90%
#
# To use caching, just pass an instance of ``FileEmbeddingCache`` (or your custom cache) to the embedding model's constructor as follows:
#
async def example_embedding_cache() -> None:
"""Demonstrate embedding with cache functionality."""
# Example texts
texts = [
"What is the capital of France?",
"Paris is the capital city of France.",
]
# Create a temporary directory for cache demonstration
# In real applications, you might want to use a persistent directory
cache_dir = tempfile.mkdtemp(prefix="embedding_cache_")
print(f"Using cache directory: {cache_dir}")
# Initialize the embedding model with cache
# We limit the cache to 100 files and 10MB for demonstration purposes
embedder = DashScopeTextEmbedding(
model_name="text-embedding-v3",
api_key=os.getenv("DASHSCOPE_API_KEY"),
embedding_cache=FileEmbeddingCache(
cache_dir=cache_dir,
max_file_number=100,
max_cache_size=10, # Maximum cache size in MB
),
)
# First call - will fetch from API and store in cache
print("\n=== First API Call (No Cache Hit) ===")
start_time = asyncio.get_event_loop().time()
response1 = await embedder(texts)
elapsed_time1 = asyncio.get_event_loop().time() - start_time
print(f"Source: {response1.source}") # Should be 'api'
print(f"Time taken: {elapsed_time1:.4f} seconds")
print(f"Tokens used: {response1.usage.tokens}")
# Second call with the same texts - should use cache
print("\n=== Second API Call (Cache Hit Expected) ===")
start_time = asyncio.get_event_loop().time()
response2 = await embedder(texts)
elapsed_time2 = asyncio.get_event_loop().time() - start_time
print(f"Source: {response2.source}") # Should be 'cache'
print(f"Time taken: {elapsed_time2:.4f} seconds")
print(
f"Tokens used: {response2.usage.tokens}",
) # Should be 0 for cached results
print(
f"Speed improvement: {elapsed_time1 / elapsed_time2:.1f}x faster with cache",
)
asyncio.run(example_embedding_cache())

View File

@@ -0,0 +1,257 @@
# -*- coding: utf-8 -*-
"""
.. _eval:
Evaluation
=========================
AgentScope provides a built-in evaluation framework for assessing agent performance across different tasks and benchmarks, featuring:
- `Ray <https://github.com/ray-project/ray>`_-based parallel and distributed evaluation
- Support continuation after interruption
- 🚧 Visualization of evaluation results
.. note:: We are keeping integrating new benchmarks into AgentScope:
- ✅ `ACEBench <https://github.com/ACEBench/ACEBench>`_
- 🚧 `GAIA <https://huggingface.co/datasets/gaia-benchmark/GAIA/tree/main>`_ Benchmark
Overview
---------------------------
The AgentScope evaluation framework consists of several key components:
- **Benchmark**: Collections of tasks for systematic evaluation
- **Task**: Individual evaluation units with inputs, ground truth, and metrics
- **Metric**: Measurement functions that assess solution quality
- **Evaluator**: Engine that runs evaluation, aggregates results, and analyzes performance
- **Evaluator Storage**: Persistent storage for recording and retrieving evaluation results
- **Solution**: The user-defined solution
.. figure:: ../../_static/images/evaluation.png
:width: 90%
:alt: AgentScope Evaluation Framework
*AgentScope Evaluation Framework*
The current implementation in AgentScope includes:
- Evaluator:
- ``RayEvaluator``: A ray-based evaluator that supports parallel and distributed evaluation.
- ``GeneralEvaluator``: A general evaluator that runs tasks sequentially, friendly for debugging.
- Benchmark:
- ``ACEBench``: A benchmark for evaluating agent capabilities.
We have provided a toy example in our `GitHub repository <https://github.com/agentscope-ai/agentscope/tree/main/examples/evaluation/ace_bench>`_ with ``RayEvaluator`` and the agent multistep tasks in ACEBench.
Core Components
---------------
We are going to build a simple toy math question benchmark to demonstrate
how to use the AgentScope evaluation module.
"""
TOY_BENCHMARK = [
{
"id": "math_problem_1",
"question": "What is 2 + 2?",
"ground_truth": 4.0,
"tags": {
"difficulty": "easy",
"category": "math",
},
},
{
"id": "math_problem_2",
"question": "What is 12345 + 54321 + 6789 + 9876?",
"ground_truth": 83331,
"tags": {
"difficulty": "medium",
"category": "math",
},
},
]
# %%
# From Tasks, Solutions and Metrics to Benchmark
# ~~~~~~~~~~~~~~~~~~~
#
# - A ``SolutionOutput`` contains all information generated by the agent, including the trajectory and final output.
# - A ``Metric`` represents a single evaluation callable instance that compares the generated solution (e.g., trajectory or final output) to the ground truth.
# In the toy example, we define a metric that simply checks whether the ``output`` field in the solution matches the ground truth.
from agentscope.evaluate import (
SolutionOutput,
MetricBase,
MetricResult,
MetricType,
)
class CheckEqual(MetricBase):
def __init__(
self,
ground_truth: float,
):
super().__init__(
name="math check number equal",
metric_type=MetricType.NUMERICAL,
description="Toy metric checking if two numbers are equal",
categories=[],
)
self.ground_truth = ground_truth
async def __call__(
self,
solution: SolutionOutput,
) -> MetricResult:
if solution.output == self.ground_truth:
return MetricResult(
name=self.name,
result=1.0,
message="Correct",
)
else:
return MetricResult(
name=self.name,
result=0.0,
message="Incorrect",
)
# %%
# - A ``Task`` is a unit in the benchmark that includes all information for the agent to execute and evaluate (e.g., input/query and its ground truth).
# - A ``Benchmark`` organizes multiple tasks for systematic evaluation.
from typing import Generator
from agentscope.evaluate import (
Task,
BenchmarkBase,
)
class ToyBenchmark(BenchmarkBase):
def __init__(self):
super().__init__(
name="Toy bench",
description="A toy benchmark for demonstrating the evaluation module.",
)
self.dataset = self._load_data()
@staticmethod
def _load_data() -> list[Task]:
dataset = []
for item in TOY_BENCHMARK:
dataset.append(
Task(
id=item["id"],
input=item["question"],
ground_truth=item["ground_truth"],
tags=item.get("tags", {}),
metrics=[
CheckEqual(item["ground_truth"]),
],
metadata={},
),
)
return dataset
def __iter__(self) -> Generator[Task, None, None]:
"""Iterate over the benchmark."""
for task in self.dataset:
yield task
def __getitem__(self, index: int) -> Task:
"""Get a task by index."""
return self.dataset[index]
def __len__(self) -> int:
"""Get the length of the benchmark."""
return len(self.dataset)
# %%
# Evaluators
# ~~~~~~~~~~
#
# Evaluators manage the evaluation process. They can automatically iterate through the
# tasks in the benchmark and feed each task into a solution-generation function,
# where developers need to define the logic for running agents and retrieving
# the execution result and trajectory. Below is an example of
# running ``GeneralEvaluator`` with our toy benchmark. If there is a large
# benchmark and the developer wants to get the evaluation more efficiently
# through parallelization, ``RayEvaluator`` is available as a built-in solution
# as well.
import os
import asyncio
from typing import Callable
from pydantic import BaseModel
from agentscope.message import Msg
from agentscope.model import DashScopeChatModel
from agentscope.formatter import DashScopeChatFormatter
from agentscope.agent import ReActAgent
from agentscope.evaluate import (
GeneralEvaluator,
FileEvaluatorStorage,
)
class ToyBenchAnswerFormat(BaseModel):
answer_as_number: float
async def toy_solution_generation(
task: Task,
pre_hook: Callable,
) -> SolutionOutput:
agent = ReActAgent(
name="Friday",
sys_prompt="You are a helpful assistant named Friday. "
"Your target is to solve the given task with your tools. "
"Try to solve the task as best as you can.",
model=DashScopeChatModel(
api_key=os.environ.get("DASHSCOPE_API_KEY"),
model_name="qwen-max",
stream=False,
),
formatter=DashScopeChatFormatter(),
)
agent.register_instance_hook(
"pre_print",
"save_logging",
pre_hook,
)
msg_input = Msg("user", task.input, role="user")
res = await agent(
msg_input,
structured_model=ToyBenchAnswerFormat,
)
return SolutionOutput(
success=True,
output=res.metadata.get("answer_as_number", None),
trajectory=[],
)
async def main() -> None:
evaluator = GeneralEvaluator(
name="Toy benchmark evaluation",
benchmark=ToyBenchmark(),
# Repeat how many times
n_repeat=1,
storage=FileEvaluatorStorage(
save_dir="./results",
),
# How many workers to use
n_workers=1,
)
# Run the evaluation
await evaluator.run(toy_solution_generation)
asyncio.run(main())

View File

@@ -0,0 +1,341 @@
# -*- coding: utf-8 -*-
"""
Evaluation with OpenJudge
=========================
This guide introduces how to use [OpenJudge](https://github.com/agentscope-ai/OpenJudge) graders as AgentScope metrics to evaluate your multi-agent applications.
OpenJudge is a comprehensive evaluation system designed to assess the quality of LLM applications. By integrating OpenJudge into AgentScope, you can extend AgentScope's native evaluation capabilities from basic execution checks to deep, semantic quality analysis.
.. note::
Install dependencies before running:
.. code-block:: bash
pip install agentscope py-openjudge
Overview
--------
While AgentScope provides a robust `MetricBase` for defining evaluation logic, implementing complex, semantic-level metrics (like "Hallucination Detection" or "Response Relevance") often requires
significant effort in prompt engineering and pipeline construction.
Integrating OpenJudge brings three dimensions of capability extension to AgentScope:
1. **Enhance Evaluation Depth:**: Move beyond simple success/failure checks to multi-dimensional assessments (Accuracy, Safety, Tone, etc.).
2. **Leverage Verified Graders**: Instantly access 50+ pre-built, expert-level graders without writing custom evaluation prompts, see the [OpenJudge documentation](https://agentscope-ai.github.io/OpenJudge/built_in_graders/overview/) for details.
3. **Closed-loop Iteration**: Seamlessly embed OpenJudge into AgentScope's `Benchmark`, obtaining quantitative scores and qualitative reasoning.
How to Evaluate with OpenJudge
--------------------
We are going to build a simple QA benchmark to demonstrate how to use the AgentScope evaluation module by integrating OpenJudge's graders.
"""
# %%
QA_BENCHMARK_DATASET = [
{
"id": "qa_task_1",
"question": "What are the health benefits of regular exercise?",
"reference_output": "Regular exercise improves cardiovascular health, strengthens muscles and bones, "
"helps maintain a healthy weight, and can improve mental health by reducing anxiety and depression.",
"ground_truth": "Answers should cover physical and mental health benefits",
"difficulty": "medium",
"category": "health",
},
{
"id": "qa_task_2",
"question": "Describe the main causes of climate change.",
"reference_output": "Climate change is primarily caused by increased concentrations of greenhouse gases "
"in the atmosphere due to human activities like burning fossil fuels, deforestation, and industrial processes.",
"ground_truth": "Answers should mention greenhouse gases and human activities",
"difficulty": "hard",
"category": "environment",
},
{
"id": "qa_task_3",
"question": "What is the significance of the Turing Test in AI?",
"reference_output": "The Turing Test, proposed by Alan Turing, is a measure of a machine's ability to exhibit"
" intelligent behavior equivalent to, or indistinguishable from, that of a human.",
"ground_truth": "Should mention Alan Turing, purpose of the test, and its implications for AI",
"difficulty": "hard",
"category": "technology",
},
]
# %% [markdown]
# AgentScope Metric vs. OpenJudge Grader
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# To make OpenJudge compatible with AgentScope, we need an adapter that inherits from
# AgentScope's ``MetricBase`` and acts as a bridge to OpenJudge's ``BaseGrader``.
#
# * **AgentScope Metric**: A generic unit of evaluation that accepts a ``SolutionOutput`` and returns a ``MetricResult``.
# * **OpenJudge Grader**: A specialized evaluation unit (e.g., ``RelevanceGrader``) that requires specific, semantic inputs (like ``query``, ``response``, ``context``), and returns a ``GraderResult``.
#
# This "Adapter" allows you to plug *any* OpenJudge grader into your AgentScope benchmark seamlessly.
#
# %%
from openjudge.graders.base_grader import BaseGrader
from openjudge.graders.schema import GraderScore, GraderError
from openjudge.utils.mapping import parse_data_with_mapper
from agentscope.evaluate import (
MetricBase,
MetricType,
MetricResult,
SolutionOutput,
)
class OpenJudgeMetric(MetricBase):
"""
A wrapper that converts an OpenJudge grader into an AgentScope Metric.
"""
def __init__(
self,
grader_cls: type[BaseGrader],
data: dict,
mapper: dict,
name: str | None = None,
description: str | None = None,
**grader_kwargs,
):
# Initialize the OpenJudge grader
self.grader = grader_cls(**grader_kwargs)
super().__init__(
name=name or self.grader.name,
metric_type=MetricType.NUMERICAL,
description=description or self.grader.description,
)
self.data = data
self.mapper = mapper
async def __call__(self, solution: SolutionOutput) -> MetricResult:
"""Execute the wrapped OpenJudge grader against the agent solution."""
if not solution.success:
return MetricResult(
name=self.name,
result=0.0,
message="Solution failed",
)
try:
# 1. Context Construction
# Combine Static Task Context (item) and Dynamic Agent Output (solution)
combined_data = {
"data": self.data,
"solution": {
"output": solution.output,
"meta": solution.meta,
"trajectory": getattr(solution, "trajectory", []),
},
}
# 2. Data Mapping
# Use the mapper to extract 'query', 'response', 'context' from the combined data
grader_inputs = parse_data_with_mapper(
combined_data,
self.mapper,
)
# 3. Evaluation Execution
result = await self.grader.aevaluate(**grader_inputs)
# 4. Result Formatting
if isinstance(result, GraderScore):
return MetricResult(
name=self.name,
result=result.score,
# We preserve the detailed reasoning provided by OpenJudge
message=result.reason or "",
)
elif isinstance(result, GraderError):
return MetricResult(
name=self.name,
result=0.0,
message=f"Error: {result.error}",
)
else:
return MetricResult(
name=self.name,
result=0.0,
message="Unknown result type",
)
except Exception as e:
return MetricResult(
name=self.name,
result=0.0,
message=f"Exception: {str(e)}",
)
# %% [markdown]
# From OpenJudge's Graders to AgentScope's Benchmark
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# OpenJudge provides a rich collection of built-in graders. In this example, we select two
# common graders suitable for Question-Answering tasks:
#
# * **RelevanceGrader**: Evaluates whether the agent's response directly addresses the user's query.
# * **CorrectnessGrader**: Verifies the factual accuracy of the response against a provided ground truth.
#
# .. tip::
# OpenJudge offers 50+ built-in graders covering diverse dimensions like **Hallucination**, **Safety**, **Code Quality**,
# and **JSON Formatting**. Please refer to the `OpenJudge Documentation <https://agentscope-ai.github.io/OpenJudge/built_in_graders/overview/>`_
# for the full list of available graders.
#
# .. note::
# Ensure you have set your ``DASHSCOPE_API_KEY`` environment variable before running the example below.
# %%
import os
from typing import Generator
from openjudge.graders.common.relevance import RelevanceGrader
from openjudge.graders.common.correctness import CorrectnessGrader
from agentscope.evaluate import (
Task,
BenchmarkBase,
)
class QABenchmark(BenchmarkBase):
"""A benchmark for QA tasks using OpenJudge metrics."""
def __init__(self):
super().__init__(
name="QA Quality Benchmark",
description="Benchmark to evaluate QA systems using OpenJudge grader classes",
)
self.dataset = self._load_data()
def _load_data(self):
tasks = []
# Configuration for LLM-based graders
# Ensure OPENAI_API_KEY is set in your environment variables
model_config = {
"model": "qwen3-32b",
"api_key": os.environ.get("DASHSCOPE_API_KEY"),
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
}
for data in QA_BENCHMARK_DATASET:
# Define the Mapping: Left is OpenJudge key, Right is AgentScope path
mapper = {
"query": "data.input",
"response": "solution.output",
"context": "data.ground_truth",
"reference_response": "data.reference_output",
}
# Instantiate Metrics via Wrapper
metrics = [
OpenJudgeMetric(
grader_cls=RelevanceGrader,
data=data,
mapper=mapper,
name="Relevance",
model=model_config,
),
OpenJudgeMetric(
grader_cls=CorrectnessGrader,
data=data,
mapper=mapper,
name="Correctness",
model=model_config,
),
]
# Create Task
task = Task(
id=data["id"],
input=data["question"],
ground_truth=data["ground_truth"],
metrics=metrics,
)
tasks.append(task)
return tasks
def __iter__(self) -> Generator[Task, None, None]:
"""Iterate over the benchmark."""
yield from self.dataset
def __getitem__(self, index: int) -> Task:
"""Get a task by index."""
return self.dataset[index]
def __len__(self) -> int:
"""Get the length of the benchmark."""
return len(self.dataset)
# %% [markdown]
# Run Evaluation
# ~~~~~~~~~~
# Finally, use AgentScope's ``GeneralEvaluator`` to run the benchmark on a QA agent.
# The results will include both the **Quantitative Score** and the **Qualitative Reasoning**
# from the OpenJudge graders.
# %%
from typing import Callable
from agentscope.agent import ReActAgent
from agentscope.evaluate import GeneralEvaluator
from agentscope.evaluate import FileEvaluatorStorage
from agentscope.formatter import DashScopeChatFormatter
from agentscope.message import Msg
from agentscope.model import OpenAIChatModel
async def qa_agent(task: Task, pre_hook: Callable) -> SolutionOutput:
"""Solution function that generates answers to QA tasks."""
model = OpenAIChatModel(
model_name="qwen3-32b",
api_key=os.getenv("DASHSCOPE_API_KEY"),
)
# Create a QA agent
agent = ReActAgent(
name="QAAgent",
sys_prompt="You are an expert at answering questions. Provide clear, accurate, and comprehensive answers.",
model=model,
formatter=DashScopeChatFormatter(),
)
# Generate response
msg_input = Msg(name="User", content=task.input, role="user")
response = await agent(msg_input)
response_text = response.content
return SolutionOutput(
success=True,
output=response_text,
trajectory=[
task.input,
response_text,
], # Store the interaction trajectory
)
async def main() -> None:
evaluator = GeneralEvaluator(
name="OpenJudge Integration Demo",
benchmark=QABenchmark(),
# Repeat how many times
n_repeat=1,
storage=FileEvaluatorStorage(
save_dir="./results",
),
# How many workers to use
n_workers=1,
)
await evaluator.run(qa_agent)

View File

@@ -0,0 +1,291 @@
# -*- coding: utf-8 -*-
"""
.. _hook:
Agent Hooks
===========================
Hooks are extension points in AgentScope that allow developers to customize agent behaviors at specific execution points, providing a flexible way to modify or extend the agent's functionality without changing its core implementation.
In AgentScope, hooks are implemented around the agent's core functions:
.. list-table:: Supported hook types in AgentScope
:header-rows: 1
* - Agent Class
- Core Function
- Hook Types
- Description
* - | ``AgentBase`` &
| its child classes
- ``reply``
- | ``pre_reply``
| ``post_reply``
- The hooks before/after agent replying to a message
* -
- ``print``
- | ``pre_print``
| ``post_print``
- The hook before/after printing a message to the target output (e.g., terminal, web interface)
* -
- ``observe``
- | ``pre_observe``
| ``post_observe``
- The hooks before/after observing a message from the environment or other agents
* - | ``ReActAgentBase`` &
| its child classes
- | ``reply``
| ``print``
| ``observe``
- | ``pre_reply``
| ``post_reply``
| ``pre_print``
| ``post_print``
| ``pre_observe``
| ``post_observe``
-
* -
- ``_reasoning``
- | ``pre_reasoning``
| ``post_reasoning``
- The hooks before/after the agent's reasoning process
* -
- ``_acting``
- | ``pre_acting``
| ``post_acting``
- The hooks before/after the agent's acting process
.. tip:: Since hooks in AgentScope are implemented using a metaclass, they support inheritance.
To simplify the usage, AgentScope provides unified signatures for all hooks.
"""
import asyncio
from typing import Any, Type
from agentscope.agent import ReActAgentBase, AgentBase
from agentscope.message import Msg
# %%
# Hook Signature
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# AgentScope provides unified hook signatures for all pre- and post-hooks as follows:
#
# **Pre-Hook Signature**
#
# .. list-table:: The signature of all pre-hooks
# :header-rows: 1
#
# * -
# - Name
# - Description
# * - Arguments
# - ``self: AgentBase | ReActAgentBase``
# - The agent instance
# * -
# - ``kwargs: dict[str, Any]``
# - | The input arguments of the target
# | function, or the modified arguments
# | from the most recent non-None return
# | value of previous hooks
# * - Returns
# - ``dict[str, Any] | None``
# - The modified arguments or None
#
# .. note:: All positional arguments and keyword arguments of the core function are passed as a single ``kwargs`` dict to the hook functions
#
# A pre-hook template is defined as follows:
#
def pre_hook_template(
self: AgentBase | ReActAgentBase,
kwargs: dict[str, Any],
) -> dict[str, Any] | None: # The modified displayed message
"""Pre hook template."""
pass
# %%
# **Post-Hook Signature**
#
# For post hooks, an additional ``output`` argument is added to the signature, which represents the output of the target function.
# If the core function has no output, the ``output`` argument will be ``None``.
#
# .. list-table:: The signature of all post-hooks
# :header-rows: 1
#
# * -
# - Name
# - Description
# * - Arguments
# - ``self: AgentBase | ReActAgentBase``
# - The agent instance
# * -
# - ``kwargs: dict[str, Any]``
# - | A dict that contains all the arguments
# | of the target function
# * -
# - ``output: Any``
# - | The output of the target function or
# | the most recent non-None return value
# | from previous hooks
# * - Returns
# - ``dict[str, Any] | None``
# - The modified arguments or None
#
def post_hook_template(
self: AgentBase | ReActAgentBase,
kwargs: dict[str, Any],
output: Any, # The output of the target function
) -> Any: # The modified output
"""Post hook template."""
pass
# %%
# Hook Management
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# AgentScope provides both instance- and class-level hooks, depending on the effective scope of the hooks.
# They execute in the following order:
#
# .. image:: ../../_static/images/sequential_hook.png
# :width: 90%
# :align: center
# :alt: Hooks in AgentScope
# :class: bordered-image
#
# AgentScope provides built-in methods to manage hooks at both instance and class levels as follows:
#
# .. list-table:: Hook management methods in AgentScope
# :header-rows: 1
#
# * - Level
# - Method
# - Description
# * - Instance-level
# - ``register_instance_hook``
# - | Register a hook for the current object with
# | given hook type and name.
# * -
# - ``remove_instance_hook``
# - | Remove a hook for the current object with
# | given hook type and name.
# * -
# - ``clear_instance_hooks``
# - | Clear all hooks for the current object with
# | given hook type.
# * - Class-level
# - ``register_class_hook``
# - | Register a hook for all objects of the class
# | with given hook type and name.
# * -
# - ``remove_class_hook``
# - | Remove a hook for all objects of the class
# | with given hook type and name.
# * -
# - ``clear_class_hooks``
# - | Clear all hooks for all objects of the
# | class with given hook type.
#
# When using hooks, you MUST follow these rules:
#
# .. important:: **Execution Order**
#
# - Hooks are executed in registration order
# - Multiple hooks can be chained together
# **Return Value Handling**
#
# - For pre-hooks: Non-None return values are passed to the next hook or core function
# - When a hook returns None, the next hook will use the most recent non-None return value from previous hooks
# - If all previous hooks return None, the next hook receives a copy of the original arguments
# - The final non-None return value (or original arguments if all hooks return None) is passed to the core function
# - For post-hooks: Works the same way as pre-hooks.
# **Important**: Never call the core function (reply/speak/observe/_reasoning/_acting) within a hook to avoid infinite loops
#
# Taking the following agent as an example, we can see how to register, remove and clear hooks:
#
# Create a simple test agent class
class TestAgent(AgentBase):
"""A test agent for demonstrating hooks."""
async def reply(self, msg: Msg) -> Msg:
"""Reply to the message."""
return msg
# %%
# We create an instance-level hook and a class-level hook to modify the message content before replying.
#
# Create two pre-reply hooks
def instance_pre_reply_hook(
self: AgentBase,
kwargs: dict[str, Any],
) -> dict[str, Any]:
"""A pre-reply hook that modifies the message content."""
msg = kwargs["msg"]
msg.content += "[instance-pre-reply]"
# return modified kwargs
return {
**kwargs,
"msg": msg,
}
def cls_pre_reply_hook(
self: AgentBase,
kwargs: dict[str, Any],
) -> dict[str, Any]:
"""A pre-reply hook that modifies the message content."""
msg = kwargs["msg"]
msg.content += "[cls-pre-reply]"
# return modified kwargs
return {
**kwargs,
"msg": msg,
}
# Register class hook
TestAgent.register_class_hook(
hook_type="pre_reply",
hook_name="test_pre_reply",
hook=cls_pre_reply_hook,
)
# Register instance hook
agent = TestAgent()
agent.register_instance_hook(
hook_type="pre_reply",
hook_name="test_pre_reply",
hook=instance_pre_reply_hook,
)
async def example_test_hook() -> None:
"""An example function to test the hooks."""
msg = Msg(
name="user",
content="Hello, world!",
role="user",
)
res = await agent(msg)
print("Response content:", res.content)
TestAgent.clear_class_hooks()
asyncio.run(example_test_hook())
# %%
# We can see that a "[instance-pre-reply]" and a "[cls-pre-reply]" are added to the message content.
#

View File

@@ -0,0 +1,435 @@
# -*- coding: utf-8 -*-
"""
.. _long-term-memory:
Long-Term Memory
========================
In AgentScope, we provide a basic class for long-term memory (``LongTermMemoryBase``) and an implementation based on the `mem0 <https://github.com/mem0ai/mem0>`_ library (``Mem0LongTermMemory``).
Together with the design of ``ReActAgent`` class in :ref:`agent` section, we provide two long-term memory modes:
- ``agent_control``: the agent autonomously manages long-term memory by tool calls, and
- ``static_control``: the developer explicitly controls long-term memory operations.
Developers can also use the ``both`` mode, which activates both memory management modes.
.. hint:: These memory modes are suitable for different usage scenarios. Developers can choose the appropriate mode based on their needs.
Using mem0 Long-Term Memory
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. note:: We provide an example of using mem0 long-term memory in the GitHub repository under the ``examples/long_term_memory/mem0`` directory.
"""
import os
import asyncio
from agentscope.message import Msg
from agentscope.memory import InMemoryMemory
from agentscope.agent import ReActAgent
from agentscope.formatter import DashScopeChatFormatter
from agentscope.model import DashScopeChatModel
from agentscope.tool import Toolkit
# Create mem0 long-term memory instance
from agentscope.memory import Mem0LongTermMemory
from agentscope.embedding import DashScopeTextEmbedding
long_term_memory = Mem0LongTermMemory(
agent_name="Friday",
user_name="user_123",
model=DashScopeChatModel(
model_name="qwen-max-latest",
api_key=os.environ.get("DASHSCOPE_API_KEY"),
stream=False,
),
embedding_model=DashScopeTextEmbedding(
model_name="text-embedding-v2",
api_key=os.environ.get("DASHSCOPE_API_KEY"),
),
on_disk=False,
)
# %%
# The ``Mem0LongTermMemory`` class provides two main methods for long-term memory operations:
# ``record`` and ``retrieve``.
# They take a list of messages as input and record/retrieve information from long-term memory.
#
# As an example, we first store a user preference and then retrieve related information from long-term memory.
#
# Basic usage example
async def basic_usage():
"""Basic usage example"""
# Record memory
await long_term_memory.record(
[Msg("user", "I like staying in homestays", "user")],
)
# Retrieve memory
results = await long_term_memory.retrieve(
[Msg("user", "My accommodation preferences", "user")],
)
print(f"Retrieval results: {results}")
asyncio.run(basic_usage())
# %%
# Integration with ReAct Agent
# ----------------------------------------
# In AgentScope, the ``ReActAgent`` class receives a ``long_term_memory``
# parameter in its constructor, as well as a ``long_term_memory_mode`` parameter
# that specifies the long-term memory mode.
#
# If ``long_term_memory_mode`` is set to ``agent_control`` or ``both``, two
# tool functions ``record_to_memory`` and ``retrieve_from_memory`` will be
# registered in the agent's toolkit, allowing the agent to autonomously
# manage long-term memory through tool calls.
#
# .. note:: To achieve the best results, the ``"agent_control"`` mode may require
# additional instructions in the system prompt.
#
# Create ReAct agent with long-term memory
agent = ReActAgent(
name="Friday",
sys_prompt="You are an assistant with long-term memory capabilities.",
model=DashScopeChatModel(
api_key=os.environ.get("DASHSCOPE_API_KEY"),
model_name="qwen-max-latest",
),
formatter=DashScopeChatFormatter(),
toolkit=Toolkit(),
memory=InMemoryMemory(),
long_term_memory=long_term_memory,
long_term_memory_mode="static_control", # Use static_control mode
)
async def record_preferences():
"""ReAct agent integration example"""
# Conversation example
msg = Msg(
"user",
"When I travel to Hangzhou, I like staying in homestays",
"user",
)
await agent(msg)
asyncio.run(record_preferences())
# %%
# Then we clear the short-term memory and ask the agent about the user's preferences.
#
async def retrieve_preferences():
"""Retrieve user preferences from long-term memory"""
# Clear short-term memory
await agent.memory.clear()
# The agent will remember previous conversations
msg2 = Msg("user", "What are my preferences? Answer briefly.", "user")
await agent(msg2)
asyncio.run(retrieve_preferences())
# %%
# Using ReMe Long-Term Memory
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# .. note:: We provide an example of using ReMe long-term memory in the GitHub repository under the ``examples/long_term_memory/reme`` directory.
#
# .. code-block:: python
# :caption: Example of ReMe long-term memory setup
#
# from agentscope.memory import ReMePersonalLongTermMemory
#
# # Create ReMe personal long-term memory instance
# reme_long_term_memory = ReMePersonalLongTermMemory(
# agent_name="Friday",
# user_name="user_123",
# model=DashScopeChatModel(
# model_name="qwen3-max",
# api_key=os.environ.get("DASHSCOPE_API_KEY"),
# stream=False,
# ),
# embedding_model=DashScopeTextEmbedding(
# model_name="text-embedding-v4",
# api_key=os.environ.get("DASHSCOPE_API_KEY"),
# dimensions=1024,
# ),
# )
#
#
# The ``ReMePersonalLongTermMemory`` class provides four main methods for long-term memory operations.
# They include ``record_to_memory`` and ``retrieve_from_memory`` for tool calls,
# as well as ``record`` and ``retrieve`` for direct calls.
#
# As an example, we use ``record_to_memory`` to record user preferences.
#
# .. code-block:: python
# :caption: Example of recording to ReMe long-term memory
#
# async def test_record_to_memory():
# """Test record_to_memory tool function interface"""
# async with reme_long_term_memory:
# result = await reme_long_term_memory.record_to_memory(
# thinking="The user is sharing their travel preferences and habits",
# content=[
# "I prefer to stay in homestays when traveling to Hangzhou",
# "I like to visit the West Lake in the morning",
# "I enjoy drinking Longjing tea",
# ],
# )
# # Extract result text
# result_text = " ".join(
# block.get("text", "")
# for block in result.content
# if block.get("type") == "text"
# )
# print(f"Recording result: {result_text}")
#
#
#
# Then we use ``retrieve_from_memory`` to retrieve related memories.
#
# .. code-block:: python
# :caption: Example of retrieving from ReMe long-term memory
#
# async def test_retrieve_from_memory():
# """Test retrieve_from_memory tool function interface"""
# async with reme_long_term_memory:
# # First record some content
# await reme_long_term_memory.record_to_memory(
# thinking="User is sharing travel preferences",
# content=[
# "I prefer to stay in homestays when traveling to Hangzhou",
# ],
# )
#
# # Then retrieve
# result = await reme_long_term_memory.retrieve_from_memory(
# keywords=["Hangzhou travel", "tea preference"],
# )
# retrieved_text = " ".join(
# block.get("text", "")
# for block in result.content
# if block.get("type") == "text"
# )
# print(f"Retrieved memories: {retrieved_text}")
#
#
# Besides the tool function interface, we can also use the ``record`` method to directly record message conversations.
#
# .. code-block:: python
# :caption: Example of direct recording to ReMe long-term memory
#
# async def test_record_direct():
# """Test record direct recording method"""
# async with reme_long_term_memory:
# await reme_long_term_memory.record(
# msgs=[
# Msg(
# role="user",
# content="I work as a software engineer and prefer remote work",
# name="user",
# ),
# Msg(
# role="assistant",
# content="Understood! You're a software engineer who values remote work flexibility.",
# name="assistant",
# ),
# Msg(
# role="user",
# content="I usually start my day at 9 AM with a cup of coffee",
# name="user",
# ),
# ],
# )
# print("Successfully recorded conversation messages")
#
#
# Similarly, we use the ``retrieve`` method to retrieve related memories.
#
# .. code-block:: python
# :caption: Example of direct retrieval from ReMe long-term memory
#
# async def test_retrieve_direct():
# """Test retrieve direct retrieval method"""
# async with reme_long_term_memory:
# # First record some content
# await reme_long_term_memory.record(
# msgs=[
# Msg(
# role="user",
# content="I work as a software engineer and prefer remote work",
# name="user",
# ),
# ],
# )
#
# # Then retrieve
# memories = await reme_long_term_memory.retrieve(
# msg=Msg(
# role="user",
# content="What do you know about my work preferences?",
# name="user",
# ),
# )
# print(
# f"Retrieved memories: {memories if memories else 'No memories found'}",
# )
#
#
# Integration with ReAct Agent
# ----------------------------------------
# In AgentScope, the ``ReActAgent`` class receives a ``long_term_memory``
# parameter in its constructor, as well as a ``long_term_memory_mode`` parameter.
#
# If ``long_term_memory_mode`` is set to ``agent_control`` or ``both``,
# ``record_to_memory`` and ``retrieve_from_memory`` tool functions will be
# registered, allowing the agent to autonomously manage long-term memory through tool calls.
#
# .. note:: To achieve the best results, the ``"agent_control"`` mode may require
# additional instructions in the system prompt.
#
# .. code-block:: python
# :caption: Example of ReAct agent with ReMe long-term memory
#
# # Create ReAct agent with long-term memory (agent_control mode)
# async def test_react_agent_with_reme():
# """Test ReActAgent integration with ReMe personal memory"""
# async with reme_long_term_memory:
# agent_with_reme = ReActAgent(
# name="Friday",
# sys_prompt=(
# "You are a helpful assistant named Friday with long-term memory capabilities. "
# "\n\n## Memory Management Guidelines:\n"
# "1. **Recording Memories**: When users share personal information, preferences, "
# "habits, or facts about themselves, ALWAYS record them using `record_to_memory` "
# "for future reference.\n"
# "\n2. **Retrieving Memories**: BEFORE answering questions about the user's preferences, "
# "past information, or personal details, you MUST FIRST call `retrieve_from_memory` "
# "to check if you have any relevant stored information. Do NOT rely solely on the "
# "current conversation context.\n"
# "\n3. **When to Retrieve**: Call `retrieve_from_memory` when:\n"
# " - User asks questions like 'what do I like?', 'what are my preferences?', "
# "'what do you know about me?'\n"
# " - User asks about their past behaviors, habits, or preferences\n"
# " - User refers to information they mentioned before\n"
# " - You need context about the user to provide personalized responses\n"
# "\nAlways check your memory first before claiming you don't know something about the user."
# ),
# model=DashScopeChatModel(
# model_name="qwen3-max",
# api_key=os.environ.get("DASHSCOPE_API_KEY"),
# stream=False,
# ),
# formatter=DashScopeChatFormatter(),
# toolkit=Toolkit(),
# memory=InMemoryMemory(),
# long_term_memory=reme_long_term_memory,
# long_term_memory_mode="agent_control", # Use agent_control mode
# )
#
# # User shares preferences
# msg = Msg(
# role="user",
# content="When I travel to Hangzhou, I prefer to stay in a homestay",
# name="user",
# )
# response = await agent_with_reme(msg)
# print(f"Agent response: {response.get_text_content()}")
#
# # Clear short-term memory to test long-term memory
# await agent_with_reme.memory.clear()
#
# # Query preferences
# msg2 = Msg(
# role="user",
# content="what preference do I have?",
# name="user",
# )
# response2 = await agent_with_reme(msg2)
# print(f"Agent response: {response2.get_text_content()}")
#
#
# Then we clear the short-term memory and ask the agent about the user's preferences.
#
# .. code-block:: python
# :caption: Example of retrieving preferences with ReAct agent and ReMe long-term memory
#
# async def retrieve_reme_preferences():
# """Retrieve user preferences from long-term memory"""
# async with reme_long_term_memory:
# # Create agent (reusing for demonstration completeness)
# agent_with_reme = ReActAgent(
# name="Friday",
# sys_prompt="You are an assistant with long-term memory capabilities.",
# model=DashScopeChatModel(
# api_key=os.environ.get("DASHSCOPE_API_KEY"),
# model_name="qwen3-max",
# stream=False,
# ),
# formatter=DashScopeChatFormatter(),
# toolkit=Toolkit(),
# memory=InMemoryMemory(),
# long_term_memory=reme_long_term_memory,
# long_term_memory_mode="agent_control",
# )
#
# # Clear short-term memory
# await agent_with_reme.memory.clear()
# # The agent will remember previous conversations
# msg2 = Msg("user", "What are my preferences? Answer briefly.", "user")
# await agent_with_reme(msg2)
#
# Customizing Long-Term Memory
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope provides the ``LongTermMemoryBase`` base class, which defines the basic
#
# Developers can inherit from ``LongTermMemoryBase`` to implement custom long-term
# memory systems according to their needs
#
# .. list-table:: Long-term memory classes in AgentScope
# :header-rows: 1
#
# * - Class
# - Abstract Methods
# - Description
# * - ``LongTermMemoryBase``
# - | ``record``
# | ``retrieve``
# | ``record_to_memory``
# | ``retrieve_from_memory``
# - - For ``"static_control"`` mode, you must implement the ``record`` and ``retrieve`` methods.
# - For ``"agent_control"`` mode, the ``record_to_memory`` and ``retrieve_from_memory`` methods must be implemented.
# * - ``Mem0LongTermMemory``
# - | ``record``
# | ``retrieve``
# | ``record_to_memory``
# | ``retrieve_from_memory``
# - Long-term memory implementation based on the mem0 library, supporting vector storage and retrieval.
# * - ``ReMePersonalLongTermMemory``
# - | ``record``
# | ``retrieve``
# | ``record_to_memory``
# | ``retrieve_from_memory``
# - Personal memory implementation based on the ReMe framework, providing powerful memory management and retrieval capabilities.
#
#
#
#
# Further Reading
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# - :ref:`memory` - Basic memory system
# - :ref:`agent` - ReAct agent
# - :ref:`tool` - Tool system

View File

@@ -0,0 +1,207 @@
# -*- coding: utf-8 -*-
"""
.. _mcp:
MCP
=========================
The tutorial covers the following features of AgentScope in support of the MCP (Model Context Protocol):
- Support both **HTTP** (streamable HTTP and SSE) and **StdIO** MCP servers
- Provide both **stateful** and **stateless** MCP clients
- Provide both **server-level** and **function-level** MCP tool management
Here the stateful/stateless distinction refers to whether the client maintains a persistent session with the MCP server or not.
The table below summarizes the supported MCP client types and protocols:
.. list-table:: Supported MCP client types and protocols
:header-rows: 1
* - Client Type
- HTTP (Streamable HTTP and SSE)
- StdIO
* - Stateful Client
- ``HttpStatefulClient``
- ``HttpStatelessClient``
* - Stateless Client
- ``StdIOStatefulClient``
-
"""
import asyncio
import json
import os
from agentscope.mcp import HttpStatefulClient, HttpStatelessClient
from agentscope.tool import Toolkit
# %%
# MCP Client
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# In AgentScope, MCP clients are responsible for
#
# - connecting to the MCP server,
# - obtaining tool functions from the server, and
# - calling tool functions in the MCP server.
#
# There are two types of MCP clients in AgentScope: **Stateful** and **Stateless**.
# They only differ in how to manage the session with the MCP server.
#
# - Stateful Client: The stateful MCP client **maintains a persistent session** with the MCP server within its lifetime. The developers should explicitly call ``connect()`` and ``close()`` methods to manage the connection lifecycle.
# - Stateless Client: The stateless MCP client creates a new session when calling the tool function, and destroys the session right after the tool function call, which is much more lightweight.
#
# .. note:: - The StdIO MCP server only has stateful client, when ``connect()`` is called, it will start the MCP server locally and then connect to it.
# - For stateful clients, developers must ensure the client is connected when calling the tool functions.
# - When multiple `HttpStatefulClients` or `StdIOStatefulClients` are connected, they should be closed in Last In First Out (LIFO) order to prevent errors.
#
# Taking Gaode map MCP server as an example, the creation of stateful and stateless clients are very similar:
#
stateful_client = HttpStatefulClient(
# The name to identify the MCP
name="mcp_services_stateful",
transport="streamable_http",
url=f"https://mcp.amap.com/mcp?key={os.environ['GAODE_API_KEY']}",
)
stateless_client = HttpStatelessClient(
# The name to identify the MCP
name="mcp_services_stateless",
transport="streamable_http",
url=f"https://mcp.amap.com/mcp?key={os.environ['GAODE_API_KEY']}",
)
# %%
# Both stateful and stateless clients provide the following methods:
#
# .. list-table:: MCP Client Methods
# :header-rows: 1
#
# * - Method
# - Description
# * - ``list_tools``
# - List all tools available in the MCP server.
# * - ``get_callable_function``
# - Get a callable function object from the MCP server by its name.
#
# MCP as Tool
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope provides fine-grained management of MCP tools, including both server-level and function-level management.
#
# Server-Level Management
# --------------------------------
# You can register all tools from an MCP server into ``Toolkit`` as follows.
#
# .. tip:: Optionally, you can specify a group name to organize the tools. Refer to :ref:`tool` section for group-wise tools management.
#
toolkit = Toolkit()
async def example_register_stateless_mcp() -> None:
"""Example of registering MCP tools from a stateless client."""
# Register all tools from the MCP server
await toolkit.register_mcp_client(
stateless_client,
# group_name="map_services", # Optional group name
)
print(
"Total number of MCP tools registered:",
len(toolkit.get_json_schemas()),
)
maps_geo = next(
tool
for tool in toolkit.get_json_schemas()
if tool["function"]["name"] == "maps_geo"
)
print("\nThe example ``maps_geo`` function:")
print(
json.dumps(
maps_geo,
indent=4,
ensure_ascii=False,
),
)
asyncio.run(example_register_stateless_mcp())
# %%
# To remove the registered tools, you can use the ``remove_tool_function`` to remove a specific tool function, or ``remove_mcp_clients`` to remove all tools from a specific MCP.
#
async def example_remove_mcp_tools() -> None:
"""Example of removing MCP tools."""
print(
"Total number of tools before removal: ",
len(toolkit.get_json_schemas()),
)
# Remove a specific tool function by its name
toolkit.remove_tool_function("maps_geo")
print("Number of tools: ", len(toolkit.get_json_schemas()))
# Remove all tools from the MCP client by its name
await toolkit.remove_mcp_clients(client_names=["mcp_services_stateless"])
print("Number of tools: ", len(toolkit.get_json_schemas()))
asyncio.run(example_remove_mcp_tools())
# %%
# Function-Level Management
# --------------------------------
# We notice the demand for more fine-grained control over MCP tools, such as post-processing the tool results, or use them to create a more complex tool function.
#
# Therefore, AgentScope supports to obtain the callable function object from MCP by its name, so that you can
#
# - call it directly,
# - wrap it into your own function, or anyway you like.
#
# Additionally, you can specify whether to wrap the tool result into ``ToolResponse`` object in AgentScope, so that you can use it seamlessly with the ``Toolkit``.
# If you set ``wrap_tool_result=False``, the raw result type ``mcp.types.CallToolResult`` will be returned.
#
# Taking the ``maps_geo`` function as an example, you can obtain it as a callable function object as follows:
#
async def example_function_level_usage() -> None:
"""Example of using function-level MCP tool."""
func_obj = await stateless_client.get_callable_function(
func_name="maps_geo",
# Whether to wrap the tool result into ToolResponse in AgentScope
wrap_tool_result=True,
)
# You can obtain its name, description and json schema
print("Function name:", func_obj.name)
print("Function description:", func_obj.description)
print(
"Function JSON schema:",
json.dumps(func_obj.json_schema, indent=4, ensure_ascii=False),
)
# Call the function object directly
res = await func_obj(
address="Tiananmen Square",
city="Beijing",
)
print("\nFunction call result:")
print(res)
asyncio.run(example_function_level_usage())
# %%
# Further Reading
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# For more details, see:
#
# - :ref:`tool`
# - :ref:`agent`
#

View File

@@ -0,0 +1,446 @@
# -*- coding: utf-8 -*-
"""
.. _memory:
Memory
========================
The memory module in AgentScope is responsible for
- storing the messages and
- managing them with specific marks
in different storage implementations.
The **mark** is a string label associated with each message in the memory,
which can be used to categorize, filter, and retrieve messages based on their
context or purpose.
It's powerful for high-level memory management in agents. For example,
In `ReActAgent` class, the hint messages are stored with the
mark "hint", and the memory compression functionality is also implemented
based on marks.
.. note:: The memory module only provides storage and management
functionalities. The algorithm logic such as compression is implemented in
the agent level.
Currently, AgentScope provides the following memory storage implementations:
.. list-table:: The built-in memory storage implementations in AgentScope
:header-rows: 1
* - Memory Class
- Description
* - ``InMemoryMemory``
- A simple in-memory implementation of memory storage.
* - ``AsyncSQLAlchemyMemory``
- An asynchronous SQLAlchemy-based implementation of memory storage, which supports various databases such as SQLite, PostgreSQL, MySQL, etc.
* - ``RedisMemory``
- A Redis-based implementation of memory storage.
.. tip:: If you're interested in contributing new memory storage implementations, please refer to the
`Contribution Guide <https://github.com/agentscope-ai/agentscope/blob/main/CONTRIBUTING.md#types-of-contributions>`_.
All the above memory classes inherit from the base class ``MemoryBase``, and
provide the following methods to manage the messages in the memory:
.. list-table:: The methods provided by the memory classes
:header-rows: 1
* - Method
- Description
* - ``add(
memories: Msg | list[Msg] | None,
marks: str | list[str] | None = None,
) -> None``
- Add ``Msg`` object(s) to the memory storage with the given mark(s) (if provided).
* - ``delete(msg_ids: list[str]) -> int``
- Delete messages from the memory storage by their IDs.
* - ``delete_by_mark(mark: str | list[str]) -> int``
- Delete messages from the memory by their marks.
* - ``size() -> int``
- Get the size of the memory storage.
* - ``clear() -> None``
- Clear the memory storage.
* - ``get_memory(
mark: str | None = None,
exclude_mark: str | None = None,
) -> list[Msg]``
- Get the messages from the memory by mark (if provided). Otherwise, get all messages. If the ``update_compressed_summary`` method is used to store a compressed summary, it will be attached to the head of the returned messages.
* - ``update_messages_mark(
new_mark: str | None,
old_mark: str | None = None,
msg_ids: list[str] | None = None,
) -> int``
- A unified method to update marks of messages in the storage (add, remove, or change marks).
* - ``update_compressed_summary(
summary: str,
) -> None``
- Update the summary attribute stored in the memory.
"""
import asyncio
import json
import fakeredis
from sqlalchemy.ext.asyncio import create_async_engine
from agentscope.memory import (
InMemoryMemory,
AsyncSQLAlchemyMemory,
RedisMemory,
)
from agentscope.message import Msg
# %%
# In-Memory Memory
# ~~~~~~~~~~~~~~~~~~~~~~~~
#
# The in-memory memory provides a simple way to store messages in memory.
# Together with the :ref:`state` module, it can persist the memory content across
# different users and sessions.
async def in_memory_example():
"""An example of using InMemoryMemory to store messages in memory."""
memory = InMemoryMemory()
await memory.add(
Msg("Alice", "Generate a report about AgentScope", "user"),
)
# Add a hint message with the mark "hint"
await memory.add(
[
Msg(
"system",
"<system-hint>Create a plan first to collect information and "
"generate the report step by step.</system-hint>",
"system",
),
],
marks="hint",
)
msgs = await memory.get_memory(mark="hint")
print("The messages with mark 'hint':")
for msg in msgs:
print(f"- {msg}")
# All the stored messages can be exported and loaded via ``state_dict`` and ``load_state_dict`` methods.
state = memory.state_dict()
print("The state dict of the memory:")
print(json.dumps(state, indent=2))
# delete messages by mark
deleted_count = await memory.delete_by_mark("hint")
print(f"Deleted {deleted_count} messages with mark 'hint'.")
print("The state dict of the memory after deletion:")
state = memory.state_dict()
print(json.dumps(state, indent=2))
asyncio.run(in_memory_example())
# %%
# Relational Database Memory
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope provides a unified interface to work with relational databases via SQLAlchemy, supporting
#
# - various databases such as SQLite, PostgreSQL, MySQL, etc.
# - user and session management, and
# - connection pooling in the production environment
#
# Specifically, here we use a memory backed by SQLite as an example.
async def sqlalchemy_example() -> None:
"""An example of using AsyncSQLAlchemyMemory to store messages in a SQLite database."""
# Create an async SQLAlchemy engine first
engine = create_async_engine("sqlite+aiosqlite:///./test_memory.db")
# Then create the memory with the engine
memory = AsyncSQLAlchemyMemory(
engine_or_session=engine,
# Optionally specify user_id and session_id
user_id="user_1",
session_id="session_1",
)
await memory.add(
Msg("Alice", "Generate a report about AgentScope", "user"),
)
await memory.add(
[
Msg(
"system",
"<system-hint>Create a plan first to collect information and "
"generate the report step by step.</system-hint>",
"system",
),
],
marks="hint",
)
msgs = await memory.get_memory(mark="hint")
print("The messages with mark 'hint':")
for msg in msgs:
print(f"- {msg}")
# Close the engine when done
await memory.close()
asyncio.run(sqlalchemy_example())
# %%
# Optionally, you can also use the ``AsyncSQLAlchemyMemory`` as an async context manager, and the session will be closed automatically when exiting the context.
async def sqlalchemy_context_example() -> None:
"""Example of using AsyncSQLAlchemyMemory as an async context manager."""
engine = create_async_engine("sqlite+aiosqlite:///./test_memory.db")
async with AsyncSQLAlchemyMemory(
engine_or_session=engine,
user_id="user_1",
session_id="session_1",
) as memory:
await memory.add(
Msg("Alice", "Generate a report about AgentScope", "user"),
)
msgs = await memory.get_memory()
print("All messages in the memory:")
for msg in msgs:
print(f"- {msg}")
asyncio.run(sqlalchemy_context_example())
# %%
# In production environment e.g. with FastAPI, the connection pooling can be enabled as follows:
#
# .. code-block:: python
# :caption: SQLAlchemy Memory with Connection Pooling in FastAPI
#
# from typing import AsyncGenerator
#
# from fastapi import FastAPI, Depends
# from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
#
# from agentscope.agent import ReActAgent
# from agentscope.pipeline import stream_printing_messages
#
#
# app = FastAPI()
#
# # Create an async SQLAlchemy engine with connection pooling
# engine = create_async_engine(
# "sqlite+aiosqlite:///./test_memory.db",
# pool_size=10,
# max_overflow=20,
# pool_timeout=30,
# # ... The other pool settings
# )
#
# # Create a session maker
# async_session_marker = async_sessionmaker(
# engine,
# expire_on_commit=False,
# autocommit=False,
# autoflush=False,
# )
#
# async def get_db() -> AsyncGenerator[AsyncSession, None]:
# async with async_session_marker() as session:
# try:
# yield session
# await session.commit()
# except Exception:
# await session.rollback()
# raise
# finally:
# await session.close()
#
# @app.post("/chat")
# async def chat_endpoint(
# user_id: str,
# session_id: str,
# input: str,
# db_session: AsyncSession = Depends(get_db),
# ):
# # Some setup for the agent
# ...
#
# # Create the agent with the SQLAlchemy memory
# agent = ReActAgent(
# # ...
# memory=AsyncSQLAlchemyMemory(
# engine_or_session=db_session,
# user_id=user_id,
# session_id=session_id,
# ),
# )
#
# # Handle the chat with the agent
# async for msg, _ in stream_printing_messages(
# agents=[agent],
# coroutine_task=agent(Msg("user", input, "user")),
# ):
# # yield msg to the client
# ...
#
#
# NoSQL Database Memory
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope also provides memory implementations based on NoSQL databases such as Redis.
# It also supports user and session management, and connection pooling in the production environment.
#
# First, we can initialize the Redis memory as follows:
async def redis_memory_example() -> None:
"""An example of using RedisMemory to store messages in Redis."""
# Use fakeredis for in-memory testing without a real Redis server
fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True)
# Create the Redis memory
memory = RedisMemory(
# Using fake redis for demonstration
connection_pool=fake_redis.connection_pool,
# You can also connect to a real Redis server by specifying host and port
# host="localhost",
# port=6379,
# Optionally specify user_id and session_id
user_id="user_1",
session_id="session_1",
)
# Add a message to the memory
await memory.add(
Msg(
"Alice",
"Generate a report about AgentScope",
"user",
),
)
# Add a hint message with the mark "hint"
await memory.add(
Msg(
"system",
"<system-hint>Create a plan first to collect information and "
"generate the report step by step.</system-hint>",
"system",
),
marks="hint",
)
# Retrieve messages with the mark "hint"
msgs = await memory.get_memory(mark="hint")
print("The messages with mark 'hint':")
for msg in msgs:
print(f"- {msg}")
asyncio.run(redis_memory_example())
# %%
# Similarly, the `RedisMemory` can also be used with connection pooling in the production environment, e.g., with FastAPI.
#
# .. code-block:: python
# :caption: Redis Memory with Connection Pooling in FastAPI
#
# from fastapi import FastAPI, HTTPException
# from redis.asyncio import ConnectionPool
# from contextlib import asynccontextmanager
#
# # Global Redis connection pool
# redis_pool: ConnectionPool | None = None
#
#
# # Use the lifespan event to manage the Redis connection pool
# @asynccontextmanager
# async def lifespan(app: FastAPI):
# global redis_pool
# redis_pool = ConnectionPool(
# host="localhost",
# port=6379,
# db=0,
# password=None,
# decode_responses=True,
# max_connections=10,
# encoding="utf-8",
# )
# print("✅ Redis connection established")
#
# yield
#
# await redis_pool.disconnect()
# print("✅ Redis connection closed")
#
#
# app = FastAPI(lifespan=lifespan)
#
#
# @app.post("/chat_endpoint")
# async def chat_endpoint(
# user_id: str, session_id: str, input: str
# ): # ✅ 直接使用BaseModel
# """A chat endpoint"""
# global redis_pool
# if redis_pool is None:
# raise HTTPException(
# status_code=500,
# detail="Redis connection pool is not initialized.",
# )
#
# # Create the Redis memory
# memory = RedisMemory(
# connection_pool=redis_pool,
# user_id=user_id,
# session_id=session_id,
# )
#
# ...
#
# # Close the Redis client connection when done
# client = memory.get_client()
# await client.aclose()
#
#
#
# Customizing Memory
# ~~~~~~~~~~~~~~~~~~~~~~~~
#
# To customize your own memory, just inherit from ``MemoryBase`` and implement the following methods:
#
# .. list-table::
# :header-rows: 1
#
# * - Method
# - Description
# * - ``add``
# - Add ``Msg`` objects to the memory
# * - ``delete``
# - Delete ``Msg`` objects from the memory
# * - ``delete_by_mark``
# - Delete ``Msg`` objects from the memory by their marks
# * - ``size``
# - The size of the memory
# * - ``clear``
# - Clear the memory content
# * - ``get_memory``
# - Get the memory content as a list of ``Msg`` objects
# * - ``update_messages_mark``
# - Update marks of messages in the memory
# * - ``state_dict``
# - Get the state dictionary of the memory
# * - ``load_state_dict``
# - Load the state dictionary of the memory
#
# Further Reading
# ~~~~~~~~~~~~~~~~~~~~~~~~
# - :ref:`agent`
# - :ref:`long-term-memory`

View File

@@ -0,0 +1,403 @@
# -*- coding: utf-8 -*-
"""
.. _middleware:
Middleware
===========================
AgentScope provides a flexible middleware system that allows developers to intercept and modify the execution of various operations.
Currently, middleware support is available for **tool execution** in the ``Toolkit`` class.
The middleware system follows an **onion model**, where each middleware wraps around the previous one, forming layers.
This allows developers to:
- Perform **pre-processing** before the operation
- **Intercept and modify** responses during execution
- Perform **post-processing** after the operation completes
- **Skip** the operation execution entirely based on conditions
.. tip:: Future versions of AgentScope will expand middleware support to other components such as agents and models.
"""
import asyncio
from typing import AsyncGenerator, Callable
from agentscope.message import TextBlock, ToolUseBlock
from agentscope.tool import ToolResponse, Toolkit
# %%
# Tool Execution Middleware
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# The ``Toolkit`` class supports middleware for tool execution via the ``register_middleware`` method.
# Each middleware can intercept the tool call and modify the input or output.
#
# Middleware Signature
# ------------------------------
#
# A middleware function should have the following signature:
#
# .. code-block:: python
#
# async def middleware(
# kwargs: dict,
# next_handler: Callable,
# ) -> AsyncGenerator[ToolResponse, None]:
# # Access parameters from kwargs
# tool_call = kwargs["tool_call"]
#
# # Pre-processing
# # ...
#
# # Call the next middleware or tool function
# async for response in await next_handler(**kwargs):
# # Post-processing
# yield response
#
# .. list-table:: Middleware Parameters
# :header-rows: 1
#
# * - Parameter
# - Type
# - Description
# * - ``kwargs``
# - ``dict``
# - Context parameters. Currently, includes ``tool_call`` (ToolUseBlock). May include additional parameters in future versions.
# * - ``next_handler``
# - ``Callable``
# - A callable that accepts kwargs dict and returns a coroutine yielding AsyncGenerator of ToolResponse objects
# * - **Returns**
# - ``AsyncGenerator[ToolResponse, None]``
# - An async generator that yields ToolResponse objects
#
# Basic Example
# ------------------------------
#
# Here is a simple middleware that logs tool calls:
#
async def logging_middleware(
kwargs: dict,
next_handler: Callable,
) -> AsyncGenerator[ToolResponse, None]:
"""A middleware that logs tool execution."""
# Access the tool call from kwargs
tool_call = kwargs["tool_call"]
# Pre-processing: log before tool execution
print(f"[Middleware] Calling tool: {tool_call['name']}")
print(f"[Middleware] Input: {tool_call['input']}")
# Call the next handler (either another middleware or the actual tool)
async for response in await next_handler(**kwargs):
# Post-processing: log the response
print(f"[Middleware] Response: {response.content[0]['text']}")
yield response
# This will execute after all responses are yielded
print(f"[Middleware] Tool {tool_call['name']} completed")
# %%
# Let's register this middleware with a toolkit and test it:
#
async def search_tool(query: str) -> ToolResponse:
"""A simple search tool.
Args:
query (`str`):
The search query.
Returns:
`ToolResponse`:
The search result.
"""
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Search results for '{query}'",
),
],
)
async def example_logging_middleware() -> None:
"""Example of using logging middleware."""
# Create a toolkit and register the tool
toolkit = Toolkit()
toolkit.register_tool_function(search_tool)
# Register the middleware
toolkit.register_middleware(logging_middleware)
# Call the tool
result = await toolkit.call_tool_function(
ToolUseBlock(
type="tool_use",
id="1",
name="search_tool",
input={"query": "AgentScope"},
),
)
async for response in result:
print(f"\n[Final] {response.content[0]['text']}\n")
print("=" * 60)
print("Example 1: Logging Middleware")
print("=" * 60)
asyncio.run(example_logging_middleware())
# %%
# Modifying Input and Output
# ------------------------------
#
# Middleware can also modify the tool call input and the response content:
#
async def transform_middleware(
kwargs: dict,
next_handler: Callable,
) -> AsyncGenerator[ToolResponse, None]:
"""A middleware that transforms input and output."""
# Access the tool call from kwargs
tool_call = kwargs["tool_call"]
# Pre-processing: modify the input
original_query = tool_call["input"]["query"]
tool_call["input"]["query"] = f"[TRANSFORMED] {original_query}"
async for response in await next_handler(**kwargs):
# Post-processing: modify the response
original_text = response.content[0]["text"]
response.content[0]["text"] = f"{original_text} [MODIFIED]"
yield response
async def example_transform_middleware() -> None:
"""Example of transforming middleware."""
toolkit = Toolkit()
toolkit.register_tool_function(search_tool)
toolkit.register_middleware(transform_middleware)
result = await toolkit.call_tool_function(
ToolUseBlock(
type="tool_use",
id="2",
name="search_tool",
input={"query": "middleware"},
),
)
async for response in result:
print(f"Result: {response.content[0]['text']}")
print("\n" + "=" * 60)
print("Example 2: Transform Middleware")
print("=" * 60)
asyncio.run(example_transform_middleware())
# %%
# Authorization Middleware
# ------------------------------
#
# You can use middleware to implement authorization checks and skip tool execution if not authorized:
#
async def authorization_middleware(
kwargs: dict,
next_handler: Callable,
) -> AsyncGenerator[ToolResponse, None]:
"""A middleware that checks authorization."""
# Access the tool call from kwargs
tool_call = kwargs["tool_call"]
# Check if the tool is authorized (simple example)
authorized_tools = {"search_tool"}
if tool_call["name"] not in authorized_tools:
# Skip execution and return error directly
print(f"[Auth] Tool {tool_call['name']} is not authorized")
yield ToolResponse(
content=[
TextBlock(
type="text",
text=f"Error: Tool '{tool_call['name']}' is not authorized", # noqa: E501
),
],
)
return
# Tool is authorized, proceed
print(f"[Auth] Tool {tool_call['name']} is authorized")
async for response in await next_handler(**kwargs):
yield response
async def unauthorized_tool(data: str) -> ToolResponse:
"""An unauthorized tool.
Args:
data (`str`):
Some data.
Returns:
`ToolResponse`:
The result.
"""
return ToolResponse(
content=[TextBlock(type="text", text=f"Processing {data}")],
)
async def example_authorization_middleware() -> None:
"""Example of authorization middleware."""
toolkit = Toolkit()
toolkit.register_tool_function(search_tool)
toolkit.register_tool_function(unauthorized_tool)
toolkit.register_middleware(authorization_middleware)
# Try authorized tool
print("\nCalling authorized tool:")
result = await toolkit.call_tool_function(
ToolUseBlock(
type="tool_use",
id="3",
name="search_tool",
input={"query": "test"},
),
)
async for response in result:
print(f"Result: {response.content[0]['text']}")
# Try unauthorized tool
print("\nCalling unauthorized tool:")
result = await toolkit.call_tool_function(
ToolUseBlock(
type="tool_use",
id="4",
name="unauthorized_tool",
input={"data": "test"},
),
)
async for response in result:
print(f"Result: {response.content[0]['text']}")
print("\n" + "=" * 60)
print("Example 3: Authorization Middleware")
print("=" * 60)
asyncio.run(example_authorization_middleware())
# %%
# Multiple Middleware (Onion Model)
# ------------------------------
#
# When multiple middleware are registered, they form an onion-like structure.
# The execution order follows the onion model:
#
# - **Pre-processing**: Executes in the order middleware are registered
# - **Post-processing**: Executes in reverse order (inner to outer)
#
# This is because the actual tool response object is passed through the middleware chain,
# and each middleware modifies it in place.
#
async def middleware_1(
kwargs: dict,
next_handler: Callable,
) -> AsyncGenerator[ToolResponse, None]:
"""First middleware."""
# Access the tool call from kwargs
tool_call = kwargs["tool_call"]
# Pre-processing
print("[M1] Pre-processing")
tool_call["input"]["query"] += " [M1]"
async for response in await next_handler(**kwargs):
# Post-processing
response.content[0]["text"] += " [M1]"
print("[M1] Post-processing")
yield response
async def middleware_2(
kwargs: dict,
next_handler: Callable,
) -> AsyncGenerator[ToolResponse, None]:
"""Second middleware."""
# Access the tool call from kwargs
tool_call = kwargs["tool_call"]
# Pre-processing
print("[M2] Pre-processing")
tool_call["input"]["query"] += " [M2]"
async for response in await next_handler(**kwargs):
# Post-processing
response.content[0]["text"] += " [M2]"
print("[M2] Post-processing")
yield response
async def example_multiple_middleware() -> None:
"""Example of multiple middleware."""
toolkit = Toolkit()
toolkit.register_tool_function(search_tool)
# Register middleware in order
toolkit.register_middleware(middleware_1)
toolkit.register_middleware(middleware_2)
result = await toolkit.call_tool_function(
ToolUseBlock(
type="tool_use",
id="5",
name="search_tool",
input={"query": "test"},
),
)
async for response in result:
print(f"\nFinal result: {response.content[0]['text']}")
print("\n" + "=" * 60)
print("Example 4: Multiple Middleware (Onion Model)")
print("=" * 60)
print("\nExecution flow:")
print("M1 Pre → M2 Pre → Tool → M2 Post → M1 Post")
print()
asyncio.run(example_multiple_middleware())
# %%
# Use Cases
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# The middleware system is useful for various scenarios:
#
# - **Logging and Monitoring**: Track tool usage and performance
# - **Authorization**: Control access to specific tools
# - **Rate Limiting**: Limit the frequency of tool calls
# - **Caching**: Cache tool responses for repeated calls
# - **Error Handling**: Add retry logic or graceful degradation
# - **Input Validation**: Validate and sanitize tool inputs
# - **Output Transformation**: Format or filter tool outputs
# - **Metrics Collection**: Collect statistics about tool usage
#
# .. note::
# - Middleware are applied in the order they are registered
# - The same ``ToolResponse`` object is passed through the middleware chain and modified in place
# - Middleware can completely skip tool execution by not calling ``next_handler``
# - All middleware must be async generator functions that yield ``ToolResponse`` objects

View File

@@ -0,0 +1,235 @@
# -*- coding: utf-8 -*-
"""
.. _model:
Model
====================
In this tutorial, we introduce the model APIs integrated in AgentScope, how to use them and how to integrate new model APIs.
The supported model APIs and providers include:
.. list-table::
:header-rows: 1
* - API
- Class
- Compatible
- Streaming
- Tools
- Vision
- Reasoning
* - OpenAI
- ``OpenAIChatModel``
- vLLM, DeepSeek
- ✅
- ✅
- ✅
- ✅
* - DashScope
- ``DashScopeChatModel``
-
- ✅
- ✅
- ✅
- ✅
* - Anthropic
- ``AnthropicChatModel``
-
- ✅
- ✅
- ✅
- ✅
* - Gemini
- ``GeminiChatModel``
-
- ✅
- ✅
- ✅
- ✅
* - Ollama
- ``OllamaChatModel``
-
- ✅
- ✅
- ✅
- ✅
.. note:: When using vLLM, you need to configure the appropriate tool calling parameters for different models during deployment, such as ``--enable-auto-tool-choice``, ``--tool-call-parser``, etc. For more details, refer to the `official vLLM documentation <https://docs.vllm.ai/en/latest/features/tool_calling.html>`_.
.. note:: For OpenAI-compatible models (e.g. vLLM, Deepseek), developers can use the ``OpenAIChatModel`` class, and specify the API endpoint by the ``client_kwargs`` parameter: ``client_kwargs={"base_url": "http://your-api-endpoint"}``. For example:
.. code-block:: python
OpenAIChatModel(client_kwargs={"base_url": "http://localhost:8000/v1"})
.. note:: Model behavior parameters (such as temperature, maximum length, etc.) can be preset in the constructor function via the ``generate_kwargs`` parameter. For example:
.. code-block:: python
OpenAIChatModel(generate_kwargs={"temperature": 0.3, "max_tokens": 1000})
To provide unified model interfaces, the above model classes has the following common methods:
- The first three arguments of the ``__call__`` method are ``messages`` , ``tools`` and ``tool_choice``, representing the input messages, JSON schema of tool functions, and tool selection mode, respectively.
- The return type are either a ``ChatResponse`` instance or an async generator of ``ChatResponse`` in streaming mode.
.. note:: Different model APIs differ in the input message format, refer to :ref:`prompt` for more details.
The ``ChatResponse`` instance contains the generated thinking/text/tool use content, identity, created time and usage information.
"""
import asyncio
import json
import os
from agentscope.message import TextBlock, ToolUseBlock, ThinkingBlock, Msg
from agentscope.model import ChatResponse, DashScopeChatModel
response = ChatResponse(
content=[
ThinkingBlock(
type="thinking",
thinking="I should search for AgentScope on Google.",
),
TextBlock(type="text", text="I'll search for AgentScope on Google."),
ToolUseBlock(
type="tool_use",
id="642n298gjna",
name="google_search",
input={"query": "AgentScope?"},
),
],
)
print(response)
# %%
# Taking ``DashScopeChatModel`` as an example, we can use it to create a chat model instance and call it with messages and tools:
async def example_model_call() -> None:
"""An example of using the DashScopeChatModel."""
model = DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
stream=False,
)
res = await model(
messages=[
{"role": "user", "content": "Hi!"},
],
)
# You can directly create a ``Msg`` object with the response content
msg_res = Msg("Friday", res.content, "assistant")
print("The response:", res)
print("The response as Msg:", msg_res)
asyncio.run(example_model_call())
# %%
# Streaming
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# To enable streaming model, set the ``stream`` parameter in the model constructor to ``True``.
# When streaming is enabled, the ``__call__`` method will return an **async generator** that yields ``ChatResponse`` instances as they are generated by the model.
#
# .. note:: The streaming mode in AgentScope is designed to be **cumulative**, meaning the content in each chunk contains all the previous content plus the newly generated content.
#
async def example_streaming() -> None:
"""An example of using the streaming model."""
model = DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
stream=True,
)
generator = await model(
messages=[
{
"role": "user",
"content": "Count from 1 to 20, and just report the number without any other information.",
},
],
)
print("The type of the response:", type(generator))
i = 0
async for chunk in generator:
print(f"Chunk {i}")
print(f"\ttype: {type(chunk.content)}")
print(f"\t{chunk}\n")
i += 1
asyncio.run(example_streaming())
# %%
# Reasoning
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope supports reasoning models by providing the ``ThinkingBlock``.
#
async def example_reasoning() -> None:
"""An example of using the reasoning model."""
model = DashScopeChatModel(
model_name="qwen-turbo",
api_key=os.environ["DASHSCOPE_API_KEY"],
enable_thinking=True,
)
res = await model(
messages=[
{"role": "user", "content": "Who am I?"},
],
)
last_chunk = None
async for chunk in res:
last_chunk = chunk
print("The final response:")
print(last_chunk)
asyncio.run(example_reasoning())
# %%
# Tools API
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Different model providers differ in their tools APIs, e.g. the tools JSON schema, the tool call/response format.
# To provide a unified interface, AgentScope solves the problem by:
#
# - Providing unified tool call block :ref:`ToolUseBlock <tool-block>` and tool response block :ref:`ToolResultBlock <tool-block>`, respectively.
# - Providing a unified tools interface in the ``__call__`` method of the model classes, that accepts a list of tools JSON schemas as follows:
#
json_schemas = [
{
"type": "function",
"function": {
"name": "google_search",
"description": "Search for a query on Google.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query.",
},
},
"required": ["query"],
},
},
},
]
# %%
# Further Reading
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# - :ref:`message`
# - :ref:`prompt`
#

View File

@@ -0,0 +1,288 @@
# -*- coding: utf-8 -*-
"""
.. _pipeline:
Pipeline
========================
For multi-agent orchestration, AgentScope provides the ``agentscope.pipeline`` module
as syntax sugar for chaining agents together, including
- **MsgHub**: a message hub for broadcasting messages among multiple agents
- **sequential_pipeline** and **SequentialPipeline**: a functional and class-based implementation that chains agents in a sequential manner
- **fanout_pipeline** and **FanoutPipeline**: a functional and class-based implementation that distributes the same input to multiple agents
- **stream_printing_messages**: a utility function that convert the printing messages from agent(s) into an async generator
"""
import os, asyncio
from agentscope.formatter import DashScopeMultiAgentFormatter
from agentscope.message import Msg
from agentscope.model import DashScopeChatModel
from agentscope.agent import ReActAgent
from agentscope.pipeline import MsgHub, stream_printing_messages
# %%
# Broadcasting with MsgHub
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# The ``MsgHub`` class is an **async context manager**, receiving a list of agents as its participants.
# When one participant generates a replying message, all other participants will receive this message by calling their ``observe`` method.
#
# That means within a ``MsgHub`` context, developers don't need to manually send a replying message from one agent to another.
# The broadcasting is automatically handled.
#
# Here we create four agents: Alice, Bob, Charlie and David.
# Then we start a meeting with Alice, Bob and Charlie by introducing themselves.
# Note David is not included in this meeting.
def create_agent(name: str, age: int, career: str) -> ReActAgent:
"""Create agent object by the given information."""
return ReActAgent(
name=name,
sys_prompt=f"You're {name}, a {age}-year-old {career}",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
),
formatter=DashScopeMultiAgentFormatter(),
)
alice = create_agent("Alice", 50, "teacher")
bob = create_agent("Bob", 35, "engineer")
charlie = create_agent("Charlie", 28, "designer")
david = create_agent("David", 30, "developer")
# %%
# Then we start a meeting and let them introduce themselves without manual message passing:
#
# .. hint:: The message in ``announcement`` will be broadcasted to all participants when entering the ``MsgHub`` context.
#
async def example_broadcast_message():
"""Example of broadcasting messages with MsgHub."""
# Create a message hub
async with MsgHub(
participants=[alice, bob, charlie],
announcement=Msg(
"user",
"Now introduce yourself in one sentence, including your name, age and career.",
"user",
),
) as hub:
# Group chat without manual message passing
await alice()
await bob()
await charlie()
asyncio.run(example_broadcast_message())
# %%
# Now let's check if Bob, Charlie and David received Alice's message.
#
async def check_broadcast_message():
"""Check if the messages are broadcast correctly."""
user_msg = Msg(
"user",
"Do you know who's Alice, and what she does? Answer me briefly.",
"user",
)
await bob(user_msg)
await charlie(user_msg)
await david(user_msg)
asyncio.run(check_broadcast_message())
# %%
# Now we observe that Bob and Charlie know Alice and her profession, while David has no idea
# about Alice since he is not included in the ``MsgHub`` context.
#
#
# Dynamic Participant Management
# ---------------------------------------
# Additionally, ``MsgHub`` supports to dynamically manage participants by the following methods:
#
# - ``add``: add one or multiple agents as new participants
# - ``delete``: remove one or multiple agents from participants, and they will no longer receive broadcasted messages
# - ``broadcast``: broadcast a message to all current participants
#
# .. note:: The newly added participants will not receive the previous messages.
#
# .. code-block:: python
#
# async with MsgHub(participants=[alice]) as hub:
# # Add new participants
# hub.add(david)
#
# # Remove participants
# hub.delete(alice)
#
# # Broadcast to all current participants
# await hub.broadcast(
# Msg("system", "Now we begin to ...", "system"),
# )
#
#
# Pipeline
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Pipeline serves as a syntax sugar for multi-agent orchestration.
#
# Currently, AgentScope provides three main pipeline implementations:
#
# 1. **Sequential Pipeline**: Execute agents one by one in a predefined order
# 2. **Fanout Pipeline**: Distribute the same input to multiple agents and collect their responses
# 3. **Stream Printing Messages**: Convert the printing messages from an agent into an async generator
#
# Sequential Pipeline
# ------------------------
# The sequential pipeline executes agents one by one, where the output of the previous agent
# becomes the input of the next agent.
#
# For example, the two following code snippets are equivalent:
#
#
# .. code-block:: python
# :caption: Code snippet 1: Manually call agents one by one
#
# msg = None
# msg = await alice(msg)
# msg = await bob(msg)
# msg = await charlie(msg)
# msg = await david(msg)
#
#
# .. code-block:: python
# :caption: Code snippet 2: Use sequential pipeline
#
# from agentscope.pipeline import sequential_pipeline
# msg = await sequential_pipeline(
# # List of agents to be executed in order
# agents=[alice, bob, charlie, david],
# # The first input message, can be None
# msg=None
# )
#
# %%
# Fanout Pipeline
# ------------------------
# The fanout pipeline distributes the same input message to multiple agents simultaneously and collects all their responses. This is useful when you want to gather different perspectives or expertise on the same topic.
#
# For example, the two following code snippets are equivalent:
#
#
# .. code-block:: python
# :caption: Code snippet 3: Manually call agents one by one
#
# from copy import deepcopy
#
# msgs = []
# msg = None
# for agent in [alice, bob, charlie, david]:
# msgs.append(await agent(deepcopy(msg)))
#
#
# .. code-block:: python
# :caption: Code snippet 4: Use fanout pipeline
#
# from agentscope.pipeline import fanout_pipeline
# msgs = await fanout_pipeline(
# # List of agents to be executed in order
# agents=[alice, bob, charlie, david],
# # The first input message, can be None
# msg=None,
# enable_gather=False,
# )
#
# .. note::
# The ``enable_gather`` parameter controls the execution mode of the fanout pipeline:
#
# - ``enable_gather=True`` (default): Executes all agents **concurrently** using ``asyncio.gather()``. This provides better performance for I/O-bound operations like API calls, as agents run in parallel.
# - ``enable_gather=False``: Executes agents **sequentially** one by one. This is useful when you need deterministic execution order or want to avoid overwhelming external services with concurrent requests.
#
# Choose concurrent execution for better performance, or sequential execution for predictable ordering and resource control.
#
# .. tip::
# By combining ``MsgHub`` and ``sequential_pipeline`` or ``fanout_pipeline``, you can create more complex workflows very easily.
#
#
# Stream Printing Messages
# -------------------------------------
# The ``stream_printing_messages`` function converts the printing messages from agent(s) into an async generator.
# It will help you to obtain the intermediate messages from the agent(s) in a streaming way.
#
# It accepts a list of agents and a coroutine task, then returns an async generator that yields tuples of ``(Msg, bool)``,
# containing the printing message during execution of the coroutine task.
#
# Note the messages with the same ``id`` are considered as the same message, and the ``last`` flag indicates whether it's the last chunk of this message.
#
# Taking the following code snippet as an example:
async def run_example_pipeline() -> None:
"""Run an example of streaming printing messages."""
agent = create_agent("Alice", 20, "student")
# We disable the terminal printing to avoid messy outputs
agent.set_console_output_enabled(False)
async for msg, last in stream_printing_messages(
agents=[agent],
coroutine_task=agent(
Msg("user", "Hello, who are you?", "user"),
),
):
print(msg, last)
if last:
print()
asyncio.run(run_example_pipeline())
# %%
# Advanced Pipeline Features
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Additionally, for reusability, we also provide a class-based implementation:
#
# .. code-block:: python
# :caption: Using SequentialPipeline class
#
# from agentscope.pipeline import SequentialPipeline
#
# # Create a pipeline object
# pipeline = SequentialPipeline(agents=[alice, bob, charlie, david])
#
# # Call the pipeline
# msg = await pipeline(msg=None)
#
# # Reuse the pipeline with different input
# msg = await pipeline(msg=Msg("user", "Hello!", "user"))
#
#
# .. code-block:: python
# :caption: Using FanoutPipeline class
#
# from agentscope.pipeline import FanoutPipeline
#
# # Create a pipeline object
# pipeline = FanoutPipeline(agents=[alice, bob, charlie, david])
#
# # Call the pipeline
# msgs = await pipeline(msg=None)
#
# # Reuse the pipeline with different input
# msgs = await pipeline(msg=Msg("user", "Hello!", "user"))
#

View File

@@ -0,0 +1,293 @@
# -*- coding: utf-8 -*-
"""
.. _plan:
Plan
=========================
The Plan Module enables agents to formally break down complex tasks into manageable sub-tasks and execute them systematically. Key features include:
- Support **manual plan specification**
- Comprehensive plan management capabilities:
- **Creating, modifying, abandoning, and restoring** plans
- **Switching** between multiple plans
- **Gracefully handling interruptions** by temporarily suspending plans to address user queries or urgent tasks
- **Real-time visualization and monitoring** of plan execution
.. note:: The current plan module has the following limitations, and we are working on improving them:
- The subtasks in a plan must be executed sequentially
Specifically, the plan module works by
- providing tool functions for plan management
- inserting hint messages to guide the ReAct agent to complete the plan
The following figure illustrates how the plan module works with the ReAct agent:
.. figure:: ../../_static/images/plan.png
:width: 90%
:alt: Plan module
:class: bordered-image
:align: center
How the plan module works with the ReAct agent
"""
import asyncio
import os
from agentscope.agent import ReActAgent
from agentscope.formatter import DashScopeChatFormatter
from agentscope.model import DashScopeChatModel
from agentscope.plan import PlanNotebook, Plan, SubTask
# %%
# PlanNotebook
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# The `PlanNotebook` class is the core of the plan module, responsible for providing
#
# - plan-related tool functions
# - hint messages to guide the agent to finish the plan
#
# The `PlanNotebook` class can be instantiated with the following parameters:
#
# .. list-table:: Parameters of the `PlanNotebook` constructor
# :header-rows: 1
#
# * - Name
# - Type
# - Description
# * - ``max_subtasks``
# - ``int | None``
# - The maximum number of subtasks allowed in a plan, infinite if None
# * - ``plan_to_hint``
# - ``Callable[[Plan | None], str | None] | None``
# - The function to generate hint message based on the current plan. If not provided, a default `DefaultPlanToHint` object will be used.
# * - ``storage``
# - ``PlanStorageBase | None``
# - The plan storage. If not provided, a default in-memory storage will be used.
#
# The ``plan_to_hint`` callable object is the most important part of the
# `PlanNotebook` class, also serves as the interface for prompt engineering.
# We have built a default `DefaultPlanToHint` class that can be used directly.
# Developers are encouraged to providing their own ``plan_to_hint`` function
# for better performance.
#
# The ``storage`` is to store historical plans, allowing agent to
# retrieve and restore historical plans. Developers are encouraged to
# implement their own plan storage by inheriting the ``PlanStorageBase`` class.
# If not provided, a default in-memory storage will be used.
#
# .. tip:: The ``PlanStorageBase`` class inherits from the ``StateModule``
# class, so that the plan storage will also be saved and loaded by the
# session management.
#
# The core attributes and methods of the `PlanNotebook` class are summarized
# as follows:
#
# .. list-table:: Core attributes and methods of the `PlanNotebook` class
# :header-rows: 1
#
# * - Type
# - Name
# - Description
# * - attribute
# - ``current_plan``
# - The current plan that the agent is executing
# * -
# - ``storage``
# - The storage for historical plans, used for retrieving and restoring historical plans
# * -
# - ``plan_to_hint``
# - A callable object that takes the current plan as input and generates a hint message to guide the agent to finish the plan
# * - method
# - ``list_tools``
# - List all the tool functions provided by the `PlanNotebook` class
# * -
# - ``get_current_hint``
# - Get the hint message for the current plan, which will call the ``plan_to_hint`` function
# * -
# - | ``create_plan``,
# | ``view_subtasks``,
# | ``revise_current_plan``,
# | ``update_subtask_state``,
# | ``finish_subtask``,
# | ``finish_plan``,
# | ``view_historical_plans``,
# | ``recover_historical_plan``
# - The tool functions that allows the agent to manage the plan and subtasks
# * -
# - ``register_plan_change_hook``
# - Register a hook function that will be called when the plan is changed, used to plan visualization and monitoring
# * -
# - ``remove_plan_change_hook``
# - Remove a registered plan change hook function
#
# The ``list_tools`` method is a quick way to obtain all tool functions, so that you can register them to the agent's toolkit.
plan_notebook = PlanNotebook()
async def list_tools() -> None:
"""List the tool functions provided by PlanNotebook."""
print("The tools provided by PlanNotebook:")
for tool in plan_notebook.list_tools():
print(tool.__name__)
asyncio.run(list_tools())
# %%
# Working with ReActAgent
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# The `ReActAgent` in AgentScope has integrated the plan module by a ``plan_notebook`` parameter in its constructor.
# Once provided, the agent will
#
# - be equipped with the plan management tool functions, and
# - be inserted with the hint messages at the beginning of each reasoning step
#
# There are two ways to use the plan module with the `ReActAgent`:
#
# - Manual plan specification: Users can manually create a plan by calling the ``create_plan`` tool function, and initialize the `ReActAgent` with the plan notebook.
# - Agent-managed plan execution: The agent will create and manage the plan by itself, by calling the plan management tool functions.
#
# Manual Plan Specification
# ---------------------------------
# Manually creating a plan is straightforward by calling the ``create_plan`` tool function.
# The following is an example of manually creating a plan to conduct a comprehensive research on the LLM-empowered agent.
#
async def manual_plan_specification() -> None:
"""Manual plan specification example."""
await plan_notebook.create_plan(
name="Research on Agent",
description="Conduct a comprehensive research on the LLM-empowered agent.",
expected_outcome="A Markdown format report answer three questions: 1. What's agent? 2. What's the current state of the art of agent? 3. What's the future trend of agent?",
subtasks=[
SubTask(
name="Search agent-related survey papers",
description=(
"Search for survey parers on multiple sources, including "
"Google Scholar, arXiv, and Semantic Scholar. Must be "
"published after 2021 and have more than 50 citations."
),
expected_outcome="A paper list in Markdown format",
),
SubTask(
name="Read and summarize the papers",
description=(
"Read the papers found in the previous step, and "
"summarize the key points, including the definition, "
"taxonomy, challenges, and key directions."
),
expected_outcome="A summary of the key points in Markdown format",
),
SubTask(
name="Research on recent advances of large company",
description=(
"Research on the recent advances of large companies, "
"including Google, Microsoft, OpenAI, Anthropic, Alibaba "
"and Meta. Find the official blogs or news articles."
),
expected_outcome="A recent advances of large company ",
),
SubTask(
name="Write a report",
description=(
"Write a report based on the previous steps, and answer "
"the three questions in the expected outcome."
),
expected_outcome=(
"A Markdown format report answer three questions: 1. "
"What's agent? 2. What's the current state of the art of "
"agent? 3. What's the future trend of agent?"
),
),
],
)
print("The current hint message:\n")
msg = await plan_notebook.get_current_hint()
print(f"{msg.name}: {msg.content}")
asyncio.run(manual_plan_specification())
# %%
# After creating the plan, you can initialize the `ReActAgent` with the
# plan notebook as follows:
agent = ReActAgent(
name="Friday",
sys_prompt="You are a helpful assistant.",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
),
formatter=DashScopeChatFormatter(),
plan_notebook=plan_notebook,
)
# %%
# Agent-Managed Plan Execution
# ---------------------------------
# Agent can also create and manage the plan by itself, by calling the plan management tool functions.
# We just need to initialize the `ReActAgent` with the plan notebook as follows:
#
agent = ReActAgent(
name="Friday",
sys_prompt="You are a helpful assistant.",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
),
formatter=DashScopeChatFormatter(),
plan_notebook=PlanNotebook(),
)
# %%
# After that, we can build a loop to interact with the agent as follows.
# Once the task is complex, the agent will create a plan by itself and
# execute the plan step by step.
#
# .. code-block:: python
# :caption: Build conversation with the plan agent
#
# async def interact_with_agent() -> None:
# """Interact with the plan agent."""
# user = UserAgent(name="user")
#
# msg = None
# while True:
# msg = await user(msg)
# if msg.get_text_content() == "exit":
# break
# msg = await agent(msg)
#
# asyncio.run(interact_with_agent())
#
#
# Plan Visualization and Monitoring
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# AgentScope supports real-time visualization and monitoring of the plan
# execution by the plan change hook function.
#
# They will be triggered when the plan is changed by calling the tool
# functions. A template of the plan change hook function is as follows:
#
def plan_change_hook_template(self: PlanNotebook, plan: Plan) -> None:
"""A template of the plan change hook function.
Args:
self (`PlanNotebook`):
The PlanNotebook instance.
plan (`Plan`):
The current plan instance (after the change).
"""
# Forward the plan to the frontend for visualization or other processing

View File

@@ -0,0 +1,328 @@
# -*- coding: utf-8 -*-
"""
.. _prompt:
Prompt Formatter
=========================
The formatter module in AgentScope is responsible for
- converting messages into the expected format for different LLM APIs,
- (optional) truncating messages to fit within token limits,
- (optional) prompt engineering, e.g. summarizing long conversations.
The last two are optional and can also be handled by developers within the memory or at the agent level.
In AgentScope, there are two types of formatters, "ChatFormatter" and "MultiAgentFormatter", distinguished by the agent identities in their input messages.
- **ChatFormatter**: Designed for standard user-assistant scenario (chatbot), using the ``role`` field to identify the user and assistant.
- **MultiAgentFormatter**: Designed for multi-agent scenario, use the ``name`` field to identify different agents, which will combine conversation history into a single user message dictionary.
The built-in formatters are listed below
.. list-table:: The built-in formatters in AgentScope
:header-rows: 1
* - API Provider
- User-assistant Scenario
- Multi-Agent Scenario
* - OpenAI
- ``OpenAIChatFormatter``
- ``OpenAIMultiAgentFormatter``
* - Anthropic
- ``AnthropicChatFormatter``
- ``AnthropicMultiAgentFormatter``
* - DashScope
- ``DashScopeChatFormatter``
- ``DashScopeMultiAgentFormatter``
* - Gemini
- ``GeminiChatFormatter``
- ``GeminiChatFormatter``
* - Ollama
- ``OllamaChatFormatter``
- ``OllamaMultiAgentFormatter``
* - DeepSeek
- ``DeepSeekChatFormatter``
- ``DeepSeekMultiAgentFormatter``
* - vLLM
- ``OpenAIChatFormatter``
- ``OpenAIMultiAgentFormatter``
.. tip:: The OpenAI API supports the `name` field, so that `OpenAIChatFormatter` can also be used in multi-agent scenario. You can also use the `OpenAIMultiAgentFormatter` instead, which combine conversation history into a single user message.
Besides, the built-in formatters support to convert different message blocks into the expected format for the target API, which are list below:
.. list-table:: The supported message blocks in the built-in formatters
:header-rows: 1
* - Formatter
- tool_use/result
- image
- audio
- video
- thinking
* - ``OpenAIChatFormatter``
- ✅
- ✅
- ✅
- ❌
-
* - ``DashScopeChatFormatter``
- ✅
- ✅
- ✅
- ❌
-
* - ``DashScopeMultiAgentFormatter``
- ✅
- ✅
- ✅
- ❌
-
* - ``AnthropicChatFormatter``
- ✅
- ✅
- ❌
- ❌
- ✅
* - ``AnthropicMultiAgentFormatter``
- ✅
- ✅
- ❌
- ❌
- ✅
* - ``GeminiChatFormatter``
- ✅
- ✅
- ✅
- ✅
-
* - ``GeminiMultiAgentFormatter``
- ✅
- ✅
- ✅
- ✅
-
* - ``OllamaChatFormatter``
- ✅
- ✅
- ❌
- ❌
-
* - ``OllamaMultiAgentFormatter``
- ✅
- ✅
- ❌
- ❌
-
* - ``DeepSeekChatFormatter``
- ✅
- ❌
- ❌
- ❌
-
* - ``DeepSeekMultiAgentFormatter``
- ✅
- ❌
- ❌
- ❌
-
.. note:: As stated in the `official documentation <https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#preserving-thinking-blocks>`_, only Anthropic suggests to preserve the thinking blocks in prompt formatting. For the others, we just ignore the thinking blocks in the input messages.
ReAct-Oriented Formatting
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The built-in formatters are all designed to support ReAct-style agents, where the input messages **consist of alternating conversation history and tool call sequences**.
In user-assistant scenario, the conversation history includes the user and assistant messages, we just convert them into the expected format directly.
However, in multi-agent scenario, the conversation history is a list of messages from different agents as follows:
.. figure:: ../../_static/images/multiagent_msgs.png
:alt: example of multiagent messages
:width: 85%
:align: center
*Example of multi-agent messages*
Therefore, we have to merge the conversation history into a single user message with tags "<history>" and "</history>".
Taking DashScope as an example, the formatted message will look like this:
"""
from agentscope.token import HuggingFaceTokenCounter
from agentscope.formatter import DashScopeMultiAgentFormatter
from agentscope.message import Msg, ToolResultBlock, ToolUseBlock, TextBlock
import asyncio, json
input_msgs = [
# System prompt
Msg("system", "You're a helpful assistant named Friday", "system"),
# Conversation history
Msg("Bob", "Hi, Alice, do you know the nearest library?", "assistant"),
Msg(
"Alice",
"Sorry, I don't know. Do you have any idea, Charlie?",
"assistant",
),
Msg(
"Charlie",
"No, let's ask Friday. Friday, get me the nearest library.",
"assistant",
),
# Tool sequence
Msg(
"Friday",
[
ToolUseBlock(
type="tool_use",
name="get_current_location",
id="1",
input={},
),
],
"assistant",
),
Msg(
"system",
[
ToolResultBlock(
type="tool_result",
name="get_current_location",
id="1",
output=[TextBlock(type="text", text="104.48, 36.30")],
),
],
"system",
),
Msg(
"Friday",
[
ToolUseBlock(
type="tool_use",
name="search_around",
id="2",
input={"location": [104.48, 36.30], "keyword": "library"},
),
],
"assistant",
),
Msg(
"system",
[
ToolResultBlock(
type="tool_result",
name="search_around",
id="2",
output=[TextBlock(type="text", text="[...]")],
),
],
"system",
),
# Conversation history continues
Msg("Friday", "The nearest library is ...", "assistant"),
Msg("Bob", "Thanks, Friday!", "assistant"),
Msg("Alice", "Let's go together.", "assistant"),
]
async def run_formatter_example() -> list[dict]:
"""Example of how to format multi-agent messages."""
formatter = DashScopeMultiAgentFormatter()
formatted_message = await formatter.format(input_msgs)
print("The formatted message:")
print(json.dumps(formatted_message, indent=4))
return formatted_message
formatted_message = asyncio.run(run_formatter_example())
# %%
# Specifically, the conversation histories are formatted into:
#
print("The first conversation history:")
print(formatted_message[1]["content"])
print("\nThe second conversation history:")
print(formatted_message[-1]["content"])
# %%
# Truncation-based Formatting
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# With the token module in AgentScope, the built-in formatters support to truncate the input messages by **deleting the oldest messages** (except the system prompt message) when the token exceeds the limit.
#
# Taking OpenAIFormatter as an example, we first calculate the total number of tokens of the input messages.
#
async def run_token_counter() -> int:
"""Compute the token number of the input messages."""
# We use huggingface token counter for dashscope models.
token_counter = HuggingFaceTokenCounter(
"Qwen/Qwen2.5-VL-3B-Instruct",
use_mirror=False,
)
return await token_counter.count(formatted_message)
# %%
# Then we set the maximum token limit to 20 tokens less than the total number of tokens and run the formatter.
#
async def run_truncated_formatter() -> None:
"""Example of how to format messages with truncation."""
token_counter = HuggingFaceTokenCounter(
pretrained_model_name_or_path="Qwen/Qwen2.5-VL-3B-Instruct",
use_mirror=False,
)
formatter = DashScopeMultiAgentFormatter(
token_counter=token_counter,
max_tokens=n_tokens - 20,
)
truncated_formatted_message = await formatter.format(input_msgs)
n_truncated_tokens = await token_counter.count(truncated_formatted_message)
print("The tokens after truncation: ", n_truncated_tokens)
print("\nThe conversation history after truncation:")
print(truncated_formatted_message[1]["content"])
# %%
# We can see the first two messages from Bob and Alice are removed to fit within the context length limits.
#
#
# Customizing Formatter
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope provides two base classes ``FormatterBase`` and its child class ``TruncatedFormatterBase``.
# The ``TruncatedFormatterBase`` class provides the FIFO truncation strategy, and all the built-in formatters are inherited from it.
#
# .. list-table:: The base classes of formatters in AgentScope
# :header-rows: 1
#
# * - Class
# - Abstract Method
# - Description
# * - ``FormatterBase``
# - ``format``
# - Format the input ``Msg`` objects into the expected format for the target API
# * - ``TruncatedFormatterBase``
# - ``_format_agent_message``
# - Format the agent messages, which may contain multiple identities in multi-agent scenario
# * -
# - ``_format_tool_sequence``
# - Format the tool use and result sequence into the expected format
# * -
# - ``_format`` (optional)
# - Format the input ``Msg`` objects into the expected format for the target API
#
# .. tip:: - The ``_format`` in ``TruncatedFormatterBase`` groups input messages into agent messages and tool sequences, and then format them by calling ``_format_agent_message`` and ``_format_tool_sequence`` respectively. You can override it to implement your own formatting strategy.
# - Optionally, you can override the ``_truncate`` method in ``TruncatedFormatterBase`` to implement your own truncation strategy.
#
# Further Reading
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# - :ref:`token`
# - :ref:`model`
#

View File

@@ -0,0 +1,422 @@
# -*- coding: utf-8 -*-
"""
.. _rag:
RAG
===========================
AgentScope provides built-in support for Retrieval-Augmented Generation (RAG)
tasks. This tutorial demonstrates
- how to use the RAG module in AgentScope,
- how to use **multimodal** RAG,
- how to integrate the RAG module with the ``ReActAgent`` in
- **agentic manner** and
- **generic manner**:
.. list-table:: RAG module integration methods
:header-rows: 1
* - Integration Manner
- Description
- Advantages
- Disadvantages
* - Agentic Manner
- The RAG module is integrated with the agent as a tool, and the agent can decide when to retrieve knowledge and the queries to be retrieved.
- - The query rewriting and knowledge retrieval are integrated into the ReAct process, which is more flexible,
- the agent can rewrite the query based on all the available information,
- only retrieve knowledge when necessary.
- High requirements for the LLM's reasoning and tool-use capabilities.
* - Generic Manner
- Retrieve knowledge at the beginning of each reply, and attach the retrieved knowledge to the prompt in a user message.
- - Simple, easy to implement,
- does not require high reasoning and tool-use capabilities from the LLM.
- - Still retrieve knowledge even when not necessary, and
- if the retrieval is imperceptible to the user, the waiting time may be longer.
.. note:: As an open-source project, AgentScope doesn't insist that developers
use the built-in RAG module. Our target is make the development easier and
more enjoyable, so integrating other RAG implementations, frameworks, or
services are welcome and encouraged!
"""
import asyncio
import json
import os
from matplotlib import pyplot as plt
import agentscope
from agentscope.agent import ReActAgent
from agentscope.embedding import (
DashScopeTextEmbedding,
DashScopeMultiModalEmbedding,
)
from agentscope.formatter import DashScopeChatFormatter
from agentscope.message import Msg
from agentscope.model import DashScopeChatModel
from agentscope.rag import (
TextReader,
SimpleKnowledge,
QdrantStore,
Document,
ImageReader,
)
from agentscope.tool import Toolkit
# %%
# Using RAG Module
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# The RAG module in AgentScope is composed of two core components:
#
# - **Reader**: responsible for reading and chunking the input documents.
# - **Knowledge**: responsible for algorithm implementation of knowledge retrieval and updating.
#
# .. note:: We're integrating more vector databases and readers into AgentScope. Contributions are welcome!
#
# The currently built-in readers include:
#
for _ in agentscope.rag.__all__:
if _.endswith("Reader"):
print(f"- {_}")
# %%
# they are responsible for reading data and chunking them into ``Document`` objects.
# The ``Document`` class has the following fields:
#
# - ``metadata``: the metadata of the document, including the content, doc_id, chunk_id, and total_chunks.
# - ``embedding``: the embedding vector of the document, which will be filled when the document is added to or retrieved from the knowledge base.
# - ``score``: the relevance score of the document, which will be filled when the document is retrieved from the knowledge base.
#
# Taking the ``TextReader`` as an example, it can read and chunk documents from text strings.
#
async def example_text_reader(print_docs: bool) -> list[Document]:
"""The example of using TextReader."""
# Create a text reader with chunk size of 512 characters, split by characters
reader = TextReader(chunk_size=512, split_by="paragraph")
# Read documents from a text string
documents = await reader(
text=(
# Fake personal profile for demonstration
"I'm John Doe, 28 years old.\n"
"I live in San Francisco. I work at OpenAI as a "
"software engineer. I love hiking and photography.\n"
"My father is Michael Doe, a doctor. I'm very proud of him. "
"My mother is Sarah Doe, a teacher. She is very kind and "
"always helps me with my studies.\n"
"I'm now a PhD student at Stanford University, majoring in "
"Computer Science. My advisor is Prof. Jane Williams, who is "
"a leading expert in artificial intelligence. I have published "
"several papers in top conferences, such as NeurIPS and ICML.\n"
"My best friend is James Smith.\n"
),
)
if print_docs:
print("The length of the documents:", len(documents))
for idx, doc in enumerate(documents):
print("Document #", idx)
print("\tScore: ", doc.score)
print("\tMetadata: ", json.dumps(doc.metadata, indent=2), "\n")
return documents
docs = asyncio.run(example_text_reader(print_docs=True))
# %%
# Note there doesn't exist a universally best chunk size and splitting method, especially for PDF files, we highly
# encourage developers to implement or contribute their own readers according to their specific scenarios.
# To create a custom reader, you only need to inherit the ``ReaderBase`` class and implement the ``__call__`` method.
#
# After chunking the documents, we can create a knowledge base to store the documents and perform retrieval.
# Such a knowledge base is initialized by providing **an embedding model** and **an embedding store** (also known as a vector database).
# Agentscope provides built-in support for `Qdrant <https://qdrant.tech/>`_ as the embedding store and a simple knowledge base implementation ``SimpleKnowledge``.
# They can be used as follows:
#
# .. note::
#
# - We're integrating more vector databases into AgentScope. Contributions are welcome!
# - The Qdrant store supports various storage backends by the ``location`` parameter, including in-memory, local file, and remote server. Refer to the `Qdrant documentation <https://qdrant.tech/>`_ for more details.
#
async def build_knowledge_base() -> SimpleKnowledge:
"""Build a knowledge base with sample documents."""
# Read documents using the text reader
documents = await example_text_reader(print_docs=False)
# Create an in-memory knowledge base instance
knowledge = SimpleKnowledge(
# Choose an embedding model to convert text to embedding vectors
embedding_model=DashScopeTextEmbedding(
api_key=os.environ["DASHSCOPE_API_KEY"],
model_name="text-embedding-v4",
dimensions=1024,
),
# Choose Qdrant as the embedding store
embedding_store=QdrantStore(
location=":memory:", # Use in-memory storage for demonstration
collection_name="test_collection",
dimensions=1024, # The dimension of the embedding vectors
),
)
# Insert documents into the knowledge base
await knowledge.add_documents(documents)
# Retrieve relevant documents based on a given query
docs = await knowledge.retrieve(
query="Who is John Doe's father?",
limit=3,
score_threshold=0.5,
)
print("Retrieved Documents:")
for doc in docs:
print(doc, "\n")
return knowledge
knowledge = asyncio.run(build_knowledge_base())
# %%
# The knowledge base class provides two main methods: ``add_documents`` and
# ``retrieve``, which are used to add documents to the knowledge base and
# retrieve relevant documents based on a given query, respectively.
#
# In addition, the knowledge base class also provides a convenient method
# ``retrieve_knowledge``, which wraps the ``retrieve`` method into a tool
# function that can be directly registered in the toolkit of an agent.
#
#
# Customizing RAG Components
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope supports and encourages developers to customize their own RAG components, including readers, knowledge bases and embedding stores.
# Specifically, we provide the following base classes for customization:
#
# .. list-table:: RAG Base Classes
# :header-rows: 1
#
# * - Base Class
# - Description
# - Abstract Methods
# * - ``ReaderBase``
# - The base class for all readers.
# - ``__call__``
# * - ``VDBStoreBase``
# - The base class for embedding stores (vector databases).
# - | ``add``
# | ``search``
# | ``get_client`` (optional)
# | ``delete`` (optional)
# * - ``KnowledgeBase``
# - The base class for knowledge bases.
# - | ``retrieve``
# | ``add_documents``
#
#
# The `get_client` method in the ``VDBStoreBase`` allows developers to access the full functionality of the underlying vector database.
# So that they can implement more advanced features based on the vector database, e.g. index management, advanced search, etc.
#
# Integrating with ReActAgent
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Next we demonstrate how to integrate the RAG module with the ``ReActAgent``
# class in AgentScope in agentic and generic manners.
#
# Agentic Manner
# --------------------------------
# In agentic manner, the ReAct agent is empowered with the ability to decide when to retrieve knowledge and the queries to be retrieved.
# It's very easy to integrate the RAG module with the ``ReActAgent`` class in AgentScope, just by registering the ``retrieve_knowledge`` method of the knowledge base as a tool,
# and providing a proper description for the tool.
async def example_agentic_manner() -> None:
"""The example of integrating RAG module with ReActAgent in agentic manner."""
# Create a ReAct agent
toolkit = Toolkit()
# Create the ReAct agent with DashScope as the model
agent = ReActAgent(
name="Friday",
sys_prompt="You are a helpful assistant named Friday.",
model=DashScopeChatModel(
api_key=os.environ["DASHSCOPE_API_KEY"],
model_name="qwen-max",
),
formatter=DashScopeChatFormatter(),
toolkit=toolkit,
)
print("The first response: ")
# Ask some questions about Tony Stank
await agent(
Msg(
"user",
"John Doe is my best friend.",
"user",
),
)
# Register the retrieve_knowledge method as a tool function in the toolkit
toolkit.register_tool_function(
knowledge.retrieve_knowledge,
func_description=( # Provide a clear description for the tool
"The tool used to retrieve documents relevant to the given query. "
"Use this tool when you need to find some information about John Doe."
),
)
print("\n\nThe second response: ")
# We hope the agent can rewrite the query to be more specific, e.g.
# "Who is Tony Stank's father?" or "Tony Stank's father"
await agent(
Msg(
"user",
"Do you know who his father is?",
"user",
),
)
asyncio.run(example_agentic_manner())
# %%
# In the above example, our question is "Do you know who his father is?".
# We hope the agent can rewrite the query with the historical information, and
# rewrite it to be more specific, e.g. "Who is John Doe's father?" or "John Doe's father".
#
#
# Generic Manner
# --------------------------------
# The ``ReActAgent`` also integrates the RAG module in a generic manner, which
# retrieves knowledge at the beginning of each reply, and attaches the
# retrieved knowledge to the prompt in a user message.
#
# Just set the ``knowledge`` parameter of the ``ReActAgent``, and the agent
# will automatically retrieve knowledge at the beginning of each reply.
#
async def example_generic_manner() -> None:
"""The example of integrating RAG module with ReActAgent in generic manner."""
# Create a ReAct agent
agent = ReActAgent(
name="Friday",
sys_prompt="You are a helpful assistant named Friday.",
model=DashScopeChatModel(
api_key=os.environ["DASHSCOPE_API_KEY"],
model_name="qwen-max",
),
formatter=DashScopeChatFormatter(),
#
knowledge=knowledge,
)
await agent(
Msg(
"user",
"Do you know who John Doe's father is?",
"user",
),
)
print("Take a look at the agent's memory:")
content = (await agent.memory.get_memory())[1].content
print(json.dumps(content, indent=2, ensure_ascii=False))
asyncio.run(example_generic_manner())
# %%
# Multimodal RAG
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# The RAG module in AgentScope supports multimodal RAG natively, as
#
# - AgentScope supports multimodal embedding API, e.g. ``DashScopeMultimodalEmbedding``.
# - The ``Document`` class supports text, image, and other modalities in its ``metadata`` field.
#
# Thus, we can directly use the multimodal reader and embedding model to build
# a multimodal knowledge base as follows.
#
# First we prepare an image with some text about my name.
# Prepare an image with the text "John Doe's father is Michael Doe."
path_image = "./example.jpg"
plt.figure(figsize=(8, 3))
plt.text(
0.5,
0.5,
"My name is Tony Stank",
ha="center",
va="center",
fontsize=30,
)
plt.axis("off")
plt.savefig(path_image, bbox_inches="tight", pad_inches=0.1)
plt.close()
# %%
# Then we can build a multimodal knowledge base with the image document.
# The example is the same as before, just using the ``ImageReader`` and
# ``DashScopeMultiModalEmbedding`` instead of the text counterparts.
#
async def example_multimodal_rag() -> None:
"""The example of using multimodal RAG."""
# Read the image using the ImageReader
reader = ImageReader()
docs = await reader(image_url=path_image)
# Create a knowledge base with the new image document
knowledge = SimpleKnowledge(
embedding_model=DashScopeMultiModalEmbedding(
api_key=os.environ["DASHSCOPE_API_KEY"],
model_name="multimodal-embedding-v1",
dimensions=1024,
),
embedding_store=QdrantStore(
location=":memory:",
collection_name="test_collection",
dimensions=1024,
),
)
await knowledge.add_documents(docs)
agent = ReActAgent(
name="Friday",
sys_prompt="You are a helpful assistant named Friday.",
model=DashScopeChatModel(
api_key=os.environ["DASHSCOPE_API_KEY"],
model_name="qwen-vl-max",
),
formatter=DashScopeChatFormatter(),
knowledge=knowledge,
)
await agent(
Msg(
"user",
"What's my name?",
"user",
),
)
# Let's see the last message from the agent
print("\nThe image is attached in the agent's memory:")
print((await agent.memory.get_memory())[1])
asyncio.run(example_multimodal_rag())
# %%
# We can see that the agent can answer the question based on the retrieved
# image.

View File

@@ -0,0 +1,496 @@
# -*- coding: utf-8 -*-
"""
.. _realtime:
Realtime Agent
====================
The **realtime** agent is designed to handle real-time interactions, such as
voice conversations or live chat sessions.
The realtime agent in AgentScope features:
- Integration with OpenAI, DashScope, Gemini, and other realtime model APIs
- Unified event interface to simplify interactions with different realtime models
- Support for tool calling capabilities
- Support for multi-agent interactions
.. note:: The realtime agent is currently under active development. We welcome
community contributions, discussions, and feedback! If you're interested in
realtime agents, please join our discussion and development.
"""
import asyncio
import os
from agentscope.agent import RealtimeAgent
from agentscope.realtime import (
DashScopeRealtimeModel,
OpenAIRealtimeModel,
GeminiRealtimeModel,
)
# %%
# Creating Realtime Models
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# AgentScope currently supports the following realtime model APIs:
#
# .. list-table::
# :header-rows: 1
# :widths: 15 25 25 15 20
#
# * - Provider
# - Class
# - Supported Models
# - Input Modalities
# - Tool Support
# * - DashScope
# - ``DashScopeRealtimeModel``
# - ``qwen3-omni-flash-realtime``
# - Text, Audio, Image
# - No
# * - OpenAI
# - ``OpenAIRealtimeModel``
# - ``gpt-4o-realtime-preview``
# - Text, Audio
# - Yes
# * - Gemini
# - ``GeminiRealtimeModel``
# - ``gemini-2.5-flash-native-audio-preview-09-2025``
# - Text, Audio, Image
# - Yes
#
#
# Here are examples of initializing different realtime models:
#
# .. code-block:: python
# :caption: Example of initializing different realtime models
# # DashScope realtime model
# dashscope_model = DashScopeRealtimeModel(
# model_name="qwen3-omni-flash-realtime",
# api_key=os.getenv("DASHSCOPE_API_KEY"),
# voice="Cherry", # Options: "Cherry", "Serena", "Ethan", "Chelsie"
# enable_input_audio_transcription=True,
# )
#
# # OpenAI realtime model
# openai_model = OpenAIRealtimeModel(
# model_name="gpt-4o-realtime-preview",
# api_key=os.getenv("OPENAI_API_KEY"),
# voice="alloy", # Options: "alloy", "echo", "marin", "cedar"
# enable_input_audio_transcription=True,
# )
#
# # Gemini realtime model
# gemini_model = GeminiRealtimeModel(
# model_name="gemini-2.5-flash-native-audio-preview-09-2025",
# api_key=os.getenv("GEMINI_API_KEY"),
# voice="Puck", # Options: "Puck", "Charon", "Kore", "Fenrir"
# enable_input_audio_transcription=True,
# )
#
# The realtime model provides the following key methods:
#
# .. list-table::
# :header-rows: 1
# :widths: 30 70
#
# * - Method
# - Description
# * - ``connect(outgoing_queue, instructions, tools)``
# - Establish WebSocket connection to the realtime model API
# * - ``disconnect()``
# - Close the WebSocket connection
# * - ``send(data)``
# - Send audio/text/image data to the realtime model for processing
#
# The ``outgoing_queue`` parameter in ``connect()`` is an asyncio queue used to
# forward events from the realtime model to the outside (e.g., the agent or frontend).
#
#
# Model Events Interface
# -----------------------
#
# AgentScope provides a unified ``agentscope.realtime.ModelEvents`` interface to simplify
# interactions with different realtime models. The following events are
# supported:
#
# .. note:: The "session" in ModelEvents refers to the WebSocket connection
# session between the realtime model and the model API, not the session
# between the frontend and backend.
#
# .. list-table::
# :header-rows: 1
# :widths: 40 60
#
# * - Event
# - Description
# * - ``ModelEvents.ModelSessionCreatedEvent``
# - Session is successfully created
# * - ``ModelEvents.ModelSessionEndedEvent``
# - Session has ended
# * - ``ModelEvents.ModelResponseCreatedEvent``
# - Model begins generating a response
# * - ``ModelEvents.ModelResponseDoneEvent``
# - Model finished generating a response
# * - ``ModelEvents.ModelResponseAudioDeltaEvent``
# - Streaming audio data chunk from the model
# * - ``ModelEvents.ModelResponseAudioDoneEvent``
# - Audio response is complete
# * - ``ModelEvents.ModelResponseAudioTranscriptDeltaEvent``
# - Streaming transcription chunk of audio response
# * - ``ModelEvents.ModelResponseAudioTranscriptDoneEvent``
# - Audio transcription is complete
# * - ``ModelEvents.ModelResponseToolUseDeltaEvent``
# - Streaming tool call parameters
# * - ``ModelEvents.ModelResponseToolUseDoneEvent``
# - Tool call parameters are complete
# * - ``ModelEvents.ModelInputTranscriptionDeltaEvent``
# - Streaming transcription chunk of user input
# * - ``ModelEvents.ModelInputTranscriptionDoneEvent``
# - User input transcription is complete
# * - ``ModelEvents.ModelInputStartedEvent``
# - Detected start of user audio input (VAD)
# * - ``ModelEvents.ModelInputDoneEvent``
# - Detected end of user audio input (VAD)
# * - ``ModelEvents.ModelErrorEvent``
# - An error occurred
#
#
#
# Creating a Realtime Agent
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# The ``RealtimeAgent`` serves as a bridge layer that:
#
# - Converts ``ModelEvents`` from realtime models into ``ServerEvents`` for
# frontend and other agents
# - Receives ``ClientEvents`` from frontend or other agents and forwards them
# to the realtime model API
# - Manages the agent's lifecycle and event queues
#
# Server and Client Events
# -------------------------
#
# AgentScope provides unified ``ServerEvents`` and ``ClientEvents`` for
# communication between backend and frontend:
#
# **ServerEvents** (Backend → Frontend):
#
# .. list-table::
# :header-rows: 1
# :widths: 40 60
#
# * - Event
# - Description
# * - ``ServerEvents.ServerSessionCreatedEvent``
# - Session created in backend
# * - ``ServerEvents.ServerSessionUpdatedEvent``
# - Session updated in backend
# * - ``ServerEvents.ServerSessionEndedEvent``
# - Session ended in backend
# * - ``ServerEvents.AgentReadyEvent``
# - Agent is ready to receive inputs
# * - ``ServerEvents.AgentEndedEvent``
# - Agent has ended
# * - ``ServerEvents.AgentResponseCreatedEvent``
# - Agent starts generating response
# * - ``ServerEvents.AgentResponseDoneEvent``
# - Agent finished generating response
# * - ``ServerEvents.AgentResponseAudioDeltaEvent``
# - Streaming audio chunk from agent
# * - ``ServerEvents.AgentResponseAudioDoneEvent``
# - Audio response complete
# * - ``ServerEvents.AgentResponseAudioTranscriptDeltaEvent``
# - Streaming transcription of agent response
# * - ``ServerEvents.AgentResponseAudioTranscriptDoneEvent``
# - Transcription complete
# * - ``ServerEvents.AgentResponseToolUseDeltaEvent``
# - Streaming tool call data
# * - ``ServerEvents.AgentResponseToolUseDoneEvent``
# - Tool call complete
# * - ``ServerEvents.AgentResponseToolResultEvent``
# - Tool execution result
# * - ``ServerEvents.AgentInputTranscriptionDeltaEvent``
# - Streaming transcription of user input
# * - ``ServerEvents.AgentInputTranscriptionDoneEvent``
# - Input transcription complete
# * - ``ServerEvents.AgentInputStartedEvent``
# - User audio input started
# * - ``ServerEvents.AgentInputDoneEvent``
# - User audio input ended
# * - ``ServerEvents.AgentErrorEvent``
# - An error occurred
#
# **ClientEvents** (Frontend → Backend):
#
# .. list-table::
# :header-rows: 1
# :widths: 40 60
#
# * - Event
# - Description
# * - ``ClientEvents.ClientSessionCreateEvent``
# - Create a new session with specified configuration
# * - ``ClientEvents.ClientSessionEndEvent``
# - End current session
# * - ``ClientEvents.ClientResponseCreateEvent``
# - Request agent to generate response immediately
# * - ``ClientEvents.ClientResponseCancelEvent``
# - Interrupt agent's current response
# * - ``ClientEvents.ClientTextAppendEvent``
# - Append text input
# * - ``ClientEvents.ClientAudioAppendEvent``
# - Append audio input
# * - ``ClientEvents.ClientAudioCommitEvent``
# - Commit audio input (signal end of input)
# * - ``ClientEvents.ClientImageAppendEvent``
# - Append image input
# * - ``ClientEvents.ClientToolResultEvent``
# - Send tool execution result
#
# Initializing a Realtime Agent
# ------------------------------
#
# Here's how to create and use a realtime agent:
async def example_realtime_agent() -> None:
"""Example of creating and using a realtime agent."""
agent = RealtimeAgent(
name="Friday",
sys_prompt="You are a helpful assistant named Friday.",
model=DashScopeRealtimeModel(
model_name="qwen3-omni-flash-realtime",
api_key=os.getenv("DASHSCOPE_API_KEY"),
),
)
# Create a queue to receive messages from the agent
outgoing_queue = asyncio.Queue()
# The agent is now ready to handle inputs
# Handle outgoing messages in a separate task
async def handle_agent_messages():
while True:
event = await outgoing_queue.get()
# Process the event (e.g., send to frontend via WebSocket)
print(f"Agent event: {event.type}")
# Start the message handling task
asyncio.create_task(handle_agent_messages())
# Start the agent (establishes connection)
await agent.start(outgoing_queue)
# Stop the agent when done
await agent.stop()
# %%
# Starting Realtime Conversation
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Now we can set up a realtime conversation between a user and a realtime agent.
#
# Here we take FastAPI as an example backend framework to demonstrate how to set up
# a realtime conversation.
#
# **Backend Setup (Server-side):**
#
# The backend needs to:
#
# 1. Create a WebSocket endpoint to accept frontend connections
# 2. Create a ``RealtimeAgent`` when the session starts
# 3. Forward ``ClientEvents`` from frontend to the agent
# 4. Forward ``ServerEvents`` from agent to the frontend
#
# .. code-block:: python
#
# from fastapi import FastAPI, WebSocket
# from agentscope.agent import RealtimeAgent
# from agentscope.realtime import (
# DashScopeRealtimeModel,
# ClientEvents,
# ServerEvents,
# )
#
# app = FastAPI()
#
# @app.websocket("/ws/{user_id}/{session_id}")
# async def websocket_endpoint(
# websocket: WebSocket,
# user_id: str,
# session_id: str,
# ):
# await websocket.accept()
#
# # Create queue for agent messages
# frontend_queue = asyncio.Queue()
#
# # Create agent
# agent = RealtimeAgent(
# name="Assistant",
# sys_prompt="You are a helpful assistant.",
# model=DashScopeRealtimeModel(
# model_name="qwen3-omni-flash-realtime",
# api_key=os.getenv("DASHSCOPE_API_KEY"),
# ),
# )
#
# # Start agent
# await agent.start(frontend_queue)
#
# # Forward messages from agent to frontend
# async def send_to_frontend():
# while True:
# msg = await frontend_queue.get()
# await websocket.send_json(msg.model_dump())
#
# asyncio.create_task(send_to_frontend())
#
# # Receive messages from frontend and forward to agent
# while True:
# data = await websocket.receive_json()
# client_event = ClientEvents.from_json(data)
# await agent.handle_input(client_event)
#
# **Frontend Setup (Client-side):**
#
# The frontend needs to:
#
# 1. Establish WebSocket connection to the backend
# 2. Send ``CLIENT_SESSION_CREATE`` event to initialize the session
# 3. Capture audio from microphone and send via ``CLIENT_AUDIO_APPEND`` events
# 4. Receive and handle ``ServerEvents`` (e.g., play audio, display transcripts)
#
# .. code-block:: javascript
#
# // Connect to WebSocket
# const ws = new WebSocket('ws://localhost:8000/ws/user1/session1');
#
# ws.onopen = () => {
# // Create session
# ws.send(JSON.stringify({
# type: 'client_session_create',
# config: {
# instructions: 'You are a helpful assistant.',
# user_name: 'User1'
# }
# }));
# };
#
# // Handle messages from backend
# ws.onmessage = (event) => {
# const data = JSON.parse(event.data);
# if (data.type === 'response_audio_delta') {
# // Play audio chunk
# playAudio(data.delta);
# }
# };
#
# // Send audio data
# function sendAudioChunk(audioData) {
# ws.send(JSON.stringify({
# type: 'client_audio_append',
# session_id: 'session1',
# audio: audioData, // base64 encoded
# format: { encoding: 'pcm16', sample_rate: 16000 }
# }));
# }
#
# For a complete working example, see
# ``examples/agent/realtime_voice_agent/`` in the AgentScope repository.
# %%
# Multi-Agent Realtime Conversation
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# AgentScope supports multi-agent realtime interactions through the ``ChatRoom``
# class.
#
# Note currently most realtime model APIs only support single-user interactions,
# but AgentScope's architecture is designed to support multiple agents and users
# when API capabilities expand.
#
# The Realtime ChatRoom
# ----------------------------
#
# AgentScope introduces the ``ChatRoom`` class to manage multiple realtime
# agents in a shared conversation space. The ChatRoom provides:
#
# - Centralized management of multiple ``RealtimeAgent`` instances
# - Automatic message broadcasting between agents
# - Unified message queue for frontend communication
# - Lifecycle management for all agents in the room
#
# Using ChatRoom
# --------------
#
# The usage of ``ChatRoom`` is similar to ``RealtimeAgent``:
#
async def example_chat_room() -> None:
"""Example of using ChatRoom with multiple realtime agents."""
from agentscope.pipeline import ChatRoom
from agentscope.agent import RealtimeAgent
from agentscope.realtime import DashScopeRealtimeModel
# Create multiple agents
agent1 = RealtimeAgent(
name="Agent1",
sys_prompt="You are Agent1, a helpful assistant.",
model=DashScopeRealtimeModel(
model_name="qwen3-omni-flash-realtime",
api_key=os.getenv("DASHSCOPE_API_KEY"),
),
)
agent2 = RealtimeAgent(
name="Agent2",
sys_prompt="You are Agent2, a helpful assistant.",
model=DashScopeRealtimeModel(
model_name="qwen3-omni-flash-realtime",
api_key=os.getenv("DASHSCOPE_API_KEY"),
),
)
# Create a chat room with multiple agents
chat_room = ChatRoom(agents=[agent1, agent2])
# Create queue to receive messages from all agents
outgoing_queue = asyncio.Queue()
# Start the chat room
await chat_room.start(outgoing_queue)
# Handle input from frontend
# The chat room will broadcast to all agents
from agentscope.realtime import ClientEvents
client_event = ClientEvents.ClientTextAppendEvent(
session_id="session1",
text="Hello everyone!",
)
await chat_room.handle_input(client_event)
# Stop the chat room when done
await chat_room.stop()
# %%
# Roadmap
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# The realtime agent feature is currently experimental and under active
# development. The future plans include:
#
# - Support for more realtime model APIs
# - Enhanced memory management for conversation history
# - Comprehensive tool calling support across all providers
# - Multi-user voice interaction support
# - Improved VAD (Voice Activity Detection) configuration
# - Better error handling and recovery mechanisms
#
# We welcome contributions and feedback from the community to help shape the
# future of realtime agents in AgentScope!

View File

@@ -0,0 +1,216 @@
# -*- coding: utf-8 -*-
"""
.. _state:
State/Session Management
=================================
In AgentScope, the **"state"** refers to the agent status in the running application, including its current system prompt, memory, context, equipped tools, and other information that **change over time**.
To manage the state of an application, AgentScope designs an automatic state registration system and session-level state management, which features:
- Support **automatic state registration** for all variables inherited from ``StateModule``
- Support **manual state registration** with custom serialization/deserialization methods
- Support **session/application-level management**
"""
import asyncio
import json
import os
from agentscope.agent import ReActAgent
from agentscope.formatter import DashScopeChatFormatter
from agentscope.memory import InMemoryMemory
from agentscope.message import Msg
from agentscope.model import DashScopeChatModel
from agentscope.module import StateModule
from agentscope.session import JSONSession
from agentscope.tool import Toolkit
# %%
# State Module
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# The ``StateModule`` class is the foundation for state management in AgentScope and provides three basic functions:
#
# .. list-table:: Methods of ``StateModule``
# :header-rows: 1
#
# * - Method
# - Arguments
# - Description
# * - ``register_state``
# - | ``attr_name``,
# | ``custom_to_json`` (optional),
# | ``custom_from_json`` (optional)
# - Register an attribute as its state, with optional serialization/deserialization function.
# * - ``state_dict``
# - \-
# - Get the state dictionary of current object
# * - ``load_state_dict``
# - | ``state_dict``,
# | ``strict`` (optional)
# - Load the state dictionary to current object
#
# Within an object of ``StateModule``, all the following attributes will be treated as parts of its state:
#
# - the **attributes** that inherit from ``StateModule``
# - the **attributes** registered by the ``register_state`` method
#
# Note the ``StateModule`` supports **NESTED** serialization and deserialization:
#
class ClassA(StateModule):
def __init__(self) -> None:
super().__init__()
self.cnt = 123
# register cnt attribute as state
self.register_state("cnt")
class ClassB(StateModule):
def __init__(self) -> None:
super().__init__()
# attribute "a" inherits from StateModule
self.a = ClassA()
# register attribute "c" as state manually
self.c = "Hello, world!"
self.register_state("c")
obj_b = ClassB()
print("State of obj_b.a:")
print(obj_b.a.state_dict())
print("\nState of obj_b:")
print(json.dumps(obj_b.state_dict(), indent=4))
# %%
# We can observe the state of ``obj_b`` contains the state of its attribute ``a`` automatically.
#
# In AgentScope, the ``AgentBase``, ``MemoryBase``, ``LongTermMemoryBase`` and ``Toolkit`` classes all inherit from ``StateModule``, thus supporting automatic and nested state management.
#
# Creating an agent
agent = ReActAgent(
name="Friday",
sys_prompt="You're a assistant named Friday.",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
),
formatter=DashScopeChatFormatter(),
memory=InMemoryMemory(),
toolkit=Toolkit(),
)
initial_state = agent.state_dict()
print("Initial state of the agent:")
print(json.dumps(initial_state, indent=4))
# %%
# Then we change its state by generating a reply message:
#
async def example_agent_state() -> None:
"""Example of agent state management"""
await agent(Msg("user", "Hello, agent!", "user"))
print("State of the agent after generating a reply:")
print(json.dumps(agent.state_dict(), indent=4))
asyncio.run(example_agent_state())
# %%
# Now we recover the state of the agent to its initial state:
#
agent.load_state_dict(initial_state)
print("State after loading the initial state:")
print(json.dumps(agent.state_dict(), indent=4))
# %%
# Session Management
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# In AgentScope, a session refers to a collection of ``StateModule`` in an application, e.g. multiple agents.
#
# AgentScope provides a ``SessionBase`` class with two abstract methods for session
# management: ``save_session_state`` and ``load_session_state``.
# Developers can implement these methods with their own storage solution.
#
# In AgentScope, we provide a JSON based session class ``JSONSession`` that
# stores/loads the session state in/from a JSON file named with the session ID.
#
# Here we show how to use the JSON based session management in AgentScope.
#
# Saving Session State
# -----------------------------------------
#
# change the agent state by generating a reply message
asyncio.run(example_agent_state())
print("\nState of agent:")
print(json.dumps(agent.state_dict(), indent=4))
# %%
# Then we save it to a session file:
session = JSONSession(
save_dir="./", # The dir used to save the session files
)
async def example_session() -> None:
"""Example of session management."""
await session.save_session_state(
session_id="user_1", # Use the name as the session id
agent=agent,
)
print("The saved state:")
with open("./user_1.json", "r", encoding="utf-8") as f:
print(json.dumps(json.load(f), indent=4))
asyncio.run(example_session())
# %%
# Loading Session State
# -----------------------------------------
# Now we load the session state from the saved file:
async def example_load_session() -> None:
"""Example of loading session state."""
# we first clear the memory of the agent
await agent.memory.clear()
print("Current state of the agent:")
print(json.dumps(agent.state_dict(), indent=4))
# then we load the session state
await session.load_session_state(
session_id="user_1",
# The keyword argument must be the same as the one used in `save_session_state`
agent=agent,
)
print("After loading the session state:")
print(json.dumps(agent.state_dict(), indent=4))
asyncio.run(example_load_session())
# %%
# Now we can see the agent state is restored to the saved state.
#

View File

@@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
"""
.. _studio:
AgentScope Studio
=========================
AgentScope Studio is a local-deployed web application that
- provides **project management** for the development of agent applications
- provides native **visualization** for running applications and tracing
- provides a **built-in agent** named "Friday" that supports secondary development
.. note:: The Studio is under fast development, more features are coming soon!
.. figure:: ../../_static/images/studio_home.webp
:width: 100%
:alt: AgentScope Studio Home Page
:class: bordered-image
:align: center
AgentScope Studio Home Page
Quick Start
~~~~~~~~~~~~~~~~~~~~~~~~
AgentScope Studio is installed via ``npm``:
.. code-block:: bash
npm install -g @agentscope/studio
Start the Studio with the following command:
.. code-block:: bash
as_studio
To connect your application to the Studio, use the ``agentscope.init`` function with the ``studio_url`` parameter:
.. code-block:: python
import agentscope
agentscope.init(studio_url="http://localhost:3000")
# your application code
...
Then, you can see your application in the Studio as follows:
.. figure:: ../../_static/images/studio_project.webp
:width: 100%
:alt: Project management
:class: bordered-image
:align: center
Project management in AgentScope Studio
The details about your running application, e.g. token usage, model invocations, and tracing information, can all be viewed in the Studio.
.. figure:: ../../_static/images/studio_run.webp
:width: 100%
:alt: AgentScope Studio run Page
:class: bordered-image
:align: center
Application visualization in AgentScope Studio
Friday Agent
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Friday is an experimental local-deployed agent built by AgentScope, aims at
- answering questions about the AgentScope,
- providing a quick secondary development environment for developers,
- integrating all available features in AgentScope to build a more powerful agent, and
- testing and integrating the advanced features in AgentScope.
.. note:: We highly greet contributions from the community to improve Friday! Feel free to open issues or pull requests on our `GitHub repository <https://github.com/agentscope-ai/agentscope>`_.
We are keeping improving Friday, and currently it integrates the following features in AgentScope:
.. list-table::
:header-rows: 1
* - Feature
- Status
- Further Reading
- Description
* - Meta tool
- ✅
- :ref:`tool`
- Group-wise tool management, and allow agent to change equipped tools by itself.
* - Agent Hook
- ✅
- :ref:`hook`
- Use hook to forward the printing messages to the frontend.
* - Agent Interruption
- ✅
- :ref:`agent`
- Allow use to interrupt the agent's reply process with post-processing.
* - Truncated Prompt
- ✅
- :ref:`prompt`
- Support to truncate the prompt with the preset max token limit.
* - State & Session Management
- ✅
- :ref:`state`
- Auto state management and session management for agents, maintaining the state between different runs.
* - Long-term Memory
- 🚧
- :ref:`memory`
- Support long-term memory management.
"""

View File

@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
"""
.. _token:
Token
=========================
AgentScope provides a token counter module under ``agentscope.token`` to
calculate the number of tokens in the given messages, allowing developers
to estimate the number of tokens in a prompt before sending it to an API.
Specifically, the following token counters are available:
.. list-table::
:header-rows: 1
* - Provider
- Class
- Support Image Data
- Support Tools
* - Anthropic
- ``AnthropicTokenCounter``
- ✅
- ✅
* - OpenAI
- ``OpenAITokenCounter``
- ✅
- ✅
* - Gemini
- ``GeminiTokenCounter``
- ✅
- ✅
* - HuggingFace
- ``HuggingFaceTokenCounter``
- Depends on the model
- Depends on the model
.. tip:: The formatter module has integrated the token counters to support prompt truncation. Refer to the :ref:`prompt` section for more details.
.. note:: For DashScope models, the dashscope library doesn't provide a token counting API. So we recommend using the HuggingFace token counter instead.
We show an example of using the OpenAI token counter to count the number of tokens:
"""
import asyncio
from agentscope.token import OpenAITokenCounter
async def example_token_counting():
# Example messages
messages = [
{"role": "user", "content": "Hello!"},
{"role": "assistant", "content": "Hi, how can I help you?"},
]
# OpenAI token counting
openai_counter = OpenAITokenCounter(model_name="gpt-4.1")
n_tokens = await openai_counter.count(messages)
print(f"Number of tokens: {n_tokens}")
asyncio.run(example_token_counting())
# %%
# Further Reading
# ------------------------------
#
# - :ref:`prompt`
#

View File

@@ -0,0 +1,453 @@
# -*- coding: utf-8 -*-
"""
.. _tool:
Tool
=========================
To ensure accurate and reliable tool parsing, AgentScope fully embraces the use of tools API with the following features:
- Support **automatic** tool parsing from Python functions with their docstrings
- Support both **synchronous and asynchronous** tool functions
- Support **streaming** tool responses (either synchronous or asynchronous generators)
- Support **dynamic extension** to the tool JSON Schema
- Support **interrupting** the tool execution with proper signal handling
- Support **autonomous tool management** by agents
All above features are implemented by the ``Toolkit`` class in AgentScope, which is responsible for managing tool functions and their execution.
.. tip:: The support of MCP (Model Context Protocol) refers to the :ref:`mcp` section.
"""
import asyncio
import inspect
import json
from typing import Any, AsyncGenerator
from pydantic import BaseModel, Field
import agentscope
from agentscope.message import TextBlock, ToolUseBlock
from agentscope.tool import ToolResponse, Toolkit, execute_python_code
# %%
# Tool Function
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# In AgentScope, a tool function is a Python function that
#
# - returns a ``ToolResponse`` object or a generator that yields ``ToolResponse`` objects
# - has a docstring that describes the tool's functionality and parameters
#
# A template of a tool function is as follows:
def tool_function(a: int, b: str) -> ToolResponse:
"""{function description}
Args:
a (int):
{description of the first parameter}
b (str):
{description of the second parameter}
"""
# %%
# .. tip:: Instance method and class method can also be used as tool functions, and the ``self`` and ``cls`` parameters will be ignored.
#
# AgentScope provides several built-in tool functions under the ``agentscope.tool`` module, such as ``execute_python_code``, ``execute_shell_command`` and text file write/read functions.
#
print("Built-in Tool Functions:")
for _ in agentscope.tool.__all__:
if _ not in ["Toolkit", "ToolResponse"]:
print(_)
# %%
# Toolkit
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# The ``Toolkit`` class is designed to manage tool functions, extracting their JSON Schema from docstrings and providing a unified interface for tool execution.
#
# Basic Usage
# ------------------------------
# The basic functionality of the ``Toolkit`` class is to register tool functions and execute them.
#
# Prepare a custom tool function
async def my_search(query: str, api_key: str) -> ToolResponse:
"""A simple example tool function.
Args:
query (str):
The search query.
api_key (str):
The API key for authentication.
"""
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Searching for '{query}' with API key '{api_key}'",
),
],
)
# Register the tool function in a toolkit
toolkit = Toolkit()
toolkit.register_tool_function(my_search)
# %%
# When registering a tool function, you can get its JSON Schema by calling the ``get_json_schemas`` method.
#
print("Tool JSON Schemas:")
print(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))
# %%
# ``Toolkit`` also allows developers to preset the arguments for tool functions, especially useful for API keys or other sensitive information.
#
# Clear the toolkit first
toolkit.clear()
# Register tool function with preset keyword arguments
toolkit.register_tool_function(my_search, preset_kwargs={"api_key": "xxx"})
print("Tool JSON Schemas with Preset Arguments:")
print(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))
# %%
# In ``Toolkit``, the ``call_tool_function`` method takes a tool use block as input and executes the corresponding tool function, returning **a unified asynchronous generator** that yields ``ToolResponse`` objects.
#
async def example_tool_execution() -> None:
"""Example of executing a tool call."""
res = await toolkit.call_tool_function(
ToolUseBlock(
type="tool_use",
id="123",
name="my_search",
input={"query": "AgentScope"},
),
)
# Only one tool response is expected in this case
print("Tool Response:")
async for tool_response in res:
print(tool_response)
asyncio.run(example_tool_execution())
# %%
# Extending JSON Schema Dynamically
# --------------------------------------
#
# Toolkit allows to extend the JSON schemas of tool functions dynamically by calling the ``set_extended_model`` method.
# Such feature allows to add more parameters to the tool function without modifying its original definition.
#
# .. tip:: Related scenarios include dynamic :ref:`structured-output` and CoT (Chain of Thought) reasoning
#
# .. note:: The function to be extended should accept variable keyword arguments (``**kwargs``), so that the additional fields can be passed to it.
#
# Taking the CoT reasoning as an example, we can extend all tool functions with a ``thinking`` field, allowing the agent to summarize the current state and then decide what to do next.
#
# Example tool function
def tool_function(**kwargs: Any) -> ToolResponse:
"""A tool function"""
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Received parameters: {kwargs}",
),
],
)
# Add a thinking field so that the agent could think before giving the other parameters.
class ThinkingModel(BaseModel):
"""A Pydantic model for additional fields."""
thinking: str = Field(
description="Summarize the current state and decide what to do next.",
)
# Register
toolkit.set_extended_model("my_search", ThinkingModel)
print("The extended JSON Schema:")
print(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))
# %%
# Interrupting Tool Execution
# ------------------------------
# The ``Toolkit`` class supports **execution interruption** of **async tool functions** and provides a comprehensive **agent-oriented post-processing mechanism**.
# Such interruption is implemented based on the asyncio cancellation mechanism, and the post-processing varies depending on the return type of tool function.
#
# .. note:: For synchronous tool functions, their execution cannot be interrupted by asyncio cancellation. So the interruption is handled within the agent rather than the toolkit.
# Refer to the :ref:`agent` section for more information.
#
# Specifically, if the tool function returns a ``ToolResponse`` object, a predefined ``ToolResponse`` object with an interrupted message will be yielded.
# So that the agent can observe the interruption and handle it accordingly.
# Besides, a flag ``is_interrupted`` will be set to ``True`` in the response, and the external caller can decide whether to throw the ``CancelledError`` exception to the outer layer.
#
# An example of async tool function that can be interrupted is as follows:
#
async def non_streaming_function() -> ToolResponse:
"""A non-streaming tool function that can be interrupted."""
await asyncio.sleep(1) # Simulate a long-running task
# Fake interruption for demonstration
raise asyncio.CancelledError()
# The following code won't be executed due to the cancellation
return ToolResponse(
content=[
TextBlock(
type="text",
text="Run successfully!",
),
],
)
async def example_tool_interruption() -> None:
"""Example of tool interruption."""
toolkit = Toolkit()
toolkit.register_tool_function(non_streaming_function)
res = await toolkit.call_tool_function(
ToolUseBlock(
type="tool_use",
id="123",
name="non_streaming_function",
input={},
),
)
async for tool_response in res:
print("Tool Response:")
print(tool_response)
print("The interrupted flag:")
print(tool_response.is_interrupted)
asyncio.run(example_tool_interruption())
# %%
# For streaming tool functions, which returns an asynchronous generator, the ``Toolkit`` will attach the interrupted message to the previous chunk of the response.
# By this way, the agent can observe what the tool has returned before the interruption.
#
# The example of interrupting a streaming tool function is as follows:
#
async def streaming_function() -> AsyncGenerator[ToolResponse, None]:
"""A streaming tool function that can be interrupted."""
# Simulate a chunk of response
yield ToolResponse(
content=[
TextBlock(
type="text",
text="1234",
),
],
stream=True,
)
# Simulate interruption
raise asyncio.CancelledError()
# The following code won't be executed due to the cancellation
yield ToolResponse(
content=[
TextBlock(
type="text",
text="123456789",
),
],
)
async def example_streaming_tool_interruption() -> None:
"""Example of streaming tool interruption."""
toolkit = Toolkit()
toolkit.register_tool_function(streaming_function)
res = await toolkit.call_tool_function(
ToolUseBlock(
type="tool_use",
id="xxx",
name="streaming_function",
input={},
),
)
i = 0
async for tool_response in res:
print(f"Chunk {i}:")
print(tool_response)
print("The interrupted flag: ", tool_response.is_interrupted, "\n")
i += 1
asyncio.run(example_streaming_tool_interruption())
# %%
# Automatic Tool Management
# -------------------------------------
# .. image:: https://img.alicdn.com/imgextra/i3/O1CN013cvRpO27MfesMsTeh_!!6000000007783-2-tps-840-521.png
# :width: 100%
# :align: center
# :alt: Automatic Tool Management
#
#
# The ``Toolkit`` class supports **automatic tool management** by introducing the concept of **tool group**, as well as a **meta tool function** named ``reset_equipped_tools``.
#
# The tool group is a set of related tool functions, e.g. browser-use tools, map services tools, etc., which will be managed together.
# Only the tools in the activated groups will be visible to agents, i.e. accessible by the ``toolkit.get_json_schemas()`` method.
#
# Note there is a special group called ``basic``, which is always activated and the tools registered without specifying the group name will be added to this group by default.
#
# .. tip:: The ``basic`` group ensures that the basic usage of tools won't be affected by the group features if you don't need them.
#
# Now we try to create a tool group named ``browser_use``, which contains some web browsing tools.
#
def navigate(url: str) -> ToolResponse:
"""Navigate to a web page.
Args:
url (str):
The URL of the web page to navigate to.
"""
pass
def click_element(element_id: str) -> ToolResponse:
"""Click an element on the web page.
Args:
element_id (str):
The ID of the element to click.
"""
pass
toolkit = Toolkit()
# Create a tool group named browser_use
toolkit.create_tool_group(
group_name="browser_use",
description="The tool functions for web browsing.",
active=False,
# The notes when using these tools
notes="""1. Use ``navigate`` to open a web page.
2. When requiring user authentication, ask the user for the credentials
3. ...""",
)
toolkit.register_tool_function(navigate, group_name="browser_use")
toolkit.register_tool_function(click_element, group_name="browser_use")
# We can also register some basic tools
toolkit.register_tool_function(execute_python_code)
# %%
# If we check the tools JSON schema, we can only see the ``execute_python_code`` tool, because the ``browser_use`` group is not activated yet:
print("Tool JSON Schemas with Group:")
print(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))
# %%
# Use the ``update_tool_groups`` method to activate or deactivate tool groups:
toolkit.update_tool_groups(group_names=["browser_use"], active=True)
print("Tool JSON Schemas with Group:")
print(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))
# %%
# Additionally, ``Toolkit`` provides a meta tool function named ``reset_equipped_tools``, taking the current group names as the argument to indicate which groups to activate:
#
# .. note:: In ``ReActAgent`` class, you can enable the meta tool function by setting ``enable_meta_tool=True`` in the constructor.
#
# Register the meta tool function
toolkit.register_tool_function(toolkit.reset_equipped_tools)
reset_equipped = next(
tool
for tool in toolkit.get_json_schemas()
if tool["function"]["name"] == "reset_equipped_tools"
)
print("JSON schema of the ``reset_equipped_tools`` function:")
print(
json.dumps(
reset_equipped,
indent=4,
ensure_ascii=False,
),
)
# %%
# When agent calls the ``reset_equipped_tools`` function, the corresponding tool groups will be activated, and the tool response will
# contain the notes of the activated tool groups.
#
async def mock_agent_reset_tools() -> None:
"""Mock agent to reset tool groups."""
# Call the meta tool function
res = await toolkit.call_tool_function(
ToolUseBlock(
type="tool_use",
id="154",
name="reset_equipped_tools",
input={
"browser_user": True,
},
),
)
async for tool_response in res:
print("Text content in tool Response:")
print(tool_response)
asyncio.run(mock_agent_reset_tools())
# %%
# The toolkit also provides a method to gather the notes of the activated tool groups, and you can assemble it into your agent's system prompt.
#
# .. tip:: The automatic tool management feature is already implemented in the ``ReActAgent`` class, refer to the :ref:`agent` section for more details.
#
# Create one more tool group
toolkit.create_tool_group(
group_name="map_service",
description="The google map service tools.",
active=True,
notes="""1. Use ``get_location`` to get the location of a place.
2. ...""",
)
print("The gathered notes of the activated tool groups:")
print(toolkit.get_activated_notes())
# %%
# Further Reading
# ---------------------
# - :ref:`agent`
# - :ref:`state`
# - :ref:`mcp`
#

View File

@@ -0,0 +1,221 @@
# -*- coding: utf-8 -*-
"""
.. _tracing:
Tracing
==============================
AgentScope implements OpenTelemetry-based tracing to monitor and debug the
execution of agent applications, which features
- Provide built-in tracing for LLM, tool, agent, formatter, etc.
- Support error and exception tracking
- Provide native tracing **visualization** in AgentScope Studio
- Support connecting to **third-party platforms** like Alibaba Cloud CloudMonitor, `Arize-Phoenix <https://github.com/Arize-ai/phoenix>`_, `Langfuse <https://langfuse.com/>`_, etc.
Setting Up
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. note:: Connecting to the :ref:`studio` or third-party tracing endpoint should be done at the beginning of your application by the ``agentscope.init`` function.
AgentScope Studio
---------------------------------------
.. figure:: ../../_static/images/studio_tracing.webp
:width: 100%
:alt: AgentScope Studio tracing Page
:class: bordered-image
:align: center
*Tracing in AgentScope Studio*
When connecting to AgentScope Studio, just provide ``studio_url`` parameter in ``agentscope.init`` function.
.. code-block:: python
import agentscope
agentscope.init(studio_url="http://xxx:port")
Third-party Platforms
---------------------------------------
To connect to third-party tracing platforms, set the ``tracing_url`` parameter in the ``agentscope.init`` function.
The ``tracing_url`` is the URL of your OpenTelemetry collector or any compatible backend that supports OTLP (OpenTelemetry Protocol).
.. code-block:: python
import agentscope
# Connect to OpenTelemetry-compatible backends
agentscope.init(tracing_url="https://your-tracing-backend:port/traces")
Taking Alibaba Cloud CloudMonitor, Arize-Phoenix, and Langfuse as examples:
**Alibaba Cloud CloudMonitor**: A fully-managed observability platform.
.. code-block:: python
:caption: Connect to Alibaba Cloud CloudMonitor
agentscope.init(tracing_url="https://tracing-cn-hangzhou.arms.aliyuncs.com/adapt_xxx/api/otlp/traces")
.. tip::
**Get your Endpoint:** In the `ARMS Console <https://arms.console.aliyun.com/>`_ under **Access Center** > **OpenTelemetry**,
select the **Public Endpoint** matching your deployment region. Customize your app name via the ``OTEL_SERVICE_NAME`` environment variable.
Alibaba Cloud CloudMonitor provides zero-code instrumentation through `LoongSuite <https://github.com/alibaba/loongsuite-python-agent>`_ agent.
Learn more in the `CloudMonitor Documentation <https://www.alibabacloud.com/help/en/cms/cloudmonitor-2-0/user-guide/model-application>`_.
**Arize-Phoenix**: You need to set the ``PHOENIX_API_KEY`` in your environment variables.
.. code-block:: python
:caption: Connect to Arize Phoenix
# Arize Phoenix Integration
import os
PHOENIX_API_KEY = os.environ.get("PHOENIX_API_KEY")
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"api_key={PHOENIX_API_KEY}"
agentscope.init(tracing_url="https://app.phoenix.arize.com/v1/traces")
**LangFuse**: You need to set the ``LANGFUSE_PUBLIC_KEY`` and
``LANGFUSE_SECRET_KEY`` in your environment variables. The authorization
header is constructed using these keys.
.. code-block:: python
:caption: Connect to LangFuse
import os, base64
LANGFUSE_PUBLIC_KEY = os.environ["LANGFUSE_PUBLIC_KEY"]
LANGFUSE_SECRET_KEY = os.environ["LANGFUSE_SECRET_KEY"]
LANGFUSE_AUTH_STRING = f"{LANGFUSE_PUBLIC_KEY}:{LANGFUSE_SECRET_KEY}"
LANGFUSE_AUTH = base64.b64encode(LANGFUSE_AUTH_STRING.encode("utf-8")).decode("ascii")
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"Authorization=Basic {LANGFUSE_AUTH}"
# EU data region
agentscope.init(tracing_url="https://cloud.langfuse.com/api/public/otel/v1/traces")
# US data region
# agentscope.init(tracing_url="https://us.cloud.langfuse.com/api/public/otel/v1/traces")
Customizing Tracing
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As stated above, the tracing in AgentScope is implemented based on OpenTelemetry.
That means your own tracing code implemented by OpenTelemetry sdk is compatible with
AgentScope natively.
Besides, AgentScope has built-in the following decorators to trace the corresponding modules:
- ``@trace_llm``: Trace the ``__call__`` function of classes inherit from ``ChatModelBase``
- ``@trace_reply``: Trace the ``reply`` function of classes inherit from ``AgentBase``
- ``@trace_format``: Trace the ``format`` function of classes inherit from ``FormatterBase``
- ``@trace``: Trace general functions
Tracing LLMs
----------------------------------------
The ``@trace_llm`` decorator is to trace the ``__call__`` function of ``ChatModelBase`` classes.
.. note:: Your LLM class must inherit from ``ChatModelBase``
.. code-block:: python
:caption: Tracing new ChatModel class
class ExampleChatModel(ChatModelBase):
\"\"\"An example Model\"\"\"
...
@trace_llm
async def __call__(
self,
*args: Any,
**kwargs: Any,
) -> AsyncGenerator[ChatResponse, None] | ChatResponse:
\"\"\"LLM call\"\"\"
...
Tracing Agent
----------------------------------------
The ``@trace_reply`` decorator is for agent implementations and tracing the `reply` function.
.. note:: Your agent class must inherit from ``AgentBase``
.. code-block:: python
:caption: Tracing new Agent class
class ExampleAgent(AgentBase):
\"\"\"An example agent class\"\"\"
@tracer_reply
async def reply(self, *args: Any, **kwargs: Any) -> Msg:
\"\"\"Reply to the message.\"\"\"
...
Tracing Formatter
----------------------------------------
The ``@trace_format`` decorator is for formatters implementations and tracing the `format` function.
.. note:: Your formatter class must inherit from ``FormatterBase``
.. code-block:: python
:caption: Tracing new Formatter class
class ExampleFormatter(FormatterBase):
\"\"\"A simple example formatter class\"\"\"
@trace_format
async def format(self, *args: Any, **kwargs: Any) -> list[dict]:
\"\"\"Example formatting\"\"\"
General Tracing
----------------------------------------
The ``@trace`` decorator is different from the above decorators, as it is a general-purpose tracing decorator that can be applied to any function.
It requires a `name` parameter to identify the traced function, and can trace various types of functions, including:
- synchronous functions
- synchronous generator functions
- asynchronous functions
- asynchronous generator functions
.. code-block:: python
:caption: General tracing example
# 1. Synchronous function
@trace(name='simple_function')
def simple_function(name: str, age: int) -> str:
\"\"\"A simple function with automatic tracing.\"\"\"
return f"Hello, {name}! You are {age} years old."
# 2. Synchronous generator function
@trace(name='number_generator')
def number_generator(n: int) -> Generator[int, None, None]:
\"\"\"Generate numbers from 0 to n-1.\"\"\"
for i in range(n):
yield i
# 3. Asynchronous function
@trace(name='async_function')
async def async_function(data: dict) -> dict:
\"\"\"Process data asynchronously.\"\"\"
return {"processed": data}
# 4. Asynchronous generator function
@trace(name='async_stream')
async def async_stream(n: int) -> AsyncGenerator[str, None]:
\"\"\"Generate stream of data asynchronously.\"\"\"
for i in range(n):
yield f"data_{i}"
"""

View File

@@ -0,0 +1,255 @@
# -*- coding: utf-8 -*-
"""
.. _tts:
TTS
====================
AgentScope provides a unified interface for Text-to-Speech (TTS) models across multiple API providers.
This tutorial demonstrates how to use TTS models in AgentScope.
AgentScope supports the following TTS APIs:
.. list-table:: Built-in TTS Models
:header-rows: 1
* - API
- Class
- Streaming Input
- Non-Streaming Input
- Streaming Output
- Non-Streaming Output
* - DashScope Realtime API
- ``DashScopeRealtimeTTSModel``
- ✅
- ✅
- ✅
- ✅
* - DashScope CosyVoice Realtime API
- ``DashScopeCosyVoiceRealtimeTTSModel``
- ✅
- ✅
- ✅
- ✅
* - DashScope API
- ``DashScopeTTSModel``
- ❌
- ✅
- ✅
- ✅
* - DashScope CosyVoice API
- ``DashScopeCosyVoiceTTSModel``
- ❌
- ✅
- ✅
- ✅
* - OpenAI API
- ``OpenAITTSModel``
- ❌
- ✅
- ✅
- ✅
* - Gemini API
- ``GeminiTTSModel``
- ❌
- ✅
- ✅
- ✅
.. note:: The streaming input and output in AgentScope TTS models are all accumulative.
**Choosing the Right Model:**
- **Use Non-Realtime TTS** when you have complete text ready (e.g., pre-written
responses, complete LLM outputs)
- **Use Realtime TTS** when text is generated progressively (e.g., streaming
LLM responses) for lower latency
"""
import asyncio
import os
from agentscope.agent import ReActAgent, UserAgent
from agentscope.formatter import DashScopeChatFormatter
from agentscope.message import Msg
from agentscope.model import DashScopeChatModel
from agentscope.tts import (
DashScopeRealtimeTTSModel,
DashScopeTTSModel,
)
# %%
# Non-Realtime TTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Non-realtime TTS models process complete text inputs and are the simplest
# to use. You can directly call their ``synthesize()`` method.
#
# Taking DashScope TTS model as an example:
async def example_non_realtime_tts() -> None:
"""A basic example of using non-realtime TTS models."""
# Example with DashScope TTS
tts_model = DashScopeTTSModel(
api_key=os.environ.get("DASHSCOPE_API_KEY", ""),
model_name="qwen3-tts-flash",
voice="Cherry",
stream=False, # Non-streaming output
)
msg = Msg(
name="assistant",
content="Hello, this is DashScope TTS.",
role="assistant",
)
# Directly synthesize without connecting
tts_response = await tts_model.synthesize(msg)
# tts_response.content contains an audio block with base64-encoded audio data
print(
"The length of audio data:",
len(tts_response.content["source"]["data"]),
)
asyncio.run(example_non_realtime_tts())
# %%
# **Streaming Output for Lower Latency:**
#
# When ``stream=True``, the model returns audio chunks progressively, allowing
# you to start playback before synthesis completes. This reduces perceived latency.
#
async def example_non_realtime_tts_streaming() -> None:
"""An example of using non-realtime TTS models with streaming output."""
# Example with DashScope TTS with streaming output
tts_model = DashScopeTTSModel(
api_key=os.environ.get("DASHSCOPE_API_KEY", ""),
model_name="qwen3-tts-flash",
voice="Cherry",
stream=True, # Enable streaming output
)
msg = Msg(
name="assistant",
content="Hello, this is DashScope TTS with streaming output.",
role="assistant",
)
# Synthesize and receive an async generator for streaming output
async for tts_response in await tts_model.synthesize(msg):
# Process each audio chunk as it arrives
print(
"Received audio chunk of length:",
len(tts_response.content["source"]["data"]),
)
asyncio.run(example_non_realtime_tts_streaming())
# %%
# Realtime TTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Realtime TTS models are designed for scenarios where text is generated
# incrementally, such as streaming LLM responses. This enables the lowest
# possible latency by starting audio synthesis before the complete text is ready.
#
# **Key Concepts:**
#
# - **Stateful Processing**: Realtime TTS maintains state for a single streaming
# session, identified by ``msg.id``. Only one streaming session can be active
# at a time.
# - **Two Methods**:
#
# - ``push(msg)``: Non-blocking method that submits text chunks and returns
# immediately. May return partial audio if available.
# - ``synthesize(msg)``: Blocking method that finalizes the session and returns
# all remaining audio. When ``stream=True``, it returns an async generator.
#
# .. code-block:: python
#
# async def example_realtime_tts_streaming():
# tts_model = DashScopeRealtimeTTSModel(
# api_key=os.environ.get("DASHSCOPE_API_KEY", ""),
# model_name="qwen3-tts-flash-realtime",
# voice="Cherry",
# stream=False,
# )
#
# # realtime tts model received accumulative text chunks
# res = await tts_model.push(msg_chunk_1) # non-blocking
# res = await tts_model.push(msg_chunk_2) # non-blocking
# ...
# res = await tts_model.synthesize(final_msg) # blocking, get all remaining audio
#
# When setting ``stream=True`` during initialization, the ``synthesize()`` method returns an async generator of ``TTSResponse`` objects, allowing you to process audio chunks as they arrive.
#
#
# Integrating with ReActAgent
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope agents can automatically synthesize their responses to speech
# when provided with a TTS model. This works seamlessly with both realtime
# and non-realtime TTS models.
#
# **How It Works:**
#
# 1. The agent generates a text response (potentially streamed from an LLM)
# 2. The TTS model synthesizes the text to audio automatically
# 3. The synthesized audio is attached to the ``speech`` field of the ``Msg`` object
# 4. The audio is played during the agent's ``self.print()`` method
#
async def example_agent_with_tts() -> None:
"""An example of using TTS with ReActAgent."""
# Create an agent with TTS enabled
agent = ReActAgent(
name="Assistant",
sys_prompt="You are a helpful assistant.",
model=DashScopeChatModel(
api_key=os.environ.get("DASHSCOPE_API_KEY", ""),
model_name="qwen-max",
stream=True,
),
formatter=DashScopeChatFormatter(),
# Enable TTS
tts_model=DashScopeRealtimeTTSModel(
api_key=os.getenv("DASHSCOPE_API_KEY"),
model_name="qwen3-tts-flash-realtime",
voice="Cherry",
),
)
user = UserAgent("User")
# Build a conversation just like normal
msg = None
while True:
msg = await agent(msg)
msg = await user(msg)
if msg.get_text_content() == "exit":
break
# %%
# Customizing TTS Model
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# You can create custom TTS implementations by inheriting from ``TTSModelBase``.
# The base class provides a flexible interface for both realtime and non-realtime
# TTS models.
# We use an attribute ``supports_streaming_input`` to indicate if the TTS model is realtime or not.
#
# For realtime TTS models, you need to implement the ``connect``, ``close``, ``push`` and ``synthesize`` methods to handle the lifecycle and streaming input.
#
# While for non-realtime TTS models, you only need to implement the ``synthesize`` method.
#
# Further Reading
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# - :ref:`agent` - Learn more about agents in AgentScope
# - :ref:`message` - Understand message format in AgentScope
# - API Reference: :class:`agentscope.tts.TTSModelBase`
#

View File

@@ -0,0 +1,247 @@
# -*- coding: utf-8 -*-
"""
.. _tuner:
Tuner
=================
AgentScope provides the ``tuner`` module for training agent applications using reinforcement learning (RL).
This tutorial will guide you through how to leverage the ``tuner`` module to improve agent performance on specific tasks, including:
- Introducing the core components of the ``tuner`` module
- Demonstrating the key code required for the tuning workflow
- Showing how to configure and run the tuning process
Main Components
~~~~~~~~~~~~~~~~~~~
The ``tuner`` module introduces three core components essential for RL-based agent training:
- **Task Dataset**: A collection of tasks for training and evaluating the agent.
- **Workflow Function**: Encapsulates the agent's logic to be tuned.
- **Judge Function**: Evaluates the agent's performance on tasks and provides reward signals for tuning.
In addition, ``tuner`` provides several configuration classes for customizing the tuning process, including:
- **TunerModelConfig**: Model configurations for tuning purposes.
- **AlgorithmConfig**: Specifies the RL algorithm (e.g., GRPO, PPO) and its parameters.
Implementation
~~~~~~~~~~~~~~~~~~~
This section demonstrates how to use ``tuner`` to train a simple math agent.
Task Dataset
--------------------
The task dataset contains tasks for training and evaluating your agent.
You dataset should follow the Huggingface `datasets <https://huggingface.co/docs/datasets/quickstart>`_ format, which can be loaded with ``datasets.load_dataset``. For example:
.. code-block:: text
my_dataset/
├── train.jsonl # training samples
└── test.jsonl # evaluation samples
Suppose your `train.jsonl` contains:
.. code-block:: json
{"question": "What is 2 + 2?", "answer": "4"}
{"question": "What is 4 + 4?", "answer": "8"}
Before starting tuning, you can verify that your dataset is loaded correctly with:
.. code-block:: python
from agentscope.tuner import DatasetConfig
dataset = DatasetConfig(path="my_dataset", split="train")
dataset.preview(n=2)
# Output the first two samples to verify correct loading
# [
# {
# "question": "What is 2 + 2?",
# "answer": "4"
# },
# {
# "question": "What is 4 + 4?",
# "answer": "8"
# }
# ]
Workflow Function
--------------------
The workflow function defines how the agent interacts with the environment and makes decisions. All workflow functions should follow the input/output signature defined in ``agentscope.tuner.WorkflowType``.
Below is an example workflow function using a ReAct agent to answer math questions:
"""
from typing import Dict, Optional
from agentscope.agent import ReActAgent
from agentscope.formatter import OpenAIChatFormatter
from agentscope.message import Msg
from agentscope.model import ChatModelBase
from agentscope.tuner import WorkflowOutput
async def example_workflow_function(
task: Dict,
model: ChatModelBase,
auxiliary_models: Optional[Dict[str, ChatModelBase]] = None,
) -> WorkflowOutput:
"""An example workflow function for tuning.
Args:
task (`Dict`): The task information.
model (`ChatModelBase`): The chat model used by the agent.
auxiliary_models (`Optional[Dict[str, ChatModelBase]]`): Additional
chat models, generally used to simulate the behavior of other
non-training agents in multi-agent scenarios.
Returns:
`WorkflowOutput`: The output generated by the workflow.
"""
agent = ReActAgent(
name="react_agent",
sys_prompt="You are a helpful math problem solving agent.",
model=model,
formatter=OpenAIChatFormatter(),
)
response = await agent.reply(
msg=Msg(
"user",
task["question"],
role="user",
), # extract question from task
)
return WorkflowOutput( # return the response
response=response,
)
# %%
# You can directly run this workflow function with a task dictionary and a ``DashScopeChatModel`` / ``OpenAIChatModel`` to test its correctness before formal training. For example:
import asyncio
import os
from agentscope.model import DashScopeChatModel
task = {"question": "What is 123 plus 456?", "answer": "579"}
model = DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
)
workflow_output = asyncio.run(example_workflow_function(task, model))
assert isinstance(
workflow_output.response,
Msg,
), "In this example, the response should be a Msg instance."
print("\nWorkflow response:", workflow_output.response.get_text_content())
# %%
#
# Judge Function
# --------------------
# The judge function evaluates the agent's performance on a given task and provides a reward signal for tuning.
# All judge functions should follow the input/output signature defined in ``agentscope.tuner.JudgeType``.
# Below is a simple judge function that compares the agent's response with the ground truth answer:
from typing import Any
from agentscope.tuner import JudgeOutput
async def example_judge_function(
task: Dict,
response: Any,
auxiliary_models: Optional[Dict[str, ChatModelBase]] = None,
) -> JudgeOutput:
"""A very simple judge function only for demonstration.
Args:
task (`Dict`): The task information.
response (`Any`): The response field from the WorkflowOutput.
auxiliary_models (`Optional[Dict[str, ChatModelBase]]`): Additional
chat models for LLM-as-a-Judge purpose.
Returns:
`JudgeOutput`: The reward assigned by the judge.
"""
ground_truth = task["answer"]
reward = 1.0 if ground_truth in response.get_text_content() else 0.0
return JudgeOutput(reward=reward)
judge_output = asyncio.run(
example_judge_function(
task,
workflow_output.response,
),
)
print(f"Judge reward: {judge_output.reward}")
# %%
# The judge function can also be locally tested in the same way as shown above before formal training to ensure its logic is correct.
#
# .. tip::
# You can leverage existing `MetricBase <https://github.com/agentscope-ai/agentscope/blob/main/src/agentscope/evaluate/_metric_base.py>`_ implementations in your judge function to compute more sophisticated metrics and combine them into a composite reward.
#
# Configuration and Running
# ~~~~~~~~~~~~~~~
# Finally, you can configure and run the tuning process using the ``tuner`` module.
# Before starting, ensure that `Trinity-RFT <https://github.com/agentscope-ai/Trinity-RFT>`_ is installed in your environment, as it is required for tuning.
#
# Below is an example of configuring and starting the tuning process:
#
# .. note::
# This example is for demonstration only. For a complete runnable example, see `Tune ReActAgent <https://github.com/agentscope-ai/agentscope/tree/main/examples/tuner/react_agent>`_
#
# .. code-block:: python
#
# from agentscope.tuner import tune, AlgorithmConfig, DatasetConfig, TunerModelConfig
# # your workflow / judge function here...
#
# if __name__ == "__main__":
# dataset = DatasetConfig(path="my_dataset", split="train")
# model = TunerModelConfig(model_path="Qwen/Qwen3-0.6B", max_model_len=16384)
# algorithm = AlgorithmConfig(
# algorithm_type="multi_step_grpo",
# group_size=8,
# batch_size=32,
# learning_rate=1e-6,
# )
# tune(
# workflow_func=example_workflow_function,
# judge_func=example_judge_function,
# model=model,
# train_dataset=dataset,
# algorithm=algorithm,
# )
#
# Here, ``DatasetConfig`` configures the training dataset, ``TunerModelConfig`` sets the parameters for the trainable model, and ``AlgorithmConfig`` specifies the reinforcement learning algorithm and its hyperparameters.
#
# .. tip::
# The ``tune`` function is based on `Trinity-RFT <https://github.com/agentscope-ai/Trinity-RFT>`_ and internally converts input parameters to a YAML configuration.
# Advanced users can skip the ``model``, ``train_dataset``, and ``algorithm`` arguments and instead provide a YAML config file path via the ``config_path`` argument.
# Using a configuration file is recommended for fine-grained control and to leverage advanced Trinity-RFT features. See the Trinity-RFT `Configuration Guide <https://agentscope-ai.github.io/Trinity-RFT/en/main/tutorial/trinity_configs.html>`_ for more options.
#
# Save the above code as ``main.py`` and run it with:
#
# .. code-block:: bash
#
# ray start --head
# python main.py
#
# Checkpoints and logs are automatically saved to the ``checkpoints/AgentScope`` directory under your workspace, with each run in a timestamped sub-directory. Tensorboard logs can be found in ``monitor/tensorboard`` within the checkpoint directory.
#
# .. code-block:: text
#
# your_workspace/
# └── checkpoints/
# └──AgentScope/
# └── Experiment-20260104185355/ # each run saved in a sub-directory with timestamp
# ├── monitor/
# │ └── tensorboard/ # tensorboard logs
# └── global_step_x/ # saved model checkpoints at step x
#
# .. tip::
# For more tuning examples, refer to the `tuner directory <https://github.com/agentscope-ai/agentscope-samples/tree/main/tuner>`_ of the AgentScope-Samples repository.

View File

@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
"""
Concurrent Agents
===================================
With the help of asynchronous programming, the concurrent agents can be executed by ``asyncio.gather`` in Python.
A simple example is shown below, where two agents are created and executed concurrently.
"""
import asyncio
from datetime import datetime
from typing import Any
from agentscope.agent import AgentBase
class ExampleAgent(AgentBase):
"""The example agent for concurrent execution."""
def __init__(self, name: str) -> None:
"""Initialize the agent with its name."""
super().__init__()
self.name = name
async def reply(self, *args: Any, **kwargs: Any) -> None:
"""Reply to the message."""
start_time = datetime.now().strftime("%H:%M:%S.%f")[:-3]
print(f"{self.name} started at {start_time}")
await asyncio.sleep(3) # Simulate a long-running task
end_time = datetime.now().strftime("%H:%M:%S.%f")[:-3]
print(f"{self.name} finished at {end_time}")
async def run_concurrent_agents() -> None:
"""Run the concurrent agents."""
agent1 = ExampleAgent("Agent 1")
agent2 = ExampleAgent("Agent 2")
await asyncio.gather(agent1(), agent2())
asyncio.run(run_concurrent_agents())

View File

@@ -0,0 +1,208 @@
# -*- coding: utf-8 -*-
"""
.. _conversation:
Conversation
======================
Conversation is a design pattern that agents exchange and share information
between each other, most commonly in game playing, chatbot, and multi-agent
discussion scenarios.
In AgentScope, the conversation is built upon the **explicit message
exchange**. In this tutorial, we will demonstrate how to build a conversation
- between a user and an agent (chatbot)
- between multiple agents (game playing, discussion, etc.)
Their main difference lies in
- how the **prompt is constructed**, and
- how the information is **propagated/shared** among agents.
"""
import asyncio
import json
import os
from agentscope.agent import ReActAgent, UserAgent
from agentscope.memory import InMemoryMemory
from agentscope.formatter import (
DashScopeChatFormatter,
DashScopeMultiAgentFormatter,
)
from agentscope.model import DashScopeChatModel
from agentscope.message import Msg
from agentscope.pipeline import MsgHub
from agentscope.tool import Toolkit
# %%
# User-Agent Conversation
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# User-agent conversation, also known as chatbot, is the most common usage
# scenario of LLM-empowered agents, and the design target of most LLM APIs.
# Such conversation features only two participants: a user and an agent.
#
# In AgentScope, the formatters with **"Chat"** in its name are designed for
# user-agent conversation, such as ``DashScopeChatFormatter``,
# ``AnthropicChatFormatter``, etc.
# They use the ``role`` field in the message to distinguish the user and the
# agent, and format the messages accordingly.
#
# Here we build a simple conversation between agent ``Friday`` and user.
#
# .. tip:: AgentScope provides a built-in ``UserAgent`` class for human-in-the-loop (HITL) interaction. Refer to :ref:`user-agent` for more details.
#
friday = ReActAgent(
name="Friday",
sys_prompt="You're a helpful assistant named Friday",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
),
formatter=DashScopeChatFormatter(), # The formatter for user-agent conversation
memory=InMemoryMemory(),
toolkit=Toolkit(),
)
# Create a user agent
user = UserAgent(name="User")
# %%
# Now, we can program the conversation by exchanging messages between these two agents until the user types "exit" to end the conversation.
#
# .. code-block:: python
#
# async def run_conversation() -> None:
# """Run a simple conversation between Friday and User."""
# msg = None
# while True:
# msg = await friday(msg)
# msg = await user(msg)
# if msg.get_text_content() == "exit":
# break
#
# asyncio.run(run_conversation())
#
# %%
# More than Two Agents
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# As stated in the beginning, we demonstrate how to build conversation with multiple agents in terms of **prompt construction** and **information sharing**.
#
# Prompt Construction
# -------------------------------
# In AgentScope, we provide built-in formatters for multi-agent conversation, featuring **"MultiAgent"** in their names, such as ``DashScopeMultiAgentFormatter``, ``AnthropicMultiAgentFormatter``, etc.
#
# Specifically, they use the ``name`` field in the message to distinguish different agents, and format the conversation history into a single user message.
# Taking ``DashScopeMultiAgentFormatter`` as an example:
#
# .. tip:: More details about the formatter can be found in :ref:`prompt`.
#
async def example_multi_agent_prompt() -> None:
msgs = [
Msg("system", "You're a helpful assistant named Bob.", "system"),
Msg("Alice", "Hi!", "user"),
Msg("Bob", "Hi! Nice to meet you guys.", "assistant"),
Msg("Charlie", "Me too! I'm Charlie, by the way.", "assistant"),
]
formatter = DashScopeMultiAgentFormatter()
prompt = await formatter.format(msgs)
print("Formatted prompt:")
print(json.dumps(prompt, indent=4, ensure_ascii=False))
# We print the content of the combined user message here for better
# understanding:
print("-------------")
print("Combined message")
print(prompt[1]["content"])
asyncio.run(example_multi_agent_prompt())
# %%
# Message Sharing
# -------------------------------
# In multi-agent conversation, exchanging messages explicitly may not be efficient and convenient, especially when broadcasting messages among multiple agents.
#
# Therefore, AgentScope provides an async context manager named ``MsgHub`` to simplify the operation of broadcasting messages.
# Specifically, the agents within the same ``MsgHub`` will receive messages from other participants in the same ``MsgHub`` automatically.
#
model = DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
)
formatter = DashScopeMultiAgentFormatter()
alice = ReActAgent(
name="Alice",
sys_prompt="You're a student named Alice.",
model=model,
formatter=formatter,
toolkit=Toolkit(),
memory=InMemoryMemory(),
)
bob = ReActAgent(
name="Bob",
sys_prompt="You're a student named Bob.",
model=model,
formatter=formatter,
toolkit=Toolkit(),
memory=InMemoryMemory(),
)
charlie = ReActAgent(
name="Charlie",
sys_prompt="You're a student named Charlie.",
model=model,
formatter=formatter,
toolkit=Toolkit(),
memory=InMemoryMemory(),
)
async def example_msghub() -> None:
"""Example of using MsgHub for multi-agent conversation."""
async with MsgHub(
[alice, bob, charlie],
announcement=Msg(
"system",
"Now you meet each other with a brief self-introduction.",
"system",
),
):
await alice()
await bob()
await charlie()
asyncio.run(example_msghub())
# %%
# Now we print the memory of Alice to check if her memory is updated correctly.
#
async def example_memory() -> None:
"""Print the memory of Alice."""
print("Memory of Alice:")
for msg in await alice.memory.get_memory():
print(f"{msg.name}: {msg.get_text_content()}")
asyncio.run(example_memory())
# %%
# Further Reading
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# - :ref:`prompt`
# - :ref:`pipeline`
#

View File

@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
"""
Handoffs
========================================
.. figure:: ../../_static/images/handoffs.png
:width: 80%
:align: center
:alt: Orchestrator-Workers Workflow
*Handoffs example*
It's very simple to implement the Orchestrator-Workers workflow with tool calls in AgentScope.
First, we create a function to allow the orchestrator to create workers dynamically.
"""
import asyncio
import os
from agentscope.agent import ReActAgent
from agentscope.formatter import DashScopeChatFormatter
from agentscope.memory import InMemoryMemory
from agentscope.message import Msg
from agentscope.model import DashScopeChatModel
from agentscope.tool import (
ToolResponse,
Toolkit,
execute_python_code,
)
# The tool function to create a worker
async def create_worker(
task_description: str,
) -> ToolResponse:
"""Create a worker to finish the given task. The worker is equipped with python execution tool.
Args:
task_description (``str``):
The description of the task to be finished by the worker.
"""
# Equip the worker agent with some tools
toolkit = Toolkit()
toolkit.register_tool_function(execute_python_code)
# Create a worker agent
worker = ReActAgent(
name="Worker",
sys_prompt="You're a worker agent. Your target is to finish the given task.",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
stream=False,
),
formatter=DashScopeChatFormatter(),
toolkit=toolkit,
)
# Let the worker finish the task
res = await worker(Msg("user", task_description, "user"))
return ToolResponse(
content=res.get_content_blocks("text"),
)
async def run_handoffs() -> None:
"""Example of handoffs workflow."""
# Initialize the orchestrator agent
toolkit = Toolkit()
toolkit.register_tool_function(create_worker)
orchestrator = ReActAgent(
name="Orchestrator",
sys_prompt="You're an orchestrator agent. Your target is to finish the given task by decomposing it into smaller tasks and creating workers to finish them.",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
stream=False,
),
memory=InMemoryMemory(),
formatter=DashScopeChatFormatter(),
toolkit=toolkit,
)
# The task description
task_description = "Execute hello world in Python"
# Create a worker to finish the task
await orchestrator(Msg("user", task_description, "user"))
asyncio.run(run_handoffs())

View File

@@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
"""
.. _multiagent-debate:
Multi-Agent Debate
========================
Debate workflow simulates a multi-turn discussion between different agents, mostly several solvers and an aggregator.
Typically, the solvers generate and exchange their answers, while the aggregator collects and summarizes the answers.
We implement the examples in `EMNLP 2024`_, where two debater agents will discuss a topic in a fixed order, and express their
arguments based on the previous debate history.
At each round a moderator agent will decide whether the correct answer can be obtained in the current iteration.
"""
import asyncio
import os
from pydantic import Field, BaseModel
from agentscope.agent import ReActAgent
from agentscope.formatter import (
DashScopeMultiAgentFormatter,
DashScopeChatFormatter,
)
from agentscope.message import Msg
from agentscope.model import DashScopeChatModel
from agentscope.pipeline import MsgHub
# Prepare a topic
topic = (
"The two circles are externally tangent and there is no relative sliding. "
"The radius of circle A is 1/3 the radius of circle B. Circle A rolls "
"around circle B one trip back to its starting point. How many times will "
"circle A revolve in total?"
)
# Create two debater agents, Alice and Bob, who will discuss the topic.
def create_solver_agent(name: str) -> ReActAgent:
"""Get a solver agent."""
return ReActAgent(
name=name,
sys_prompt=f"You're a debater named {name}. Hello and welcome to the "
"debate competition. It's unnecessary to fully agree with "
"each other's perspectives, as our objective is to find "
"the correct answer. The debate topic is stated as "
f"follows: {topic}.",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
stream=False,
),
formatter=DashScopeMultiAgentFormatter(),
)
alice, bob = [create_solver_agent(name) for name in ["Alice", "Bob"]]
# Create a moderator agent
moderator = ReActAgent(
name="Aggregator",
sys_prompt=f"""You're a moderator. There will be two debaters involved in a debate competition. They will present their answer and discuss their perspectives on the topic:
``````
{topic}
``````
At the end of each round, you will evaluate both sides' answers and decide which one is correct.""",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
stream=False,
),
# Use multiagent formatter because the moderator will receive messages from more than a user and an assistant
formatter=DashScopeMultiAgentFormatter(),
)
# A structured output model for the moderator
class JudgeModel(BaseModel):
"""The structured output model for the moderator."""
finished: bool = Field(
description="Whether the debate is finished.",
)
correct_answer: str | None = Field(
description="The correct answer to the debate topic, only if the debate is finished. Otherwise, leave it as None.",
default=None,
)
async def run_multiagent_debate() -> None:
"""Run the multi-agent debate workflow."""
while True:
# The reply messages in MsgHub from the participants will be broadcasted to all participants.
async with MsgHub(participants=[alice, bob, moderator]):
await alice(
Msg(
"user",
"You are affirmative side, Please express your viewpoints.",
"user",
),
)
await bob(
Msg(
"user",
"You are negative side. You disagree with the affirmative side. Provide your reason and answer.",
"user",
),
)
# Alice and Bob doesn't need to know the moderator's message, so moderator is called outside the MsgHub.
msg_judge = await moderator(
Msg(
"user",
"Now you have heard the answers from the others, have the debate finished, and can you get the correct answer?",
"user",
),
structured_model=JudgeModel,
)
if msg_judge.metadata.get("finished"):
print(
"\nThe debate is finished, and the correct answer is: ",
msg_judge.metadata.get("correct_answer"),
)
break
asyncio.run(run_multiagent_debate())
# %%
# Further Reading
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# - :ref:`pipeline`
#
# .. _EMNLP 2024:
# Encouraging Divergent Thinking in Large Language Models through Multi-Agent Debate. EMNLP 2024.
#

View File

@@ -0,0 +1,176 @@
# -*- coding: utf-8 -*-
"""
.. _routing:
Routing
==========================
There are two ways to implement routing in AgentScope, both simple and easy to implement:
- Routing by structured output
- Routing by tool calls
.. tip:: Considering there is no unified standard/definition for agent routing, we follow the setting in `Building effective agents <https://www.anthropic.com/engineering/building-effective-agents>`_
Routing by Structured Output
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
By this way, we can directly use the structured output of the agent to determine which agent to route the message to.
Initialize a routing agent
"""
import asyncio
import json
import os
from typing import Literal
from pydantic import BaseModel, Field
from agentscope.agent import ReActAgent
from agentscope.formatter import DashScopeChatFormatter
from agentscope.memory import InMemoryMemory
from agentscope.message import Msg
from agentscope.model import DashScopeChatModel
from agentscope.tool import Toolkit, ToolResponse
router = ReActAgent(
name="Router",
sys_prompt="You're a routing agent. Your target is to route the user query to the right follow-up task.",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
stream=False,
),
formatter=DashScopeChatFormatter(),
)
# Use structured output to specify the routing task
class RoutingChoice(BaseModel):
your_choice: Literal[
"Content Generation",
"Programming",
"Information Retrieval",
None,
] = Field(
description="Choose the right follow-up task, and choose ``None`` if the task is too simple or no suitable task",
)
task_description: str | None = Field(
description="The task description",
default=None,
)
async def example_router_explicit() -> None:
"""Example of explicit routing with structured output."""
msg_user = Msg(
"user",
"Help me to write a poem",
"user",
)
# Route the query
msg_res = await router(
msg_user,
structured_model=RoutingChoice,
)
# The structured output is stored in the metadata field
print("The structured output:")
print(json.dumps(msg_res.metadata, indent=4, ensure_ascii=False))
asyncio.run(example_router_explicit())
# %%
# Routing by Tool Calls
# ~~~~~~~~~~~~~~~~~~~~~~~~~
# Another way is to wrap the downstream agents into a tool function, so that the routing agent decides which tool to call based on the user query.
#
# We first define several tool functions:
#
async def generate_python(demand: str) -> ToolResponse:
"""Generate Python code based on the demand.
Args:
demand (``str``):
The demand for the Python code.
"""
# An example demand agent
python_agent = ReActAgent(
name="PythonAgent",
sys_prompt="You're a Python expert, your target is to generate Python code based on the demand.",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
stream=False,
),
memory=InMemoryMemory(),
formatter=DashScopeChatFormatter(),
toolkit=Toolkit(),
)
msg_res = await python_agent(Msg("user", demand, "user"))
return ToolResponse(
content=msg_res.get_content_blocks("text"),
)
# Fake some other tool functions for demonstration purposes
async def generate_poem(demand: str) -> ToolResponse:
"""Generate a poem based on the demand.
Args:
demand (``str``):
The demand for the poem.
"""
pass
async def web_search(query: str) -> ToolResponse:
"""Search the web for the query.
Args:
query (``str``):
The query to search.
"""
pass
# %%
# After that, we define a routing agent and equip it with the above tool functions.
#
toolkit = Toolkit()
toolkit.register_tool_function(generate_python)
toolkit.register_tool_function(generate_poem)
toolkit.register_tool_function(web_search)
# Initialize the routing agent with the toolkit
router_implicit = ReActAgent(
name="Router",
sys_prompt="You're a routing agent. Your target is to route the user query to the right follow-up task.",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
stream=False,
),
formatter=DashScopeChatFormatter(),
toolkit=toolkit,
memory=InMemoryMemory(),
)
async def example_router_implicit() -> None:
"""Example of implicit routing with tool calls."""
msg_user = Msg(
"user",
"Help me to generate a quick sort function in Python",
"user",
)
# Route the query
await router_implicit(msg_user)
asyncio.run(example_router_implicit())

View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -e
# Clean old build files
rm -rf build/ doctrees/
# Build the html
sphinx-build -M html ./ build
# Remove temporary files (double insurance)
rm -rf build/html/.doctrees
rm -f build/html/.buildinfo
find build/html -name "*.pickle" -delete
find build/html -name "__pycache__" -delete
find build/html -name "*.pyc" -delete
echo "✅ Chinese docs built successfully, temporary files cleaned"

140
docs/tutorial/zh_CN/conf.py Normal file
View File

@@ -0,0 +1,140 @@
# -*- coding: utf-8 -*-
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "AgentScope"
copyright = "2025, Alibaba"
author = "Alibaba Tongyi Lab"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
"myst_parser",
"sphinx_gallery.gen_gallery",
"sphinx.ext.autodoc",
"sphinx.ext.viewcode",
"sphinx.ext.napoleon",
]
myst_enable_extensions = [
"colon_fence",
]
sphinx_gallery_conf = {
"download_all_examples": False,
"examples_dirs": [
"src",
],
"gallery_dirs": [
"tutorial",
],
"filename_pattern": "src/.*\.py",
"example_extensions": [".py"],
}
templates_path = ["../_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
languages = ["en", "zh_CN"]
language = "zh_CN"
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "furo"
html_title = (
"<span style='font-weight: 700; color: #2196f3;'>AgentScope</span>"
)
html_logo = "../_static/images/logo.svg"
html_favicon = "../_static/images/logo.svg"
html_static_path = ["../_static"]
html_css_files = [
"css/gallery.css",
]
html_js_files = [
"language_switch.js",
]
html_theme_options = {
"footer_icons": [
{
"name": "GitHub",
"url": "https://github.com/agentscope-ai/agentscope",
"html": """
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
</svg>
""",
"class": "",
},
{
"name": "Discord",
"url": "https://discord.gg/eYMpfnkG8h",
"html": """
<svg stroke="currentColor" fill="currentColor" stroke-width="0" t="1753331148815" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5721" width="200" height="200">
<path d="M723.903423 359.138018c-69.65045-52.952793-136.256577-51.476757-136.256576-51.476757l-6.088649 7.564685c83.027027 25.738378 121.127207 62.085766 121.127207 62.085766a387.459459 387.459459 0 0 0-145.297297-46.956397 418.179459 418.179459 0 0 0-98.340901 1.752793 73.801802 73.801802 0 0 1-7.564684 1.476036 357.385225 357.385225 0 0 0-110.702703 30.258739 278.786306 278.786306 0 0 0-28.782703 13.653333S353.049369 339.488288 440.873514 313.657658l-4.612613-6.088649s-66.513874-1.476036-136.164324 51.476757A654.252973 654.252973 0 0 0 230.630631 642.167928s40.867748 71.126486 148.341621 73.801802c0 0 16.697658-22.694054 31.827027-40.867748-62.085766-18.45045-84.77982-57.565405-84.77982-57.565405a130.998198 130.998198 0 0 0 13.653334 7.564684s0 1.568288 1.476036 1.568289c1.476036 1.476036 3.044324 1.476036 4.52036 3.044324a238.748829 238.748829 0 0 0 34.779099 16.605405 513.199279 513.199279 0 0 0 71.218739 21.218018 350.558559 350.558559 0 0 0 125.555315 0 329.894054 329.894054 0 0 0 69.650451-21.218018A247.328288 247.328288 0 0 0 702.685405 618.09009s-24.262342 39.391712-87.824144 57.565405c13.653333 18.45045 31.827027 39.299459 31.827027 39.29946 107.473874-2.952072 148.341622-73.801802 146.773334-72.602523a654.990991 654.990991 0 0 0-69.558199-283.214414zM421.131532 596.77982a54.705586 54.705586 0 0 1 0-109.042162 54.705586 54.705586 0 0 1 0 109.042162z m177.124324 0a54.705586 54.705586 0 1 1 49.908468-54.521081 52.491532 52.491532 0 0 1-49.908468 54.521081z" p-id="5722"></path><path d="M512 1024A512 512 0 1 1 1024 512 512.645766 512.645766 0 0 1 512 1024z m0-972.892252a461.261261 461.261261 0 1 0 461.261261 461.261261 461.261261 461.261261 0 0 0-461.261261-461.261261z" p-id="5723"></path>
</svg>
""",
"class": "",
},
{
"name": "DingTalk",
"url": "https://qr.dingtalk.com/action/joingroup?code=v1,k1,OmDlBXpjW+I2vWjKDsjvI9dhcXjGZi3bQiojOq3dlDw=&_dt_no_comment=1&origin=11",
"html": """
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
<path d="M512 0C229.205333 0 0 229.205333 0 512s229.205333 512 512 512 512-229.205333 512-512S794.794667 0 512 0z m237.312 480.810667c-1.109333 4.48-3.712 11.093333-7.424 18.986666h0.128l-0.426667 0.682667c-21.504 46.037333-77.610667 136.106667-77.610666 136.106667l-0.298667-0.597334-16.384 28.501334h79.018667l-150.912 200.917333 34.304-136.533333h-62.208l21.589333-90.282667c-17.493333 4.224-38.101333 10.026667-62.592 17.92 0 0-33.109333 19.370667-95.317333-37.333333 0 0-41.984-36.992-17.578667-46.165334 10.410667-3.925333 50.304-8.917333 81.706667-13.226666 42.410667-5.674667 68.48-8.789333 68.48-8.789334s-130.773333 2.005333-161.792-2.901333c-30.976-4.906667-70.4-56.704-78.805334-102.186667 0 0-12.970667-25.002667 27.904-13.226666 40.917333 11.818667 210.005333 46.08 210.005334 46.08S321.109333 411.434667 306.517333 394.922667c-14.634667-16.469333-43.093333-89.770667-39.424-134.869334 0 0 1.621333-11.221333 13.098667-8.192 0 0 162.602667 74.282667 273.792 114.986667 111.104 40.704 207.786667 61.397333 195.328 114.005333z" opacity=".65" p-id="6077"></path>
</svg>
""",
"class": "",
},
],
"light_css_variables": {
"color-brand-primary": "#2196f3",
"color-brand-content": "#2196f3",
"color-admonition-background": "#f8f9fa",
},
"dark_css_variables": {
"color-link": "#2196f3",
"color-link--hover": "#2196f3",
"color-brand-primary": "#64b5f6",
"color-brand-content": "#64b5f6",
},
}
source_suffix = [".md", ".rst"]
# -- Options for API documentation -------------------------------------------
autodoc_member_order = "bysource"
autodoc_typehints = "description"
autodoc_class_signature = "separated"
autodoc_default_options = {
"special-members": "__call__",
}
add_module_names = False
python_display_short_literal_types = True
def skip_member(app, what, name, obj, skip, options):
if name in [
"__call__",
"_format",
"_format_agent_message",
"_format_tool_sequence",
]:
return False
return skip
def setup(app):
app.connect("autodoc-skip-member", skip_member)

View File

@@ -0,0 +1,77 @@
.. AgentScope Doc documentation master file, created by
sphinx-quickstart on Thu Aug 8 15:07:21 2024.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to AgentScope's documentation!
==========================================
.. toctree::
:maxdepth: 1
:caption: Tutorial
tutorial/quickstart_installation
tutorial/quickstart_key_concept
tutorial/quickstart_message
tutorial/quickstart_agent
.. toctree::
:maxdepth: 1
:caption: Workflow
tutorial/workflow_conversation
tutorial/workflow_multiagent_debate
tutorial/workflow_concurrent_agents
tutorial/workflow_routing
tutorial/workflow_handoffs
.. toctree::
:maxdepth: 1
:caption: FAQ
tutorial/faq
.. toctree::
:maxdepth: 1
:caption: Model and Context
tutorial/task_model
tutorial/task_prompt
tutorial/task_token
tutorial/task_memory
tutorial/task_long_term_memory
.. toctree::
:maxdepth: 1
:caption: Tool
tutorial/task_tool
tutorial/task_mcp
tutorial/task_agent_skill
.. toctree::
:maxdepth: 1
:caption: Agent
tutorial/task_agent
tutorial/task_state
tutorial/task_hook
tutorial/task_middleware
tutorial/task_a2a
tutorial/task_realtime
.. toctree::
:maxdepth: 1
:caption: Features
tutorial/task_pipeline
tutorial/task_plan
tutorial/task_rag
tutorial/task_studio
tutorial/task_tracing
tutorial/task_eval
tutorial/task_eval_openjudge
tutorial/task_embedding
tutorial/task_tts
tutorial/task_tuner

View File

@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View File

View File

@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
"""
.. _faq:
常见问题
========================================
关于 AgentScope
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*什么是AgentScope*
AgentScope 是一个多智能体框架,旨在提供一种简单高效的方式来构建基于大语言模型的智能体应用程序。
*AgentScope v1.0 与 v0.x 版本有什么区别?*
AgentScope v1.0是对框架的完全重写,配备了新功能和改进。详细变更请参考相关文档。
关于模型
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*如何在 AgentScope 中集成我自己的模型?*
通过继承 ``agentscope.model.ChatModelBase`` 并实现 ``__call__`` 方法来创建您自己的模型。
*AgentScope 支持哪些模型?*
目前AgentScope 内置支持 DashScope、Gemini、OpenAI、Anthropic 和 Ollama API以及与 DeepSeek 和 vLLMs 模型兼容的 ``OpenAIChatModel``。
*如何在 AgentScope 中监控token 使用情况?*
在 AgentScope Studio中我们提供了 token 使用情况的可视化和追踪功能。详情请参考:ref:`studio` 部分。
关于智能体
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*如何创建我自己的智能体?*
您可以选择直接使用 ``ReActAgent`` 类,或者通过继承 ``AgentBase`` 或 ``ReActAgentBase`` 类来创建您自己的智能体。详情请参考 :ref:`agent` 部分。
*如何将智能体的(流式)输出转发到我自己的前端或应用程序?*
使用 ``print`` 函数的前置钩子来转发打印消息。请参考 :ref:`hook` 部分。
关于工具
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*AgentScope 提供了多少工具?*
AgentScope 提供了一套内置工具,包括 ``execute_python_code``、``execute_shell_command``、``write_text_file`` 等。您可以在 ``agentscope.tool`` 模块下找到它们。
关于错误报告
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*如何报告 AgentScope中的错误*
如果您在使用 AgentScope 时遇到错误,请通过在我们的 GitHub仓库中开启一个issue来报告。
*如何报告AgentScope 中的安全漏洞?*
如果您在AgentScope 中发现安全问题,请通过 `阿里巴巴安全响应中心(ASRC) <https://security.alibaba.com/>`_ 向我们报告。
"""

View File

@@ -0,0 +1,243 @@
# -*- coding: utf-8 -*-
"""
.. _react-agent:
创建 ReAct 智能体
====================
AgentScope 在 ``agentscope.agent`` 模块下提供了开箱即用的 ReAct 智能体 ``ReActAgent`` 供开发者使用。
它同时支持以下功能:
- ✨ 基础功能
- 支持围绕 ``reply``、``observe``、``print``、``_reasoning`` 和 ``_acting`` 的 **钩子函数hooks**
- 支持结构化输出
- ✋ 实时介入Realtime Steering
- 支持用户 **中断**
- 支持自定义 **中断处理**
- 🛠️ 工具
- 支持 **同步/异步** 工具函数
- 支持 **流式** 工具响应
- 支持 **状态化** 的工具管理
- 支持 **并行** 工具调用
- 支持 **MCP** 服务器
- 💾 记忆
- 支持智能体 **自主管理** 长期记忆
- 支持“静态”的长期记忆管理
.. tip:: 有关这些功能的更多详细信息,请参考 :ref:`agent` 部分。本章节中,我们重点介绍如何创建 ReAct 智能体并运行。
"""
from agentscope.agent import ReActAgent, AgentBase
from agentscope.formatter import DashScopeChatFormatter
from agentscope.memory import InMemoryMemory
from agentscope.message import Msg
from agentscope.model import DashScopeChatModel
import asyncio
import os
from agentscope.tool import Toolkit, execute_python_code
# %%
# 创建 ReAct 智能体
# ------------------------------
# 为了提高灵活性,``ReActAgent`` 类在其构造函数中暴露了以下参数:
#
# .. list-table:: ``ReActAgent`` 类的初始化参数
# :header-rows: 1
#
# * - 参数
# - 进一步阅读
# - 描述
# * - ``name`` (必需)
# -
# - 智能体的名称
# * - ``sys_prompt`` (必需)
# -
# - 智能体的系统提示
# * - ``model`` (必需)
# - :ref:`model`
# - 智能体用于生成响应的模型
# * - ``formatter`` (必需)
# - :ref:`prompt`
# - 提示构建策略,应与使用的模型保持一致
# * - ``toolkit``
# - :ref:`tool`
# - 用于注册/调用工具函数的工具模块
# * - ``memory``
# - :ref:`memory`
# - 用于存储对话历史的短期记忆
# * - ``long_term_memory``
# - :ref:`long-term-memory`
# - 长期记忆
# * - ``long_term_memory_mode``
# - :ref:`long-term-memory`
# - 长期记忆的管理模式:
#
# - ``agent_control``: 允许智能体通过工具函数自己控制长期记忆
# - ``static_control``: 在每次回复reply的开始/结束时,会自动从长期记忆中检索/记录信息
# - ``both``: 同时激活上述两种模式
# * - ``enable_meta_tool``
# - :ref:`tool`
# - 是否启用元工具Meta tool即允许智能体自主管理工具函数
# * - ``parallel_tool_calls``
# - :ref:`agent`
# - 是否允许并行工具调用
# * - ``max_iters``
# -
# - 智能体生成响应的最大迭代次数
# * - ``plan_notebook``
# - :ref:`plan`
# - 计划模块,允许智能体制定和管理计划与子任务
# * - ``print_hint_msg``
# -
# - 是否在终端打印 ``plan_notebook`` 生成的提示消息
#
# 以 DashScope API 为例,我们创建一个智能体对象如下:
async def creating_react_agent() -> None:
"""创建一个 ReAct 智能体并运行一个简单任务。"""
# 准备工具
toolkit = Toolkit()
toolkit.register_tool_function(execute_python_code)
jarvis = ReActAgent(
name="Jarvis",
sys_prompt="你是一个名为 Jarvis 的助手",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
stream=True,
enable_thinking=False,
),
formatter=DashScopeChatFormatter(),
toolkit=toolkit,
memory=InMemoryMemory(),
)
msg = Msg(
name="user",
content="你好Jarvis用 Python 运行 Hello World。",
role="user",
)
await jarvis(msg)
asyncio.run(creating_react_agent())
# %%
# 从零开始创建
# --------------------------------
# 为了支持开发者从零开始创建智能体AgentScope 提供了两个基类:
#
# .. list-table::
# :header-rows: 1
#
# * - 类
# - 抽象方法
# - 描述
# * - ``AgentBase``
# - | ``reply``
# | ``observe``
# | ``handle_interrupt``
# - - 所有智能体的基类,支持 ``reply``、``observe`` 和 ``print`` 函数的前置和后置钩子函数。
# - 在 ``__call__`` 函数内实现了基础的实时介入Realtime Steering逻辑。
# * - ``ReActAgentBase``
# - | ``reply``
# | ``observe``
# | ``handle_interrupt``
# | ``_reasoning``
# | ``_acting``
# - 在 ``AgentBase`` 的基础上添加了两个抽象函数 ``_reasoning`` 和 ``_acting``,以及它们的钩子函数。
#
# 有关智能体类的更多详细信息,请参考 :ref:`agent` 部分。
#
# 以 ``AgentBase`` 类为例,我们可以通过继承它并实现 ``reply`` 方法来创建自定义智能体类。
class MyAgent(AgentBase):
"""自定义智能体类"""
def __init__(self) -> None:
"""初始化智能体"""
super().__init__()
self.name = "Friday"
self.sys_prompt = "你是一个名为 Friday 的助手。"
self.model = DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
stream=False,
)
self.formatter = DashScopeChatFormatter()
self.memory = InMemoryMemory()
async def reply(self, msg: Msg | list[Msg] | None) -> Msg:
"""直接调用大模型,产生回复消息。"""
await self.memory.add(msg)
# 准备提示
prompt = await self.formatter.format(
[
Msg("system", self.sys_prompt, "system"),
*await self.memory.get_memory(),
],
)
# 调用模型
response = await self.model(prompt)
msg = Msg(
name=self.name,
content=response.content,
role="assistant",
)
# 在记忆中记录响应
await self.memory.add(msg)
# 打印消息
await self.print(msg)
return msg
async def observe(self, msg: Msg | list[Msg] | None) -> None:
"""观察消息。"""
# 将消息存储在记忆中
await self.memory.add(msg)
async def handle_interrupt(self) -> Msg:
"""处理中断。"""
# 以固定响应为例
return Msg(
name=self.name,
content="我注意到您打断了我的回复,我能为你做些什么?",
role="assistant",
)
async def run_custom_agent() -> None:
"""运行自定义智能体。"""
agent = MyAgent()
msg = Msg(
name="user",
content="你是谁?",
role="user",
)
await agent(msg)
asyncio.run(run_custom_agent())
# %%
#
# 进一步阅读
# ---------------------
# - :ref:`agent`
# - :ref:`model`
# - :ref:`prompt`
# - :ref:`tool`
#

View File

@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
"""
.. _installation:
安装
============================
AgentScope 需要 Python 3.10 或更高版本。您可以从源代码或 PyPI 安装。
从 PyPI 安装
----------------
.. code-block:: bash
pip install agentscope
从源代码安装
----------------
从源代码安装 AgentScope需要从 GitHub 克隆仓库,并通过以下命令安装
.. code-block:: bash
git clone -b main https://github.com/agentscope-ai/agentscope
cd agentscope
pip install -e .
执行以下代码确保 AgentScope 正常安装:
"""
import agentscope
print(agentscope.__version__)
# %%
# 额外依赖
# ----------------------------
#
# 为了满足不同功能的需求AgentScope 提供了额外依赖项。
#
# - full: 包含模型 API 和工具函数的额外依赖项
# - dev: 开发依赖项,包括测试和文档工具
#
# 以 full 模式为例,安装命令根据您的操作系统而有所不同。
#
# 对于 Windows 用户:
#
# .. code-block:: bash
#
# pip install agentscope[full]
#
# 对于 Mac 和 Linux 用户:
#
# .. code-block:: bash
#
# pip install agentscope\[full\]

View File

@@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
"""
.. key-concepts:
核心概念
====================================
本章从工程实践的角度介绍 AgentScope 中的核心概念,从而阐明 AgentScope 的设计理念。
.. note:: 介绍核心概念的目标是为了更好的阐明 AgentScope 在工程实践中解决的问题,以及为开发者提供的帮助,而非给出严谨的定义。
状态
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
状态State管理是 AgentScope 框架构建的基础。其中,状态表示对象运行时某一时刻数据的快照。
AgentScope 将对象的“初始化”与“状态管理”分离,对象在初始化后通过 ``load_state_dict`` 和 ``state_dict`` 方法恢复到不同的状态,或导出当前的状态。
在 AgentScope 中智能体Agent、记忆memory、长期记忆Long-term memory和工具模块toolkit都是有状态的对象。
AgentScope 通过支持嵌套式的状态管理,将这些对象的状态管理联系起来。
消息
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
消息message是 AgentScope 最核心的数据结构,用于
- 在智能体之间交换信息,
- 在用户交互界面显示信息,
- 在记忆中存储信息,
- 作为 AgentScope 与不同 LLM API 之间的统一媒介。
工具
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
AgentScope 中的“工具”指的是可调用的 Python 对象,包括
- 函数,
- 偏函数Partial function
- 实例方法,
- 类方法,
- 静态方法,以及
- 带有 ``__call__`` 方法的可调用实例。
此外,可调用对象可以是
- 异步或同步调用的,
- 流式或非流式返回结果的。
因此,请放心在 AgentScope 中使用任何调用对象作为智能体的工具。
智能体
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
在 AgentScope 中智能体Agent行为被抽象为 ``AgentBase`` 类中的三个核心函数:
- ``reply``:处理传入的消息并生成响应消息。
- ``observe``:接收来自环境或其它智能体的消息,但不返回响应。
- ``print``将消息显示到目标输出例如终端、Web 界面)。
为了支持用户实时介入Realtime SteeringAgentScope 提供了额外
的 ``handle_interrupt`` 函数来处理智能体回复过程中的用户中断。
此外ReAct 智能体是 AgentScope 中最重要的智能体,该智能体的回复过程分为两个阶段:
- 推理Reasoning通过调用 LLM 进行推理并生成工具调用
- 行动Acting执行工具函数。
因此,我们在 ``ReActAgentBase`` 类中提供了两个额外的核心函数,``_reasoning`` 和 ``_acting``。
提示词格式化
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
提示词格式化器Prompt Formatter是 AgentScope 中保证 LLM 兼容性的核心组件,负责将消息对象转换为 LLM API 所需的格式。
此外,诸如提示工程、截断和消息验证等附加功能也可以在格式化器中实现。
在格式化器中,"多智能体"(或"多实体")概念与常见的多智能体编排概念不同。
它专注于给定消息中包含多个身份实体的场景,因此 LLM API 中常用的 ``role`` 字段(通常取值为 "user""assistant""system")无法区分它们。
因此AgentScope 提供 MultiAgentFormatter 来处理这种场景,通常用于游戏、多人聊天和社交仿真。
.. note:: 多智能体工作流 **!=** 格式化器中的多智能体。例如,即使以下代码片段可能涉及多个
智能体(``tool_agent`` 和 ``tool_function`` 的调用者),但是输入的 ``query`` 参数
被包装成了 ``role`` 为 **“user”** 消息,因此 ``role`` 字段仍然可以区分它们。
.. code-block:: python
async def tool_function(query: str) -> str:
\"\"\"调用另一个智能体的工具函数\"\"\"
msg = Msg("user", query, role="user")
tool_agent = Agent(name="Programmer")
return await tool_agent(msg)
理解这种区别有助于开发者了解格式化器部分中单智能体和多智能体的区别。
长期记忆
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
虽然 AgentScope 为短期记忆和长期记忆提供了不同的基类,但是 AgentScope 中并没有严格区分它们的作用。
在我们看来,一切都应该是 **需求驱动的**。只要开发者的需求得到了很好的满足,完全可以只使用一个强大的记忆系统。
这里为了确保 AgentScope 的灵活性,我们为长期记忆提供了两种运行和管理方式,开发者可以根据自己的需要进行选择。
其中“agent_control”模式允许智能体自己主动管理长期记忆而“static_control”则是传统的由开发者管理的长期记忆
模式。
"""

View File

@@ -0,0 +1,267 @@
# -*- coding: utf-8 -*-
"""
.. _message:
创建消息
====================
消息是 AgentScope 中的核心概念,用于支持多模态数据、工具 API、信息存储/交换和提示构建。
一个消息由四个字段组成:
- ``name``
- ``role``
- ``content``,以及
- ``metadata``
这些字段的类型和含义如下:
.. list-table:: 消息对象中的字段
:header-rows: 1
* - 字段
- 类型
- 描述
* - name
- ``str``
- 消息发送者的名称/身份
* - role
- | ``Literal[``
| ``"system",``
| ``"assistant",``
| ``"user"``
| ``]``
- 消息发送者的角色,必须是 "system""assistant""user" 之一。
* - content
- ``str | list[ContentBlock]``
- 消息包含的数据,可以是字符串或 block 的列表。
* - metadata
- ``dict[str, JSONSerializableObject] | None``
- 包含额外元数据的字典,通常用于结构化输出。
.. tip:: - 在具有多个身份实体的应用程序中,``name`` 字段用于区分不同的身份。
- 建议将 ``metadata`` 字段用于结构化输出,在 AgentScope 内置的模块中,``metadata`` 不会参与 LLM 的提示构建。
接下来,我们根据不同的场景分别介绍 ``content`` 字段中支持的不同数据结构block
"""
from agentscope.message import (
Msg,
Base64Source,
TextBlock,
ThinkingBlock,
ImageBlock,
AudioBlock,
VideoBlock,
ToolUseBlock,
ToolResultBlock,
)
import json
# %%
# 创建文本消息
# -----------------------------
# 通过提供 ``name``、``role`` 和 ``content`` 字段来创建消息对象。
#
msg = Msg(
name="Jarvis",
role="assistant",
content="你好!我能怎么帮助你?",
)
print(f"发送者的名称: {msg.name}")
print(f"发送者的角色: {msg.role}")
print(f"消息的内容: {msg.content}")
# %%
# 创建多模态消息
# --------------------------------------
# Message 类通过提供不同的 block 结构来支持多模态内容:
#
# .. list-table:: AgentScope 中的多模态 block
# :header-rows: 1
#
# * - 类
# - 描述
# - 示例
# * - TextBlock
# - 纯文本数据
# - .. code-block:: python
#
# TextBlock(
# type="text",
# text="Hello, world!"
# )
# * - ImageBlock
# - 图像数据
# - .. code-block:: python
#
# ImageBlock(
# type="image",
# source=URLSource(
# type="url",
# url="https://example.com/image.jpg"
# )
# )
# * - AudioBlock
# - 音频数据
# - .. code-block:: python
#
# AudioBlock(
# type="audio",
# source=URLSource(
# type="url",
# url="https://example.com/audio.mp3"
# )
# )
# * - VideoBlock
# - 视频数据
# - .. code-block:: python
#
# VideoBlock(
# type="video",
# source=URLSource(
# type="url",
# url="https://example.com/video.mp4"
# )
# )
#
# 对于 ``ImageBlock``、``AudioBlock`` 和 ``VideoBlock``,还可以使用 base64 编码的字符串作为数据源Source
#
msg = Msg(
name="Jarvis",
role="assistant",
content=[
TextBlock(
type="text",
text="这是一个包含 base64 编码数据的多模态消息。",
),
ImageBlock(
type="image",
source=Base64Source(
type="base64",
media_type="image/jpeg",
data="/9j/4AAQSkZ...",
),
),
AudioBlock(
type="audio",
source=Base64Source(
type="base64",
media_type="audio/mpeg",
data="SUQzBAAAAA...",
),
),
VideoBlock(
type="video",
source=Base64Source(
type="base64",
media_type="video/mp4",
data="AAAAIGZ0eX...",
),
),
],
)
# %%
# 创建推理消息
# --------------------------------------
# ``ThinkingBlock`` 用于支持推理模型,包含模型的思考过程。
#
msg_thinking = Msg(
name="Jarvis",
role="assistant",
content=[
ThinkingBlock(
type="thinking",
thinking="我正在为 AgentScope 构建一个思考块的示例。",
),
TextBlock(
type="text",
text="这是一个思考块的示例。",
),
],
)
# %%
# .. _tool-block:
#
# 创建工具使用/结果消息
# --------------------------------------
# ``ToolUseBlock`` 和 ``ToolResultBlock`` 用于支持工具 API
#
msg_tool_call = Msg(
name="Jarvis",
role="assistant",
content=[
ToolUseBlock(
type="tool_use",
id="343",
name="get_weather",
input={
"location": "Beijing",
},
),
],
)
msg_tool_res = Msg(
name="system",
role="system",
content=[
ToolResultBlock(
type="tool_result",
id="343",
name="get_weather",
output="北京的天气是晴天,温度为 25°C。",
),
],
)
# %%
# .. tip:: AgentScope 中,通常使用 ``role`` 为“system”的消息来记录工具函数的执行结果。有关 AgentScope 中工具的更多信息,请参考 :ref:`tool` 部分。
#
# 序列化和反序列化
# ------------------------------------------------
# 消息对象可以分别通过 ``to_dict`` 和 ``from_dict`` 方法进行序列化和反序列化。
serialized_msg = msg.to_dict()
print(type(serialized_msg))
print(json.dumps(serialized_msg, indent=4, ensure_ascii=False))
# %%
# 从 JSON 格式的数据反序列化消息。
new_msg = Msg.from_dict(serialized_msg)
print(type(new_msg))
print(f'消息的发送者: "{new_msg.name}"')
print(f'发送者的角色: "{new_msg.role}"')
print(f'消息的内容: "{json.dumps(new_msg.content, indent=4, ensure_ascii=False)}"')
# %%
# 属性函数
# ------------------------------------------------
# 为了便于使用 Msg 对象AgentScope 提供了以下函数:
#
# .. list-table:: Msg 对象的函数
# :header-rows: 1
#
# * - 函数
# - 参数
# - 描述
# * - ``get_text_content``
# -
# - 将所有 ``TextBlock`` 中的内容收集到单个字符串中(用 "\\n" 分隔)。
# * - ``get_content_blocks``
# - ``block_type``
# - 返回指定类型的内容块列表。如果未提供 ``block_type``,则以块格式返回全部内容。
# * - ``has_content_blocks``
# - ``block_type``
# - 检查消息是否具有指定类型的内容块。``str`` 内容会被视为 ``TextBlock`` 类型。

View File

@@ -0,0 +1,213 @@
# -*- coding: utf-8 -*-
"""
.. _a2a:
A2A 智能体
============================
A2AAgent-to-Agent是一种开放标准协议用于实现不同 AI 智能体之间的互操作通信。
AgentScope 从获取 Agent Card 信息和连接远程智能体两个层面提供对 A2A 协议的支持,涉及到的相关 API 如下:
.. list-table:: A2A 相关类
:header-rows: 1
* - 类
- 描述
* - ``A2AAgent``
- 用于与远程 A2A 智能体通信的智能体类
* - ``A2AChatFormatter``
- 用于在 AgentScope 消息和 A2A 消息/任务格式之间进行转换的格式化器
* - ``AgentCardResolverBase``
- Agent Card 解析器基类
* - ``FileAgentCardResolver``
- 从本地 JSON 文件加载 Agent Card 的解析器
* - ``WellKnownAgentCardResolver``
- 从 URL 的 well-known 路径获取 Agent Card 的解析器
* - ``NacosAgentCardResolver``
- 从 Nacos Agent 注册中心获取 Agent Card 的解析器
本节将演示如何创建 ``A2aAgent`` 并与远程 A2A 智能体进行通信。
.. note:: 注意 A2A 的支持为**实验性功能**,可能会在未来版本中发生变化。同时由于 A2A 协议自身
的局限性,因此功能上 ``A2AAgent`` 无法完全对齐 ``ReActAgent`` 等本地智能体,包括:
- 仅支持 chatbot 场景,即仅支持一个用户与一个智能体之间的对话(不影响 handsoff/router 等使用方式)
- 不支持在对话过程中实时打断
- 不支持 agentic 结构化输出
- 目前实现中,``observe`` 方法收到的消息会被存储在本地,并在调用 ``reply`` 方法时一并发送给远程智能体,因此如果最后若干 ``observe`` 调用后未发生 ``reply`` 调用,则这些消息不会被远程智能体看到
"""
# %%
# 获取 Agent Card
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# 首先,我们需要获得一个 Agent Card 来连接对应的智能体。Agent Card 中包含了智能体的名称,描述,能力以及连接方式等信息。
#
# 手动创建 Agent Card 对象
# --------------------------------
#
# 在已知 Agent Card 各项信息的情况下,可以直接从 `a2a.types.AgentCard` 手动创建 Agent Card 对象。
#
from a2a.types import AgentCard, AgentCapabilities
from v2.nacos import ClientConfig
from agentscope.a2a import WellKnownAgentCardResolver, NacosAgentCardResolver
from agentscope.agent import A2AAgent, UserAgent
from agentscope.message import Msg, TextBlock
from agentscope.tool import ToolResponse
# 创建 Agent Card 对象
agent_card = AgentCard(
name="Friday", # 智能体名称
description="一个有趣的聊天伙伴", # 智能体描述
url="http://localhost:8000", # 智能体的 RPC 服务地址
version="1.0.0", # 智能体版本
capabilities=AgentCapabilities( # 智能体能力配置
push_notifications=False,
state_transition_history=True,
streaming=True,
),
default_input_modes=["text/plain"], # 支持的输入格式
default_output_modes=["text/plain"], # 支持的输出格式
skills=[], # 智能体技能列表
)
# %%
#
# 从远程服务获取 Agent Card
# --------------------------------
# 同时AgentScope 也支持通过多种方式动态获取 Agent Card包括从本地文件加载、从远程服务 (well-known server) 的标准路径获取以及从 Nacos 注册中心获取等。
# 这里以 ``WellKnownAgentCardResolver`` 为例,从远程服务的标准路径获取 Agent Card
#
async def agent_card_from_well_known_website() -> AgentCard:
"""从远程服务的 well-known 路径获取 Agent Card 的示例。"""
# 创建 Agent Card 解析器
resolver = WellKnownAgentCardResolver(
base_url="http://localhost:8000",
)
# 获取并返回 Agent Card
return await resolver.get_agent_card()
# %%
# 从本地文件加载 Agent Card
# --------------------------------
#
# ``FileAgentCardResolver`` 类支持从本地 JSON 文件加载 Agent Card适用于配置文件管理的场景。
# 一个 JSON 格式的 Agent Card 样例如下所示:
#
# .. code-block:: json
# :caption: 示例 Agent Card JSON 文件内容
#
# {
# "name": "RemoteAgent",
# "url": "http://localhost:8000",
# "description": "远程 A2A 智能体",
# "version": "1.0.0",
# "capabilities": {},
# "default_input_modes": ["text/plain"],
# "default_output_modes": ["text/plain"],
# "skills": []
# }
#
# 通过 ``FileAgentCardResolver`` 可以方便地加载该文件:
#
async def agent_card_from_file() -> AgentCard:
"""从本地 JSON 文件加载 Agent Card 的示例。"""
from agentscope.a2a import FileAgentCardResolver
# 从 JSON 文件加载 Agent Card
resolver = FileAgentCardResolver(
file_path="./agent_card.json", # JSON 文件路径
)
# 获取并返回 Agent Card
return await resolver.get_agent_card()
# %%
# 从 Nacos 注册中心获取 Agent Card
# --------------------------------
#
# Nacos 是一款开源的动态服务发现、配置管理和服务管理平台,在 3.1.0 版本中引入了 Agent 注册中心功能,支持 A2A 智能体的分布式注册、发现和版本管理。
#
# .. important:: 使用 ``NacosAgentCardResolver`` 的前提是用户已经部署了 3.1.0 版本以上的 Nacos 服务端,部署与注册流程请参考`官方文档 <https://nacos.io/docs/latest/quickstart/quick-start>`_。
#
async def agent_card_from_nacos() -> AgentCard:
"""从 Nacos 注册中心获取 Agent Card 的示例。"""
# 创建 Nacos Agent Card 解析器
resolver = NacosAgentCardResolver(
remote_agent_name="my-remote-agent", # Nacos 中注册的智能体名称
nacos_client_config=ClientConfig(
server_addresses="http://localhost:8848", # Nacos 服务器地址
# 其他可选配置项
),
)
# 获取并返回 Agent Card
return await resolver.get_agent_card()
# %%
# 构建 A2A 智能体
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# AgentScope 提供的 ``A2AAgent`` 类用于与远程 A2A 智能体进行通信,其使用方式与普通智能体类似。
agent = A2AAgent(agent_card=agent_card)
# %%
# 利用 ``A2AAgent``,开发者可以构建 chatbot 场景的聊天,或是封装成工具函数,从而构建 handsoff/router 等更复杂的应用场景。
# 目前 ``A2AAgent`` 支持的格式协议转换由 ``agentscope.formatter.A2AChatFormatter`` 负责,支持
#
# - 将 AgentScope 的 ``Msg`` 消息转换为 A2A 协议的 ``Message`` 格式
# - 将 A2A 协议的响应转换回 AgentScope 的 ``Msg`` 格式
# - 将 A2A 协议的 ``Task`` 相应转换成 AgentScope 的 ``Msg`` 格式
# - 支持文本、图像、音频、视频等多种内容类型
#
async def a2a_in_chatbot() -> None:
"""使用 A2AAgent 进行聊天的示例。"""
user = UserAgent("user")
msg = None
while True:
msg = await user(msg)
if msg.get_text_content() == "exit":
break
msg = await agent(msg)
# %%
# 或是如下封装成工具函数用于调用:
async def create_worker(query: str) -> ToolResponse:
"""通过子智能体完成给定的任务
Args:
query (`str`):
需要子智能体完成的任务描述
"""
res = await agent(
Msg("user", query, "user"),
)
return ToolResponse(
content=[
TextBlock(
type="text",
text=res.get_text_content(),
),
],
)

View File

@@ -0,0 +1,400 @@
# -*- coding: utf-8 -*-
"""
.. _agent:
智能体
=========================
在章我们首先重点介绍 AgentScope 中的 ReAct 智能体,然后简要介绍如何从零开始自定义智能体。
ReAct 智能体
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
在 AgentScope 中,``ReActAgent`` 类将各种功能集成到最终实现中,具体包括
.. list-table:: ``ReActAgent`` 的功能特性
:header-rows: 1
* - 功能特性
- 参考文档
* - 支持实时介入Realtime Steering
-
* - 支持记忆压缩
-
* - 支持并行工具调用
-
* - 支持结构化输出
-
* - 支持智能体自主管理工具Meta tool
- :ref:`tool`
* - 支持函数粒度的 MCP 控制
- :ref:`mcp`
* - 支持智能体自主控制长期记忆
- :ref:`long-term-memory`
* - 支持自动状态管理
- :ref:`state`
由于篇幅限制,本章我们仅演示 ``ReActAgent`` 类的前三个功能特性,其它功能我们在对应的章节进行介绍。
"""
import asyncio
import json
import os
from datetime import datetime
import time
from pydantic import BaseModel, Field
from agentscope.agent import ReActAgent
from agentscope.formatter import DashScopeChatFormatter
from agentscope.memory import InMemoryMemory
from agentscope.message import TextBlock, Msg
from agentscope.model import DashScopeChatModel
from agentscope.tool import Toolkit, ToolResponse
# %%
# 实时控制
# ---------------------------------------
#
# 实时控制指 **允许用户随时中断智能体的回复,介入智能体的执行过程**AgentScope 基于 asyncio 取消机制实现了该功能。
#
# 具体来说AgentScope 中智能体提供了 ``interrupt`` 方法,当该函数被调用时,它将取消当前正在执行的 `reply` 函数,并执行 ``handle_interrupt`` 方法进行后处理。
#
# .. hint:: 结合 :ref:`tool` 中提到的 AgentScope 支持工具函数流式返回结果的功能,工具执行过程中如果执行时间过长或偏离用户期望,用户可以通过在终端中按 Ctrl+C 或在代码中调用智能体的
# ``interrupt`` 方法来中断工具执行。
#
# .. hint:: ``ReActAgent`` 中提供了完善的中断逻辑,智能体的记忆和状态会在中断发生时被正确的保存。
#
# 中断逻辑已在 ``AgentBase`` 类中作为基本功能实现,并提供 ``handle_interrupt`` 抽象方法供用户自定义
# 中断的后处理,如下所示:
#
# .. code-block:: python
#
# # AgentBase 的代码片段
# class AgentBase:
# ...
# async def __call__(self, *args: Any, **kwargs: Any) -> Msg:
# ...
# reply_msg: Msg | None = None
# try:
# self._reply_task = asyncio.current_task()
# reply_msg = await self.reply(*args, **kwargs)
#
# except asyncio.CancelledError:
# # 捕获中断并通过 handle_interrupt 方法处理
# reply_msg = await self.handle_interrupt(*args, **kwargs)
#
# ...
#
# @abstractmethod
# async def handle_interrupt(self, *args: Any, **kwargs: Any) -> Msg:
# pass
#
#
# 在 ``ReActAgent`` 类的实现中,我们返回一个固定消息"I noticed that you have interrupted me. What can I do for you?",如下所示:
#
# .. figure:: ../../_static/images/interruption_zh.gif
# :width: 100%
# :align: center
# :class: bordered-image
# :alt: 中断示例
#
# 中断智能体 ``reply`` 的执行过程
#
# 开发者可以通过覆盖 ``handle_interrupt`` 函数实现自定义的中断后处理逻辑,例如,调用 LLM 生成对中断的简单响应。
#
#
# 记忆压缩
# ----------------------------------------
# 随着对话的不断增长,记忆中的 token 数量可能会超过模型的上下文限制或导致推理速度变慢。
# ``ReActAgent`` 提供了自动记忆压缩功能来解决这个问题。
#
# **基础用法**
#
# 要启用记忆压缩,在初始化 ``ReActAgent`` 时提供一个 ``CompressionConfig`` 实例:
#
# .. code-block:: python
#
# from agentscope.agent import ReActAgent
# from agentscope.token import CharTokenCounter
#
# agent = ReActAgent(
# name="助手",
# sys_prompt="你是一个有用的助手。",
# model=model,
# formatter=formatter,
# compression_config=ReActAgent.CompressionConfig(
# enable=True,
# agent_token_counter=CharTokenCounter(), # 智能体的 token 计数器
# trigger_threshold=10000, # 超过 10000 个 token 时触发压缩
# keep_recent=3, # 保持最近 3 条消息不被压缩
# ),
# )
#
# 启用记忆压缩后,智能体会监控其记忆中的 token 数量。
# 一旦超过 ``trigger_threshold``,智能体会自动:
#
# 1. 识别尚未被压缩的消息(通过 ``exclude_mark``
# 2. 保持最近 ``keep_recent`` 条消息不被压缩(以保留最近的上下文)
# 3. 将较早的消息发送给 LLM 生成结构化摘要
# 4. 使用 ``MemoryMark.COMPRESSED`` 标记已压缩的消息(通过 ``update_messages_mark``
# 5. 将摘要存储在记忆中(通过 ``update_compressed_summary``
#
# .. important:: 压缩采用**标记机制**而非替换消息。旧消息被标记为已压缩,并通过 ``exclude_mark=MemoryMark.COMPRESSED`` 在后续检索中被排除,而生成的摘要则单独存储,在需要时检索。这种方式保留了原始消息,允许灵活的记忆管理。关于标记功能的更多详情,请参考 :ref:`memory`。
#
# 默认情况下,压缩摘要被结构化为五个关键字段:
#
# - **task_overview**:用户的核心请求和成功标准
# - **current_state**:到目前为止已完成的工作,包括文件和输出
# - **important_discoveries**:技术约束、决策、错误和失败的尝试
# - **next_steps**:完成任务所需的具体操作
# - **context_to_preserve**:用户偏好、领域细节和做出的承诺
#
# **自定义压缩**
#
# 可以通过指定 ``summary_schema``、``summary_template`` 和 ``compression_prompt`` 参数来自定义压缩的工作方式。
#
# - **summary_schema**:使用 Pydantic 模型定义压缩摘要的结构
# - **compression_prompt**:指导 LLM 如何生成摘要
# - **summary_template**:格式化压缩摘要如何呈现给智能体
#
# 下面是一个自定义压缩的示例:
#
# .. code-block:: python
#
# from pydantic import BaseModel, Field
#
# # 定义自定义摘要结构
# class CustomSummary(BaseModel):
# main_topic: str = Field(
# max_length=200,
# description="对话的主题"
# )
# key_points: str = Field(
# max_length=400,
# description="讨论的重要观点"
# )
# pending_tasks: str = Field(
# max_length=200,
# description="待完成的任务"
# )
#
# # 使用自定义压缩配置创建智能体
# agent = ReActAgent(
# name="助手",
# sys_prompt="你是一个有用的助手。",
# model=model,
# formatter=formatter,
# compression_config=ReActAgent.CompressionConfig(
# enable=True,
# agent_token_counter=CharTokenCounter(),
# trigger_threshold=10000,
# keep_recent=3,
# # 结构化摘要的自定义 schema
# summary_schema=CustomSummary,
# # 指导压缩的自定义提示
# compression_prompt=(
# "<system-hint>请总结上述对话,"
# "重点关注主题、关键讨论点和待完成任务。</system-hint>"
# ),
# # 格式化摘要的自定义模板
# summary_template=(
# "<system-info>对话摘要:\n"
# "主题:{main_topic}\n\n"
# "关键观点:\n{key_points}\n\n"
# "待完成任务:\n{pending_tasks}"
# "</system-info>"
# ),
# ),
# )
#
# ``summary_template`` 使用 ``summary_schema`` 中定义的字段作为占位符
# (例如 ``{main_topic}``、``{key_points}``)。在 LLM 生成结构化摘要后,
# 这些占位符将被实际值替换。
#
# .. note:: 智能体确保工具使用和工具结果对在压缩过程中保持在一起,以维护对话流程的完整性。
#
# .. tip:: 可以通过指定不同的 ``compression_model`` 和 ``compression_formatter`` 来使用更小、更快的模型进行压缩,以降低成本和延迟。
#
#
#
# 并行工具调用
# ----------------------------------------
# ``ReActAgent`` 通过在其构造函数中提供 ``parallel_tool_calls`` 参数来支持并行工具调用。
# 当 LLM 生成多个工具调用且 ``parallel_tool_calls`` 设置为 ``True`` 时,
# 它们将通过 ``asyncio.gather`` 函数并行执行。
#
# .. note:: ``ReActAgent`` 中的工具并行调用是基于异步 ``asyncio.gather`` 实现的,因此,只有当工具函数是异步函数,同时工具函数内也为异步逻辑时,才能最大程度发挥工具并行执行的效果
#
# .. note:: 运行时请确保模型层面支持工具并行调用,并且相应参数设置正确(可以通过 ``generate_kwargs`` 传入例如对于DashScope API需要设置 ``parallel_tool_calls`` 为 ``True``,否则将无法进行并行工具调用。
# 准备一个工具函数
async def example_tool_function(tag: str) -> ToolResponse:
"""一个示例工具函数"""
start_time = datetime.now().strftime("%H:%M:%S.%f")
# 休眠 3 秒以模拟长时间运行的任务
await asyncio.sleep(3)
end_time = datetime.now().strftime("%H:%M:%S.%f")
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"标签 {tag} 开始于 {start_time},结束于 {end_time}",
),
],
)
toolkit = Toolkit()
toolkit.register_tool_function(example_tool_function)
# 创建一个 ReAct 智能体
agent = ReActAgent(
name="Jarvis",
sys_prompt="你是一个名为 Jarvis 的有用助手。",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
# 启用并行工具调用
generate_kwargs={
"parallel_tool_calls": True,
},
),
memory=InMemoryMemory(),
formatter=DashScopeChatFormatter(),
toolkit=toolkit,
parallel_tool_calls=True,
)
async def example_parallel_tool_calls() -> None:
"""并行工具调用示例"""
# 提示智能体同时生成两个工具调用
await agent(
Msg(
"user",
"同时生成两个 'example_tool_function' 函数的工具调用,标签分别为 'tag1''tag2',以便它们可以并行执行。",
"user",
),
)
asyncio.run(example_parallel_tool_calls())
# %%
# 结构化输出
# ----------------------------------------
# AgentScope 中的结构化输出是与工具调用紧密结合的。具体来说,``ReActAgent`` 类在其 ``__call__`` 函数中接收 ``pydantic.BaseModel`` 的子类作为 ``structured_model`` 参数。
# 从而提供复杂的结构化输出限制。
# 然后我们可以从 返回消息的 ``metadata`` 字段获取结构化输出。
#
# 以介绍爱因斯坦为例:
#
# 创建一个 ReAct 智能体
agent = ReActAgent(
name="Jarvis",
sys_prompt="你是一个名为 Jarvis 的有用助手。",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
),
formatter=DashScopeChatFormatter(),
)
# 结构化模型
class Model(BaseModel):
name: str = Field(description="人物的姓名")
description: str = Field(description="人物的一句话描述")
age: int = Field(description="年龄")
honor: list[str] = Field(description="人物荣誉列表")
async def example_structured_output() -> None:
"""结构化输出示例"""
res = await agent(
Msg(
"user",
"介绍爱因斯坦",
"user",
),
structured_model=Model,
)
print("\n结构化输出:")
print(json.dumps(res.metadata, indent=4, ensure_ascii=False))
asyncio.run(example_structured_output())
# %%
# 自定义智能体
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope 提供了两个基类:``AgentBase`` 和 ``ReActAgentBase``,它们在抽象方法和支持的钩子函数方面有所不同。
# 具体来说,``ReActAgentBase`` 扩展了 ``AgentBase``,增加了额外的 ``_reasoning`` 和 ``_acting`` 抽象方法,以及它们的前置和后置钩子函数。
#
# 开发者可以根据需要选择继承这两个基类中的任一个。
# 我们总结了 ``agentscope.agent`` 模块下的智能体如下:
#
# .. list-table:: AgentScope 中的智能体类
# :header-rows: 1
#
# * - 类
# - 抽象方法
# - 支持的钩子函数
# - 描述
# * - ``AgentBase``
# - | ``reply``
# | ``observe``
# | ``print``
# | ``handle_interrupt``
# - | pre\_/post_reply
# | pre\_/post_observe
# | pre\_/post_print
# - 所有智能体的基类,提供基本接口和钩子。
# * - ``ReActAgentBase``
# - | ``reply``
# | ``observe``
# | ``print``
# | ``handle_interrupt``
# | ``_reasoning``
# | ``_acting``
# - | pre\_/post_reply
# | pre\_/post_observe
# | pre\_/post_print
# | pre\_/post_reasoning
# | pre\_/post_acting
# - ReAct 类智能体的抽象类,扩展了 ``AgentBase``,增加了 ``_reasoning`` 和 ``_acting`` 抽象方法及其钩子。
# * - ``ReActAgent``
# - \-
# - | pre\_/post_reply
# | pre\_/post_observe
# | pre\_/post_print
# | pre\_/post_reasoning
# | pre\_/post_acting
# - ``ReActAgentBase`` 的实现
# * - ``UserAgent``
# -
# -
# - 代表用户的特殊智能体,用于与智能体交互
# * - ``A2aAgent``
# - \-
# - | pre\_/post_reply
# | pre\_/post_observe
# | pre\_/post_print
# - 用于与远程 A2A 代理通信的智能体,详见 :ref:`a2a`
#
#
#
# 进一步阅读
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# - :ref:`tool`
# - :ref:`hook`
# - :ref:`a2a`
#

View File

@@ -0,0 +1,124 @@
# -*- coding: utf-8 -*-
"""
.. _agent_skill:
智能体技能
============================
`智能体技能Agent skill <https://claude.com/blog/skills>`_ 是 Anthropic 提出的一种提升智能体在特定任务上能力的方法。
AgentScope 通过 ``Toolkit`` 类提供了对智能体技能的内置支持,让开发者可以注册和管理智能体技能。
相关 API 如下:
.. list-table:: ``Toolkit`` 类中的智能体技能 API
:header-rows: 1
* - API
- 描述
* - ``register_agent_skill``
- 从指定目录注册智能体技能
* - ``remove_agent_skill``
- 根据名称移除已注册的智能体技能
* - ``get_agent_skill_prompt``
- 获取所有已注册智能体技能的提示词,可以附加到智能体的系统提示词中
本节将演示如何注册智能体技能并在 ReActAgent 类中使用它们。
"""
import os
from agentscope.agent import ReActAgent
from agentscope.formatter import DashScopeChatFormatter
from agentscope.memory import InMemoryMemory
from agentscope.model import DashScopeChatModel
from agentscope.tool import Toolkit
# %%
# 注册智能体技能
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# 首先,我们需要准备一个智能体技能目录,该目录需要遵循 `Anthropic blog <https://claude.com/blog/skills>`_ 中指定的要求。
#
# .. note:: 技能目录必须包含一个 ``SKILL.md`` 文件,其中包含 YAML 前置元数据和指令说明。
#
# 这里我们创建一个示例技能目录 ``sample_skill``,包含以下文件:
#
# .. code-block:: markdown
#
# ---
# name: sample_skill
# description: 用于演示的示例智能体技能
# ---
#
# # 示例技能
# ...
#
os.makedirs("sample_skill", exist_ok=True)
with open("sample_skill/SKILL.md", "w", encoding="utf-8") as f:
f.write(
"""---
name: sample_skill
description: 用于演示的示例智能体技能
---
# 示例技能
...
""",
)
# %%
# 然后,我们可以使用 ``Toolkit`` 类的 ``register_agent_skill`` API 注册技能。
#
toolkit = Toolkit()
toolkit.register_agent_skill("sample_skill")
# %%
# 之后,我们可以使用 ``get_agent_skill_prompt`` API 获取所有已注册智能体技能的提示词
agent_skill_prompt = toolkit.get_agent_skill_prompt()
print("智能体技能提示词:")
print(agent_skill_prompt)
# %%
# 当然,我们也可以在创建 ``Toolkit`` 实例时自定义提示词模板。
custom_toolkit = Toolkit(
# 向智能体/大语言模型介绍如何使用技能的指令
agent_skill_instruction="<system-info>为你提供了一组技能,每个技能都在一个目录中,并由 SKILL.md 文件进行描述。</system-info>",
# 用于格式化每个技能提示词的模板,必须包含 {name}、{description} 和 {dir} 字段
agent_skill_template="- {name}(in directory '{dir}'): {description}",
)
custom_toolkit.register_agent_skill("sample_skill")
agent_skill_prompt = custom_toolkit.get_agent_skill_prompt()
print("自定义智能体技能提示词:")
print(agent_skill_prompt)
# %%
# 在 ReActAgent 中集成智能体技能
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# AgentScope 中的 `ReActAgent` 类会自动将智能体技能提示词附加到系统提示词中。
#
# 我们可以按如下方式创建一个带有已注册智能体技能的 ReAct 智能体:
#
# .. important:: 使用智能体技能时,智能体必须配备文本文件读取或 shell 命令工具,以便访问 `SKILL.md` 文件中的技能指令。
#
agent = ReActAgent(
name="Friday",
sys_prompt="你是一个名为 Friday 的智能助手。",
model=DashScopeChatModel(
model_name="qwen3-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
),
memory=InMemoryMemory(),
formatter=DashScopeChatFormatter(),
toolkit=toolkit,
)
print("带有智能体技能的系统提示词:")
print(agent.sys_prompt)

View File

@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
"""
.. _embedding:
嵌入(Embedding)
=========================
AgentScope 中,嵌入模块提供了用于向量生成的统一接口,具有以下特性:
- 支持 **缓存 embedding** 以避免冗余的 API 调用
- 支持 **不同 embedding API 提供商** 并提供一致的接口
AgentScope 内置支持以下 API
.. list-table::
:header-rows: 1
* - API 提供商
- 类
* - OpenAI
- ``OpenAITextEmbedding``
* - Gemini
- ``GeminiTextEmbedding``
* - DashScope
- ``DashScopeTextEmbedding``, ``DashScopeMultiModalEmbedding``
* - Ollama
- ``OllamaTextEmbedding``
所有类都继承自 ``EmbeddingModelBase``,实现了 ``__call__`` 方法并生成包含嵌入和使用信息的 ``EmbeddingResponse`` 对象。
其中 ``DashScopeMultiModalEmbedding`` 支持文本,图像和视频的多模态嵌入。
以 DashScope 嵌入类为例,可以按如下方式使用:
"""
import asyncio
import os
import tempfile
from agentscope.embedding import DashScopeTextEmbedding, FileEmbeddingCache
async def example_dashscope_embedding() -> None:
"""DashScope 文本嵌入的使用示例。"""
texts = [
"法国的首都是什么?",
"巴黎是法国的首都城市。",
]
# 初始化 DashScope 文本嵌入实例
embedding_model = DashScopeTextEmbedding(
model_name="text-embedding-v2",
api_key=os.getenv("DASHSCOPE_API_KEY"),
)
# 从模型获取嵌入
response = await embedding_model(texts)
print("嵌入 ID: ", response.id)
print("嵌入创建时间: ", response.created_at)
print("嵌入使用情况: ", response.usage)
print("嵌入向量:")
print(response.embeddings)
asyncio.run(example_dashscope_embedding())
# %%
# 可以通过继承 ``EmbeddingModelBase`` 并实现 ``__call__`` 方法来自定义 embedding 模型。
#
# Embedding 缓存
# ---------------------
# AgentScope 提供了用于缓存 embedding 的基类 ``EmbeddingCacheBase``,以及基于文件的实现 ``FileEmbeddingCache``。
# 它在 embedding 模块中的工作方式如下:
#
# .. image:: ../../_static/images/embedding_cache.png
# :align: center
# :width: 90%
#
# 要使用缓存,只需将 ``FileEmbeddingCache`` 实例(或自定义缓存)传给模型的构造函数,如下所示:
#
async def example_embedding_cache() -> None:
"""演示带有缓存功能的 embedding。"""
# 示例文本
texts = [
"法国的首都是什么?",
"巴黎是法国的首都城市。",
]
# 为缓存演示创建临时目录
# 在实际应用中,建议使用持久目录以最大发挥缓存效果
cache_dir = tempfile.mkdtemp(prefix="embedding_cache_")
print(f"使用缓存目录: {cache_dir}")
# 使用缓存初始化嵌入模型
# 为演示目的,我们将缓存限制为 100 个文件和 10MB
embedder = DashScopeTextEmbedding(
model_name="text-embedding-v3",
api_key=os.getenv("DASHSCOPE_API_KEY"),
embedding_cache=FileEmbeddingCache(
cache_dir=cache_dir,
max_file_number=100,
max_cache_size=10, # 最大缓存大小MB
),
)
# 第一次调用 - 将从 API 获取并存储在缓存中
print("\n=== 第一次 API 调用(无缓存命中)===")
start_time = asyncio.get_event_loop().time()
response1 = await embedder(texts)
elapsed_time1 = asyncio.get_event_loop().time() - start_time
print(f"来源: {response1.source}") # 应该是 'api'
print(f"耗时: {elapsed_time1:.4f}")
print(f"使用的 token: {response1.usage.tokens}")
# 使用相同文本的第二次调用 - 应该使用缓存
print("\n=== 第二次 API 调用(预期缓存命中)===")
start_time = asyncio.get_event_loop().time()
response2 = await embedder(texts)
elapsed_time2 = asyncio.get_event_loop().time() - start_time
print(f"来源: {response2.source}") # 应该是 'cache'
print(f"耗时: {elapsed_time2:.4f}")
print(
f"使用的 token: {response2.usage.tokens}",
) # 缓存结果应该为 0
print(
f"速度提升: 使用缓存快 {elapsed_time1 / elapsed_time2:.1f}",
)
asyncio.run(example_embedding_cache())

View File

@@ -0,0 +1,255 @@
# -*- coding: utf-8 -*-
"""
.. _eval:
智能体评测
=========================
AgentScope 提供了一个内置的评测框架,用于评测智能体在不同任务和基准测试中的性能,主要特性包括:
- 基于 `Ray <https://github.com/ray-project/ray>`_ 的并行和分布式评估
- 支持中断后继续评估
- [开发中] 评估结果可视化
.. note:: 我们正在持续集成新的基准测试到 AgentScope 中:
- ✅ `ACEBench <https://github.com/ACEBench/ACEBench>`_
- 🚧 `GAIA <https://huggingface.co/datasets/gaia-benchmark/GAIA/tree/main>`_ 基准测试
概述
---------------------------
AgentScope 评估框架由几个关键组件组成:
- **基准测试 (Benchmark)**: 用于系统性评估的任务集合
- **任务 (Task)**: 包含输入、标准答案和指标的独立评估单元
- **指标 (Metric)**: 评估解决方案质量的测量函数
- **评估器 (Evaluator)**: 运行评估的引擎,聚合结果并分析性能
- **评估器存储 (Evaluator Storage)**: 用于记录和检索评估结果的持久化存储
- **解决方案 (Solution)**: 用户定义的解决方案
.. figure:: ../../_static/images/evaluation.png
:width: 90%
:alt: AgentScope 评估框架
*AgentScope 评估框架*
AgentScope 当前的实现包括:
- 评估器:
- ``RayEvaluator``: 基于 ray 的评估器,支持并行和分布式评估。
- ``GeneralEvaluator``: 通用评估器,按顺序运行任务,便于调试。
- 基准测试:
- ``ACEBench``: 用于评估智能体能力的基准测试。
我们在 `GitHub 仓库 <https://github.com/agentscope-ai/agentscope/tree/main/examples/evaluation/ace_bench>`_ 中提供了一个使用 ``RayEvaluator`` 和 ACEBench 中智能体多步骤任务的玩具示例。
核心组件
---------------
我们将构建一个简单的玩学问题基准测试来演示如何使用 AgentScope 评估模块。
"""
TOY_BENCHMARK = [
{
"id": "math_problem_1",
"question": "What is 2 + 2?",
"ground_truth": 4.0,
"tags": {
"difficulty": "easy",
"category": "math",
},
},
{
"id": "math_problem_2",
"question": "What is 12345 + 54321 + 6789 + 9876?",
"ground_truth": 83331,
"tags": {
"difficulty": "medium",
"category": "math",
},
},
]
# %%
# 从任务、解决方案和指标到基准测试
# ~~~~~~~~~~~~~~~~~~~
#
# - 一个 ``SolutionOutput`` (Agent解决方案输出) 包含智能体生成的所有信息,包括轨迹和最终输出。
# - 一个 ``Metric`` (评测指标) 代表一个单一的评估可调用实例,它将生成的解决方案(例如,轨迹或最终输出)与标准答案进行比较。
# 在这个示例中,我们定义了一个指标,简单地检查解决方案中的 ``output`` 字段是否与标准答案匹配。
from agentscope.evaluate import (
SolutionOutput,
MetricBase,
MetricResult,
MetricType,
)
class CheckEqual(MetricBase):
def __init__(
self,
ground_truth: float,
):
super().__init__(
name="math check number equal",
metric_type=MetricType.NUMERICAL,
description="Toy metric checking if two numbers are equal",
categories=[],
)
self.ground_truth = ground_truth
async def __call__(
self,
solution: SolutionOutput,
) -> MetricResult:
if solution.output == self.ground_truth:
return MetricResult(
name=self.name,
result=1.0,
message="Correct",
)
else:
return MetricResult(
name=self.name,
result=0.0,
message="Incorrect",
)
# %%
# - 一个 ``Task`` (任务) 是基准测试中的一个单元,包含智能体执行和评估所需的所有信息(例如,输入/查询及其标准答案)。
# - 一个 ``Benchmark`` (基准测试) 组织多个任务进行系统性评估。
from typing import Generator
from agentscope.evaluate import (
Task,
BenchmarkBase,
)
class ToyBenchmark(BenchmarkBase):
def __init__(self):
super().__init__(
name="Toy bench",
description="A toy benchmark for demonstrating the evaluation module.",
)
self.dataset = self._load_data()
@staticmethod
def _load_data() -> list[Task]:
dataset = []
for item in TOY_BENCHMARK:
dataset.append(
Task(
id=item["id"],
input=item["question"],
ground_truth=item["ground_truth"],
tags=item.get("tags", {}),
metrics=[
CheckEqual(item["ground_truth"]),
],
metadata={},
),
)
return dataset
def __iter__(self) -> Generator[Task, None, None]:
"""遍历基准测试。"""
for task in self.dataset:
yield task
def __getitem__(self, index: int) -> Task:
"""根据索引获取任务。"""
return self.dataset[index]
def __len__(self) -> int:
"""获取基准测试的长度。"""
return len(self.dataset)
# %%
# 评估器
# ~~~~~~~~~~
#
# 评估器 (Evaluators) 管理评估过程。它们可以自动遍历
# 基准测试中的任务,并将每个任务输入到解决方案生成函数中,
# 开发者需要在其中定义运行智能体和检索
# 执行结果和轨迹的逻辑。下面是一个
# 使用我们的玩具基准测试运行 ``GeneralEvaluator`` (通用评估器) 的示例。如果有一个大型
# 基准测试,开发者希望通过并行化更高效地进行评估,
# ``RayEvaluator`` (Ray评估器) 也可作为内置解决方案使用。
import os
import asyncio
from typing import Callable
from pydantic import BaseModel
from agentscope.message import Msg
from agentscope.model import DashScopeChatModel
from agentscope.formatter import DashScopeChatFormatter
from agentscope.agent import ReActAgent
from agentscope.evaluate import (
GeneralEvaluator,
FileEvaluatorStorage,
)
class ToyBenchAnswerFormat(BaseModel):
answer_as_number: float
async def toy_solution_generation(
task: Task,
pre_hook: Callable,
) -> SolutionOutput:
agent = ReActAgent(
name="Friday",
sys_prompt="You are a helpful assistant named Friday. "
"Your target is to solve the given task with your tools. "
"Try to solve the task as best as you can.",
model=DashScopeChatModel(
api_key=os.environ.get("DASHSCOPE_API_KEY"),
model_name="qwen-max",
stream=False,
),
formatter=DashScopeChatFormatter(),
)
agent.register_instance_hook(
"pre_print",
"save_logging",
pre_hook,
)
msg_input = Msg("user", task.input, role="user")
res = await agent(
msg_input,
structured_model=ToyBenchAnswerFormat,
)
return SolutionOutput(
success=True,
output=res.metadata.get("answer_as_number", None),
trajectory=[],
)
async def main() -> None:
evaluator = GeneralEvaluator(
name="Toy benchmark evaluation",
benchmark=ToyBenchmark(),
# 重复多少次
n_repeat=1,
storage=FileEvaluatorStorage(
save_dir="./results",
),
# 使用多少个工作进程
n_workers=1,
)
# 运行评估
await evaluator.run(toy_solution_generation)
asyncio.run(main())

View File

@@ -0,0 +1,346 @@
# -*- coding: utf-8 -*-
"""
OpenJudge 评估器
=======================
[OpenJudge](https://github.com/agentscope-ai/OpenJudge) 是一个专为评估LLM/Agent应用质量而设计的评估框架。通过将 OpenJudge 集成到 AgentScope 中,您可以将 AgentScope 的原生评估能力从基础的执行检查扩展到深度的语义质量分析。
本指南中我们将介绍如何使用 OpenJudge 的评估器(Grader)作为 AgentScope 的评估指标(Metric)来评估您的智能体应用。
.. note::
在运行本教程之前,请安装必要的依赖:
.. code-block:: bash
pip install agentscope py-openjudge
为什么选择 OpenJudge
----------------------
虽然 AgentScope 提供了强大的评估框架用于定义评估逻辑,但实现复杂的语义级指标(如“幻觉检测”或“回复相关性”)通常需要大量的 Prompt 工程和流程构建工作。
集成 OpenJudge 可以为 AgentScope 带来了三个维度的能力提升:
1. **提升评估深度**:从简单的成功/失败检查升级为多维度的质量评估(如准确性、安全性、语气等)。
2. **开箱即用的 Grader**:直接使用 50+ 个预置的、专家级验证过的 Grader无需手动编写评估 Prompt详情请参阅 [OpenJudge官方文档](https://agentscope-ai.github.io/OpenJudge/built_in_graders/overview/)。
3. **闭环迭代**:将 OpenJudge 无缝嵌入 AgentScope 的 ``Benchmark`` 中,同时获取量化的分数和定性的理由分析。
如何使用 OpenJudge 进行评估
---------------------------
我们将构建一个简单的问答QA基准测试演示如何通过集成 OpenJudge 的 Grader 来使用 AgentScope 的评估模块。
"""
# %%
QA_BENCHMARK_DATASET = [
{
"id": "qa_task_1",
"question": "What are the health benefits of regular exercise?",
"reference_output": "Regular exercise improves cardiovascular health, strengthens muscles and bones, "
"helps maintain a healthy weight, and can improve mental health by reducing anxiety and depression.",
"ground_truth": "Answers should cover physical and mental health benefits",
"difficulty": "medium",
"category": "health",
},
{
"id": "qa_task_2",
"question": "Describe the main causes of climate change.",
"reference_output": "Climate change is primarily caused by increased concentrations of greenhouse gases "
"in the atmosphere due to human activities like burning fossil fuels, deforestation, and industrial processes.",
"ground_truth": "Answers should mention greenhouse gases and human activities",
"difficulty": "hard",
"category": "environment",
},
{
"id": "qa_task_3",
"question": "What is the significance of the Turing Test in AI?",
"reference_output": "The Turing Test, proposed by Alan Turing, is a measure of a machine's ability to exhibit"
" intelligent behavior equivalent to, or indistinguishable from, that of a human.",
"ground_truth": "Should mention Alan Turing, purpose of the test, and its implications for AI",
"difficulty": "hard",
"category": "technology",
},
]
# %% [markdown]
# AgentScope Metric vs. OpenJudge Grader
# ~~~~~~~~~~
# 为了使 AgentScope 兼容 OpenJudge我们需要一个适配器Adapter来完成两个框架间的转换。
# 这个适配器继承自 AgentScope 的 ``MetricBase``,并充当通往 OpenJudge ``BaseGrader`` 的桥梁。
#
# * **AgentScope Metric**: 一个通用的评估单元,接收 ``SolutionOutput`` 并返回 ``MetricResult``。
# * **OpenJudge Grader**: 一个特定的评估单元(例如 ``RelevanceGrader``),需要特定的语义输入(如 ``query``, ``response``, ``context``),返回``GraderResult``。
#
# 这个“适配器”允许您将 *任何* OpenJudge Grader 无缝插入到您的 AgentScope 基准测试中。
# %%
from openjudge.graders.base_grader import BaseGrader
from openjudge.graders.schema import GraderScore, GraderError
from openjudge.utils.mapping import parse_data_with_mapper
from agentscope.evaluate import (
MetricBase,
MetricType,
MetricResult,
SolutionOutput,
)
class OpenJudgeMetric(MetricBase):
def __init__(
self,
grader_cls: type[BaseGrader],
data: dict,
mapper: dict,
name: str | None = None,
description: str | None = None,
**grader_kwargs,
):
"""Initializes the OpenJudgeMetric.
Args:
grader_cls (`type[BaseGrader]`):
The OpenJudge grader class to be wrapped.
data (`dict`):
The static data for the task.
mapper (`dict`):
The mapper to extract grader inputs from combined data.
name (`str | None`, optional):
The name of the metric. Defaults to the grader's name.
description (`str | None`, optional):
The description of the metric. Defaults to the grader's
description.
**grader_kwargs:
Additional keyword arguments for the grader initialization.
"""
self.grader = grader_cls(**grader_kwargs)
super().__init__(
name=name or self.grader.name,
metric_type=MetricType.NUMERICAL,
description=description or self.grader.description,
)
self.data = data
self.mapper = mapper
async def __call__(self, solution: SolutionOutput) -> MetricResult:
"""针对 Agent 的输出执行封装好的 OpenJudge Grader。"""
if not solution.success:
return MetricResult(
name=self.name,
result=0.0,
message="Solution failed",
)
try:
# 1. 构建上下文
# 将静态的任务上下文 (data) 和动态的 Agent 输出 (solution) 组合
combined_data = {
"data": self.data,
"solution": {
"output": solution.output,
"meta": solution.meta,
"trajectory": getattr(solution, "trajectory", []),
},
}
# 2. 数据映射
# 使用 mapper 从组合数据中提取Grader需要的 'query', 'response', 'context' 等参数
grader_inputs = parse_data_with_mapper(
combined_data,
self.mapper,
)
## 3. 执行评估
result = await self.grader.aevaluate(**grader_inputs)
# 4. 格式化结果
if isinstance(result, GraderScore):
return MetricResult(
name=self.name,
result=result.score,
# 保留 OpenJudge 提供的详细理由
message=result.reason or "",
)
elif isinstance(result, GraderError):
return MetricResult(
name=self.name,
result=0.0,
message=f"Error: {result.error}",
)
else:
return MetricResult(
name=self.name,
result=0.0,
message="Unknown result type",
)
except Exception as e:
return MetricResult(
name=self.name,
result=0.0,
message=f"Exception: {str(e)}",
)
# %% [markdown]
# 从 OpenJudge 到 AgentScope
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# OpenJudge 提供了丰富的内置 Grader 集合。在当前实例中,我们选择两个适合问答任务的常用 Grader
#
# * **RelevanceGrader (相关性)**:评估 Agent 的回答是否直接回应了用户的查询(忽略事实准确性)。
# * **CorrectnessGrader (正确性)**根据提供的参考答案Ground Truth验证回答的事实准确性。
#
# .. tip::
# OpenJudge 提供了 50+ 种内置 Grader涵盖 **幻觉检测**、**安全性**、**代码质量** 和 **JSON 格式化** 等多个维度。
# 请查阅 `OpenJudge 官方文档 <https://agentscope-ai.github.io/OpenJudge/built_in_graders/overview/>`_ 获取完整列表。
#
# .. note::
# 在运行以下示例之前,请确保您已设置 ``DASHSCOPE_API_KEY`` 环境变量。
# %%
import os
from typing import Generator
from openjudge.graders.common.relevance import RelevanceGrader
from openjudge.graders.common.correctness import CorrectnessGrader
from agentscope.evaluate import (
Task,
BenchmarkBase,
)
class QABenchmark(BenchmarkBase):
def __init__(self):
super().__init__(
name="QA Quality Benchmark",
description="Benchmark to evaluate QA systems using OpenJudge grader classes",
)
self.dataset = self._load_data()
def _load_data(self):
tasks = []
# 配置 LLM Grader 的模型参数
# 注意:如果不使用环境变量,请在此处设置 "api_key"
model_config = {
"model": "qwen3-32b",
"api_key": os.environ.get("DASHSCOPE_API_KEY"),
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
}
for data in QA_BENCHMARK_DATASET:
# 定义映射关系:左侧是 OpenJudge 的键,右侧是 AgentScope 的数据路径
mapper = {
"query": "data.input",
"response": "solution.output",
"context": "data.ground_truth",
"reference_response": "data.reference_output",
}
# 通过 Adapter 实例化 Metrics
metrics = [
OpenJudgeMetric(
grader_cls=RelevanceGrader,
data=data,
mapper=mapper,
name="Relevance",
model=model_config,
),
OpenJudgeMetric(
grader_cls=CorrectnessGrader,
data=data,
mapper=mapper,
name="Correctness",
model=model_config,
),
]
# 创建 Task
task = Task(
id=data["id"],
input=data["question"],
ground_truth=data["ground_truth"],
metrics=metrics,
)
tasks.append(task)
return tasks
def __iter__(self) -> Generator[Task, None, None]:
yield from self.dataset
def __getitem__(self, index: int) -> Task:
return self.dataset[index]
def __len__(self) -> int:
return len(self.dataset)
# %% [markdown]
# 运行评估
# ~~~~~~~~~~
# 最后,使用 AgentScope 的 ``GeneralEvaluator`` 对一个简单的QA Agent进行评估测试。
# 我们将收集到来自 OpenJudge Grader 的 **量化分数 (Score)** 和 **定性理由 (Reasoning)**。
# %%
import asyncio
from typing import Callable
from agentscope.agent import ReActAgent
from agentscope.evaluate import GeneralEvaluator
from agentscope.evaluate import FileEvaluatorStorage
from agentscope.formatter import DashScopeChatFormatter
from agentscope.message import Msg
from agentscope.model import OpenAIChatModel
async def qa_agent(task: Task, pre_hook: Callable) -> SolutionOutput:
model = OpenAIChatModel(
model_name="qwen3-32b",
api_key=os.getenv("DASHSCOPE_API_KEY"),
)
agent = ReActAgent(
name="QAAgent",
sys_prompt="You are an expert at answering questions. Provide clear, accurate, and comprehensive answers.",
model=model,
formatter=DashScopeChatFormatter(),
)
# Generate response
msg_input = Msg(name="User", content=task.input, role="user")
response = await agent(msg_input)
response_text = response.content
return SolutionOutput(
success=True,
output=response_text,
trajectory=[
task.input,
response_text,
], # Store the interaction trajectory
)
async def main() -> None:
evaluator = GeneralEvaluator(
name="OpenJudge Integration Demo",
benchmark=QABenchmark(),
# Repeat how many times
n_repeat=1,
storage=FileEvaluatorStorage(
save_dir="./results",
),
# How many workers to use
n_workers=1,
)
await evaluator.run(qa_agent)
# %% [markdown]
#
# ~~~~~~~~~~
# 最后,使用 AgentScope 的 ``GeneralEvaluator`` 对一个简单的QA Agent进行评估测试。
# 我们将收集到来自 OpenJudge Grader 的 **量化分数 (Score)** 和 **定性理由 (Reasoning)**。

View File

@@ -0,0 +1,286 @@
# -*- coding: utf-8 -*-
"""
.. _hook:
智能体钩子函数
===========================
钩子Hook是 AgentScope 中的扩展点,允许开发者在特定位置自定义智能体行为,提供了一种灵活的方式来修改或扩展智能体的功能,而无需更改其核心实现。
在 AgentScope 中,钩子围绕智能体的核心函数实现:
.. list-table:: AgentScope 中支持的钩子类型
:header-rows: 1
* - 智能体类
- 核心函数
- 钩子类型
- 描述
* - | ``AgentBase`` 及其子类
- ``reply``
- | ``pre_reply``
| ``post_reply``
- 智能体回复消息前/后的钩子
* -
- ``print``
- | ``pre_print``
| ``post_print``
- 向目标输出如终端、Web 界面)打印消息前/后的钩子
* -
- ``observe``
- | ``pre_observe``
| ``post_observe``
- 从环境或其它智能体观察消息前/后的钩子
* - | ``ReActAgentBase`` 及其子类
- | ``reply``
| ``print``
| ``observe``
- | ``pre_reply``
| ``post_reply``
| ``pre_print``
| ``post_print``
| ``pre_observe``
| ``post_observe``
-
* -
- ``_reasoning``
- | ``pre_reasoning``
| ``post_reasoning``
- 智能体推理过程前/后的钩子
* -
- ``_acting``
- | ``pre_acting``
| ``post_acting``
- 智能体行动过程前/后的钩子
.. tip:: 由于 AgentScope 中的钩子函数是通过 meta class 实现的,因此支持继承。
为了简化使用AgentScope 为所有钩子提供了统一的签名。
"""
import asyncio
from typing import Any, Type
from agentscope.agent import ReActAgentBase, AgentBase
from agentscope.message import Msg
# %%
# 钩子签名
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# AgentScope 为所有前置pre_和后置post_钩子提供统一的钩子签名如下所示
#
# **前置钩子签名**
#
# .. list-table:: 所有前置钩子的签名
# :header-rows: 1
#
# * -
# - 名称
# - 描述
# * - 参数
# - ``self: AgentBase | ReActAgentBase``
# - 智能体实例
# * -
# - ``kwargs: dict[str, Any]``
# - | 目标函数的输入参数,或来自最近
# | 一个非 None 返回值的钩子修
# | 改后的参数
# * - 返回值
# - ``dict[str, Any] | None``
# - 修改后的参数或 None
#
# .. note:: 核心函数的所有位置参数(*args和关键字参数**kwargs被统一成单个 ``kwargs`` 字典传递给钩子函数
#
# 前置钩子模板定义如下:
#
def pre_hook_template(
self: AgentBase | ReActAgentBase,
kwargs: dict[str, Any],
) -> dict[str, Any] | None: # 修改后的输入
"""前置钩子模板。"""
pass
# %%
# **后置钩子签名**
#
# 对于后置钩子,在签名中增加了一个额外的 ``output`` 参数,表示目标函数的输出。
# 如果核心函数没有输出,``output`` 参数将为 ``None``。
#
# .. list-table:: 所有后置钩子的签名
# :header-rows: 1
#
# * -
# - 名称
# - 描述
# * - 参数
# - ``self: AgentBase | ReActAgentBase``
# - 智能体实例
# * -
# - ``kwargs: dict[str, Any]``
# - | 包含目标函数所有参数的字典
# * -
# - ``output: Any``
# - | 目标函数的输出或来自前序钩子
# | 最近一个非 None 返回值
# * - 返回值
# - ``dict[str, Any] | None``
# - 修改后的输出或 None
#
def post_hook_template(
self: AgentBase | ReActAgentBase,
kwargs: dict[str, Any],
output: Any, # 目标函数的输出
) -> Any: # 修改后的输出
"""后置钩子模板。"""
pass
# %%
# 钩子管理
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# AgentScope 提供实例级instance和类级class钩子其区别在于钩子函数的作用范围。
# 它们按以下顺序执行:
#
# .. image:: ../../_static/images/sequential_hook.png
# :width: 90%
# :align: center
# :alt: AgentScope 中的钩子
# :class: bordered-image
#
# AgentScope 提供内置方法来管理实例级和类级的钩子,如下所示:
#
# .. list-table:: AgentScope 中的钩子管理方法
# :header-rows: 1
#
# * - 级别
# - 方法
# - 描述
# * - 实例级
# - ``register_instance_hook``
# - | 为当前对象注册具有给定钩子类型
# | 和名称的钩子。
# * -
# - ``remove_instance_hook``
# - | 移除当前对象具有给定钩子类型
# | 和名称的钩子。
# * -
# - ``clear_instance_hooks``
# - | 清除当前对象具有给定钩子类型
# | 的所有钩子。
# * - 类级
# - ``register_class_hook``
# - | 为该类的所有对象注册具有给定
# | 钩子类型和名称的钩子。
# * -
# - ``remove_class_hook``
# - | 移除该类所有对象具有给定
# | 钩子类型和名称的钩子。
# * -
# - ``clear_class_hooks``
# - | 清除该类所有对象具有给定
# | 钩子类型的所有钩子。
#
# 使用钩子时,开发者需要注意以下规则:
#
# .. important:: **执行顺序**
#
# - 钩子按注册顺序执行
# - 多个钩子可以链式连接
# **返回值处理**
#
# - 对于前置钩子:非 None 返回值会传递给下一个钩子或核心函数
# - 当钩子返回 None 时,下一个钩子将使用前序钩子中最近的非 None 返回值
# - 如果所有前序钩子都返回 None那该钩子接收原始参数的副本作为输入
# - 最后一个非 None 返回值(或如果所有钩子都返回 None 则使用原始参数)传递给核心函数
# - 对于后置钩子:工作方式与前置钩子相似。
# **重要提示**不要在钩子内调用核心函数reply/speak/observe/_reasoning/_acting以避免循环调用
#
# 以下面的智能体为例,我们可以看到如何注册、移除和清除钩子:
#
# 创建一个简单的测试智能体类
class TestAgent(AgentBase):
"""用于演示钩子的测试智能体。"""
async def reply(self, msg: Msg) -> Msg:
"""回复消息。"""
return msg
# %%
# 我们创建一个实例级钩子和一个类级钩子来在回复前修改消息内容。
#
# 创建两个前置回复钩子
def instance_pre_reply_hook(
self: AgentBase,
kwargs: dict[str, Any],
) -> dict[str, Any]:
"""修改消息内容的前置回复钩子。"""
msg = kwargs["msg"]
msg.content += "[instance-pre-reply]"
# 返回修改后的 kwargs
return {
**kwargs,
"msg": msg,
}
def cls_pre_reply_hook(
self: AgentBase,
kwargs: dict[str, Any],
) -> dict[str, Any]:
"""修改消息内容的前置回复钩子。"""
msg = kwargs["msg"]
msg.content += "[cls-pre-reply]"
# 返回修改后的 kwargs
return {
**kwargs,
"msg": msg,
}
# 注册类钩子
TestAgent.register_class_hook(
hook_type="pre_reply",
hook_name="test_pre_reply",
hook=cls_pre_reply_hook,
)
# 注册实例钩子
agent = TestAgent()
agent.register_instance_hook(
hook_type="pre_reply",
hook_name="test_pre_reply",
hook=instance_pre_reply_hook,
)
async def example_test_hook() -> None:
"""测试钩子的示例函数。"""
msg = Msg(
name="user",
content="Hello, world!",
role="user",
)
res = await agent(msg)
print("响应内容:", res.content)
TestAgent.clear_class_hooks()
asyncio.run(example_test_hook())
# %%
# 我们可以看到 "[instance-pre-reply]" 和 "[cls-pre-reply]" 被添加到了消息内容中。
#

View File

@@ -0,0 +1,408 @@
# -*- coding: utf-8 -*-
"""
.. _long-term-memory:
长期记忆
========================
AgentScope 为长期记忆提供了一个基类 ``LongTermMemoryBase`` 和一个基于 `mem0 <https://github.com/mem0ai/mem0>`_ 的具体实现 ``Mem0LongTermMemory``。
结合 :ref:`agent` 章节中 ``ReActAgent`` 类的设计,我们提供了两种长期记忆模式:
- ``agent_control``:智能体通过工具调用自主管理长期记忆。
- ``static_control``:开发者通过编程显式控制长期记忆操作。
当然,开发者也可以使用 ``both`` 参数,将同时激活上述两种记忆管理模式。
.. hint:: 不同的记忆模式适用于不同的使用场景,开发者可以根据需要选择合适的模式。
使用 mem0 长期记忆
~~~~~~~~~~~~~~~~~~~~~~~~
.. note:: 在 GitHub 仓库的 ``examples/long_term_memory/mem0`` 目录下我们提供了 mem0 长期记忆的使用示例。
"""
import os
import asyncio
from agentscope.message import Msg
from agentscope.memory import InMemoryMemory, Mem0LongTermMemory
from agentscope.agent import ReActAgent
from agentscope.embedding import DashScopeTextEmbedding
from agentscope.formatter import DashScopeChatFormatter
from agentscope.model import DashScopeChatModel
from agentscope.tool import Toolkit
# 创建 mem0 长期记忆实例
long_term_memory = Mem0LongTermMemory(
agent_name="Friday",
user_name="user_123",
model=DashScopeChatModel(
model_name="qwen-max-latest",
api_key=os.environ.get("DASHSCOPE_API_KEY"),
stream=False,
),
embedding_model=DashScopeTextEmbedding(
model_name="text-embedding-v2",
api_key=os.environ.get("DASHSCOPE_API_KEY"),
),
on_disk=False,
)
# %%
# ``Mem0LongTermMemory`` 类提供了两个操作长期记忆的方法,``record`` 和 ``retrieve``。
# 它们接收消息对象的列表作为输入,分别记录和检索长期记忆中的信息。
#
# 例如下面的例子中,我们先存入用户的一条偏好,然后在长期记忆中检索相关信息。
#
# 基本使用示例
async def basic_usage():
"""基本使用示例"""
# 记录记忆
await long_term_memory.record([Msg("user", "我喜欢住民宿", "user")])
# 检索记忆
results = await long_term_memory.retrieve(
[Msg("user", "我的住宿偏好", "user")],
)
print(f"检索结果: {results}")
asyncio.run(basic_usage())
# %%
# 与 ReAct 智能体集成
# ----------------------------------------
# AgentScope 中的 ``ReActAgent`` 在构造函数中包含 ``long_term_memory`` 和 ``long_term_memory_mode`` 两个参数,
# 其中 ``long_term_memory`` 用于指定长期记忆实例,``long_term_memory_mode`` 的取值为 ``"agent_control"``, ``"static_control"`` 或 ``"both"``。
#
# 当 ``long_term_memory_mode`` 设置为 ``"agent_control"`` 或 ``both`` 时,在 ``ReActAgent`` 的构造函数中将
# 注册两个工具函数:``record_to_memory`` 和 ``retrieve_from_memory``。
# 从而使智能体能够自主的管理长期记忆。
#
# .. note:: 为了达到最好的效果,``"agent_control"`` 模式可能还需要在系统提示system prompt中添加相应的说明。
#
# 创建带有长期记忆的 ReAct 智能体
agent = ReActAgent(
name="Friday",
sys_prompt="你是一个具有长期记忆功能的助手。",
model=DashScopeChatModel(
api_key=os.environ.get("DASHSCOPE_API_KEY"),
model_name="qwen-max-latest",
),
formatter=DashScopeChatFormatter(),
toolkit=Toolkit(),
memory=InMemoryMemory(),
long_term_memory=long_term_memory,
long_term_memory_mode="static_control", # 使用 static_control 模式
)
async def record_preferences():
"""ReAct agent integration example"""
# 对话示例
msg = Msg("user", "我去杭州旅行时,喜欢住民宿", "user")
await agent(msg)
asyncio.run(record_preferences())
# %%
# 然后我们清空智能体的短期记忆,以避免造成干扰,并测试智能体是否会记住之前的对话。
#
async def retrieve_preferences():
"""Retrieve user preferences from long-term memory"""
# 我们清空智能体的短期记忆,以避免造成干扰
await agent.memory.clear()
# 测试智能体是否会记住之前的对话
msg2 = Msg("user", "我有什么偏好?简要的回答我", "user")
await agent(msg2)
asyncio.run(retrieve_preferences())
# %%
# 使用 ReMe 个人长期记忆
# ~~~~~~~~~~~~~~~~~~~~~~~~
#
# .. note:: 在 GitHub 仓库的 ``examples/long_term_memory/reme`` 目录下我们提供了 ReMe 长期记忆的使用示例。
#
# .. code-block:: python
# :caption: 安装 ReMe 依赖
#
# from agentscope.memory import ReMePersonalLongTermMemory
#
# # 创建 ReMe 个人长期记忆实例
# reme_long_term_memory = ReMePersonalLongTermMemory(
# agent_name="Friday",
# user_name="user_123",
# model=DashScopeChatModel(
# model_name="qwen3-max",
# api_key=os.environ.get("DASHSCOPE_API_KEY"),
# stream=False,
# ),
# embedding_model=DashScopeTextEmbedding(
# model_name="text-embedding-v4",
# api_key=os.environ.get("DASHSCOPE_API_KEY"),
# dimensions=1024,
# ),
# )
#
#
# ``ReMePersonalLongTermMemory`` 类提供了四个操作长期记忆的方法。
# 它们分别是用于工具调用的 ``record_to_memory`` 和 ``retrieve_from_memory``
# 以及用于直接调用的 ``record`` 和 ``retrieve``。
#
# 例如下面的例子中,我们使用 ``record_to_memory`` 记录用户偏好。
#
# .. code-block:: python
# :caption: 创建 ReMe 个人长期记忆实例
#
# async def test_record_to_memory():
# """测试 record_to_memory 工具函数接口"""
# async with reme_long_term_memory:
# result = await reme_long_term_memory.record_to_memory(
# thinking="用户正在分享他们的旅行偏好和习惯",
# content=[
# "我去杭州旅行时喜欢住民宿",
# "我喜欢早上去西湖游玩",
# "我喜欢喝龙井茶",
# ],
# )
# # 提取结果文本
# result_text = " ".join(
# block.get("text", "")
# for block in result.content
# if block.get("type") == "text"
# )
# print(f"记录结果: {result_text}")
#
#
# 然后我们使用 ``retrieve_from_memory`` 检索相关记忆。
#
# .. code-block:: python
# :caption: 使用 retrieve_from_memory 检索记忆
#
# async def test_retrieve_from_memory():
# """测试 retrieve_from_memory 工具函数接口"""
# async with reme_long_term_memory:
# # 先记录一些内容
# await reme_long_term_memory.record_to_memory(
# thinking="用户正在分享旅行偏好",
# content=["我去杭州旅行时喜欢住民宿"],
# )
#
# # 然后检索
# result = await reme_long_term_memory.retrieve_from_memory(
# keywords=["杭州旅行", "茶偏好"],
# )
# retrieved_text = " ".join(
# block.get("text", "")
# for block in result.content
# if block.get("type") == "text"
# )
# print(f"检索到的记忆: {retrieved_text}")
#
#
# 除了工具函数接口,我们也可以使用 ``record`` 方法直接记录消息对话。
#
# .. code-block:: python
# :caption: 使用 record 直接记录消息
#
# async def test_record_direct():
# """测试 record 直接记录方法"""
# async with reme_long_term_memory:
# await reme_long_term_memory.record(
# msgs=[
# Msg(
# role="user",
# content="我是一名软件工程师,喜欢远程工作",
# name="user",
# ),
# Msg(
# role="assistant",
# content="明白了!您是一名重视远程工作灵活性的软件工程师。",
# name="assistant",
# ),
# Msg(
# role="user",
# content="我通常早上9点开始工作会先喝一杯咖啡",
# name="user",
# ),
# ],
# )
# print("成功记录了对话消息")
#
#
# 类似地,我们使用 ``retrieve`` 方法检索相关记忆。
#
# .. code-block:: python
# :caption: 使用 retrieve 直接检索消息
#
# async def test_retrieve_direct():
# """测试 retrieve 直接检索方法"""
# async with reme_long_term_memory:
# # 先记录一些内容
# await reme_long_term_memory.record(
# msgs=[
# Msg(
# role="user",
# content="我是一名软件工程师,喜欢远程工作",
# name="user",
# ),
# ],
# )
#
# # 然后检索
# memories = await reme_long_term_memory.retrieve(
# msg=Msg(
# role="user",
# content="你知道我的工作偏好吗?",
# name="user",
# ),
# )
# print(f"检索到的记忆: {memories if memories else '未找到相关记忆'}")
#
#
# 与 ReAct 智能体集成
# ----------------------------------------
# AgentScope 中的 ``ReActAgent`` 在构造函数中包含 ``long_term_memory`` 和 ``long_term_memory_mode`` 两个参数。
#
# 当 ``long_term_memory_mode`` 设置为 ``"agent_control"`` 或 ``both`` 时,在 ``ReActAgent`` 的构造函数中将
# 注册 ``record_to_memory`` 和 ``retrieve_from_memory`` 工具函数,使智能体能够自主的管理长期记忆。
#
# .. note:: 为了达到最好的效果,``"agent_control"`` 模式可能还需要在系统提示system prompt中添加相应的说明。
#
# .. code-block:: python
# :caption: 创建带有长期记忆的 ReAct 智能体
#
# # 创建带有长期记忆的 ReAct 智能体agent_control 模式)
# async def test_react_agent_with_reme():
# """测试 ReActAgent 与 ReMe 个人记忆的集成"""
# async with reme_long_term_memory:
# agent_with_reme = ReActAgent(
# name="Friday",
# sys_prompt=(
# "你是一个名为 Friday 的助手,具有长期记忆能力。"
# "\n\n## 记忆管理指南:\n"
# "1. **记录记忆**:当用户分享个人信息、偏好、习惯或关于自己的事实时,"
# "始终使用 `record_to_memory` 记录这些信息以供将来参考。\n"
# "\n2. **检索记忆**:在回答关于用户偏好、过去信息或个人详细信息的问题之前,"
# "你必须首先调用 `retrieve_from_memory` 来检查是否有任何相关的存储信息。"
# "不要仅依赖当前对话上下文。\n"
# "\n3. **何时检索**:在以下情况下调用 `retrieve_from_memory`\n"
# " - 用户问类似'我喜欢什么?'、'我的偏好是什么?'、"
# "'你对我了解多少?'的问题\n"
# " - 用户询问他们过去的行为、习惯或偏好\n"
# " - 用户提到他们之前提到的信息\n"
# " - 你需要关于用户的上下文来提供个性化的响应\n"
# "\n在声称不了解用户的某些信息之前始终先检查你的记忆。"
# ),
# model=DashScopeChatModel(
# model_name="qwen3-max",
# api_key=os.environ.get("DASHSCOPE_API_KEY"),
# stream=False,
# ),
# formatter=DashScopeChatFormatter(),
# toolkit=Toolkit(),
# memory=InMemoryMemory(),
# long_term_memory=reme_long_term_memory,
# long_term_memory_mode="agent_control", # 使用 agent_control 模式
# )
#
# # 用户分享偏好
# msg = Msg(
# role="user",
# content="我去杭州旅行时,喜欢住民宿",
# name="user",
# )
# response = await agent_with_reme(msg)
# print(f"智能体响应: {response.get_text_content()}")
#
# # 清空短期记忆以测试长期记忆
# await agent_with_reme.memory.clear()
#
# # 查询偏好
# msg2 = Msg(role="user", content="我有什么偏好?", name="user")
# response2 = await agent_with_reme(msg2)
# print(f"智能体响应: {response2.get_text_content()}")
#
#
# 然后我们清空智能体的短期记忆,以避免造成干扰,并测试智能体是否会记住之前的对话。
#
# .. code-block:: python
# :caption: 测试 ReAct 智能体是否记住偏好
#
# async def retrieve_reme_preferences():
# """从长期记忆中检索用户偏好"""
# async with reme_long_term_memory:
# # 创建智能体(这里可以复用之前创建的智能体,为了示例完整性重新创建)
# agent_with_reme = ReActAgent(
# name="Friday",
# sys_prompt="你是一个具有长期记忆功能的助手。",
# model=DashScopeChatModel(
# api_key=os.environ.get("DASHSCOPE_API_KEY"),
# model_name="qwen3-max",
# stream=False,
# ),
# formatter=DashScopeChatFormatter(),
# toolkit=Toolkit(),
# memory=InMemoryMemory(),
# long_term_memory=reme_long_term_memory,
# long_term_memory_mode="agent_control",
# )
#
# # 我们清空智能体的短期记忆,以避免造成干扰
# await agent_with_reme.memory.clear()
#
# # 测试智能体是否会记住之前的对话
# msg2 = Msg("user", "我有什么偏好?简要的回答我", "user")
# await agent_with_reme(msg2)
#
#
# 自定义长期记忆
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope 提供了 ``LongTermMemoryBase`` 基类,它定义了长期记忆的基本接口。
#
# 开发者可以继承 ``LongTermMemoryBase`` 并实现以下的抽象方法来定义自己的长期记忆类:
#
# .. list-table:: AgentScope 中的长期记忆类
# :header-rows: 1
#
# * - 类
# - 抽象方法
# - 描述
# * - ``LongTermMemoryBase``
# - | ``record``
# | ``retrieve``
# | ``record_to_memory``
# | ``retrieve_from_memory``
# - - 如果想支持 "static_control" 模式,必须实现 ``record`` 和 ``retrieve`` 方法。
# - 想要支持 "agent_control" 模式,必须实现 ``record_to_memory`` 和 ``retrieve_from_memory`` 方法。
# * - ``Mem0LongTermMemory``
# - | ``record``
# | ``retrieve``
# | ``record_to_memory``
# | ``retrieve_from_memory``
# - 基于 mem0 库的长期记忆实现,支持向量存储和检索。
# * - ``ReMePersonalLongTermMemory``
# - | ``record``
# | ``retrieve``
# | ``record_to_memory``
# | ``retrieve_from_memory``
# - 基于 ReMe 框架的个人记忆实现,提供强大的记忆管理和检索功能。
#
#
# 进一步阅读
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# - :ref:`memory` - 基础记忆系统
# - :ref:`agent` - ReAct 智能体
# - :ref:`tool` - 工具系统

View File

@@ -0,0 +1,203 @@
# -*- coding: utf-8 -*-
"""
.. _mcp:
MCP
=========================
本章将介绍 AgentScope 对 MCPModel Context Protocol的以下支持
- 支持 **HTTP** StreamableHTTP 和 SSE和 **StdIO** 类型的 MCP 服务器
- 提供 **有状态** 和 **无状态** 两种 MCP 客户端
- 提供 **MCP 级别** 和 **函数级别** 的 MCP 工具管理
这里的有状态/无状态是指客户端是否会维持与 MCP 服务器的会话session
无状态客户端只会在调用工具发生时建立会话,并在工具调用结束后立即销毁会话,是一种轻量化的使用方式。
下表总结了支持的 MCP 客户端类型和协议:
.. list-table:: 支持的 MCP 客户端类型和协议
:header-rows: 1
* - 客户端类型
- HTTPStreamableHTTP 和 SSE
- StdIO
* - 有状态客户端
- ``HttpStatefulClient``
- ``StdIOStatefulClient``
* - 无状态客户端
- ``HttpStatelessClient``
-
"""
import asyncio
import json
import os
from agentscope.mcp import HttpStatefulClient, HttpStatelessClient
from agentscope.tool import Toolkit
# %%
# MCP 客户端
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# 在 AgentScope 中MCP 客户端负责
#
# - 连接到 MCP 服务器,
# - 从服务器获取工具函数,以及
# - 调用 MCP 服务器中的工具函数。
#
# AgentScope 中有两种类型的 MCP 客户端:**有状态** 和 **无状态**。
# 它们仅在 **如何管理与 MCP 服务器的会话** 方面有所不同。
#
# - 有状态客户端:有状态 MCP 客户端在其生命周期内 **维持与 MCP 服务器的持久会话**。开发者应显式调用 ``connect()`` 和 ``close()`` 方法来管理会话的生命周期。
# - 无状态客户端:无状态 MCP 客户端在调用工具函数时创建新会话,在工具函数调用完成后立即销毁会话,更加轻量化。
#
# .. note:: - StdIO MCP 服务器只有有状态客户端,当调用 ``connect()`` 时,它将在本地启动 MCP 服务器然后连接到它。
# - 对于有状态客户端,开发者必须确保在调用工具函数时客户端已连接。
# - 当有多个 `HttpStatefulClient` 或 `StdIOStatefulClient` 建立连接时,应按照后进先出 (LIFO) 的顺序关闭它们以避免引发错误。
#
# 以高德地图 MCP 服务器为例,有状态和无状态客户端的创建非常相似:
#
stateful_client = HttpStatefulClient(
# 用于标识 MCP 的名称
name="mcp_services_stateful",
transport="streamable_http",
url=f"https://mcp.amap.com/mcp?key={os.environ['GAODE_API_KEY']}",
)
stateless_client = HttpStatelessClient(
# 用于标识 MCP 的名称
name="mcp_services_stateless",
transport="streamable_http",
url=f"https://mcp.amap.com/mcp?key={os.environ['GAODE_API_KEY']}",
)
# %%
# 有状态和无状态客户端都提供以下方法:
#
# .. list-table:: MCP 客户端方法
# :header-rows: 1
#
# * - 方法
# - 描述
# * - ``list_tools``
# - 列出 MCP 服务器中所有可用的工具。
# * - ``get_callable_function``
# - 通过名称从 MCP 服务器获取可调用的函数对象。
#
# MCP 作为工具
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope 提供了对 MCP 工具的细粒度管理,包括 MCP 级别和函数级别的管理。
#
# MCP 级别管理
# --------------------------------
# 您可以将 MCP 服务器的所有工具一次性注册到 ``Toolkit`` 中,如下所示。
#
# .. tip:: 可选地,开发者可以通过指定组名来管理工具。有关分组工具管理,请参考 :ref:`tool` 部分。
#
toolkit = Toolkit()
async def example_register_stateless_mcp() -> None:
"""注册无状态客户端 MCP 工具的示例。"""
# 从 MCP 服务器注册所有工具
await toolkit.register_mcp_client(
stateless_client,
# group_name="map_services", # 可选的组名
)
print("注册的 MCP 工具总数:", len(toolkit.get_json_schemas()))
maps_geo = next(
tool
for tool in toolkit.get_json_schemas()
if tool["function"]["name"] == "maps_geo"
)
print("\n示例 ``maps_geo`` 函数:")
print(
json.dumps(
maps_geo,
indent=4,
ensure_ascii=False,
),
)
asyncio.run(example_register_stateless_mcp())
# %%
# 要移除已注册的工具,可以使用 ``remove_tool_function`` 函数,或使用 ``remove_mcp_clients`` 移除特定 MCP 的所有工具。
#
async def example_remove_mcp_tools() -> None:
"""移除 MCP 工具的示例。"""
print("移除前的工具总数:", len(toolkit.get_json_schemas()))
# 通过名称移除特定的工具函数
toolkit.remove_tool_function("maps_geo")
print("工具数量:", len(toolkit.get_json_schemas()))
# 通过名称移除 MCP 客户端的所有工具
await toolkit.remove_mcp_clients(client_names=["mcp_services_stateless"])
print("工具数量:", len(toolkit.get_json_schemas()))
asyncio.run(example_remove_mcp_tools())
# %%
# 函数级别管理
# --------------------------------
# 注意到开发者有对 MCP 工具进行更细粒度控制的需求,例如对工具结果进行后处理,或使用它们创建更复杂的工具函数。
#
# 因此AgentScope 支持通过工具名从 MCP 客户端获取可调用的函数对象,这样开发者可以
#
# - 直接调用它,
# - 将其包装到自己的函数中,或以任何其它方式进行使用。
#
# 此外,开发者可以指定是否将工具函数执行结果包装成 ``ToolResponse`` 对象,以便与 ``Toolkit`` 无缝使用。
# 如果设置 ``wrap_tool_result=False``,将返回原始结果类型 ``mcp.types.CallToolResult``。
#
# 以 ``maps_geo`` 函数为例,可以将其获取为可调用的函数对象,如下所示:
#
async def example_function_level_usage() -> None:
"""使用函数级别 MCP 工具的示例。"""
func_obj = await stateless_client.get_callable_function(
func_name="maps_geo",
# 是否将工具结果包装到 AgentScope 的 ToolResponse 中
wrap_tool_result=True,
)
# 您可以获取其名称、描述和 JSON schema
print("函数名称:", func_obj.name)
print("函数描述:", func_obj.description)
print(
"函数 JSON schema",
json.dumps(func_obj.json_schema, indent=4, ensure_ascii=False),
)
# 直接调用函数对象
res = await func_obj(
address="天安门广场",
city="北京",
)
print("\n函数调用结果:")
print(res)
asyncio.run(example_function_level_usage())
# %%
# 进一步阅读
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# 有关更多详细信息,请参见:
#
# - :ref:`tool`
# - :ref:`agent`
#

View File

@@ -0,0 +1,433 @@
# -*- coding: utf-8 -*-
"""
.. _memory:
记忆
========================
AgentScope 中的记忆模块负责:
- 存储消息对象(``Msg``
- 利用标记mark管理消息
**标记** 是与记忆中每条消息关联的字符串标签,可用于根据消息的上下文或目的对消息进行分类、过滤和检索。
可用于实现进阶的记忆管理功能,例如在 `ReActAgent` 类中,使用``"hint"``标签标记一次性的提示消息,
以便在使用完成后将其从记忆中删除。
.. note:: AgentScope 中的记忆模块仅提供消息存储和管理的原子功能,记忆压缩等算法逻辑在 `智能体 <agent>`_ 中实现。
目前AgentScope 提供以下记忆存储实现:
.. list-table:: AgentScope 中的内置记忆类
:header-rows: 1
* - 类
- 描述
* - ``InMemoryMemory``
- 简单的内存记忆存储实现。
* - ``AsyncSQLAlchemyMemory``
- 基于异步 SQLAlchemy 的记忆存储实现,支持如 SQLite、PostgreSQL、MySQL 等多种关系数据库。
* - ``RedisMemory``
- 基于 Redis 的记忆存储实现。
.. tip:: 如果您有兴趣贡献新的记忆存储实现,请参考 `贡献指南 <https://github.com/agentscope-ai/agentscope/blob/main/CONTRIBUTING.md#types-of-contributions>`_。
以上所有记忆类均继承自基类 ``MemoryBase``,并提供以下方法来管理记忆中的消息:
.. list-table:: 记忆类提供的方法
:header-rows: 1
* - 方法
- 描述
* - ``add(
memories: Msg | list[Msg] | None,
marks: str | list[str] | None = None,
) -> None``
- 将 ``Msg`` 对象添加到记忆存储中,并使用给定的标记(如果提供)。
* - ``delete(msg_ids: list[str]) -> int``
- 通过ID从记忆存储中删除消息。
* - ``delete_by_mark(mark: str | list[str]) -> int``
- 通过标记从记忆中删除消息。
* - ``size() -> int``
- 获取记忆存储的大小。
* - ``clear() -> None``
- 清空记忆存储。
* - ``get_memory(
mark: str | None = None,
exclude_mark: str | None = None,
) -> list[Msg]``
- 通过标记从记忆中获取消息(如果提供)。否则,获取所有消息。如果使用 ``update_compressed_summary`` 方法存储压缩摘要,它将附加到返回消息的头部。
* - ``update_messages_mark(
new_mark: str | None,
old_mark: str | None = None,
msg_ids: list[str] | None = None,
) -> int``
- 统一的方法,用于更新存储中消息的标记(添加、删除或更改标记)。
* - ``update_compressed_summary(
summary: str,
) -> None``
- 更新存储在记忆中的摘要属性。
"""
import asyncio
import json
import fakeredis
from sqlalchemy.ext.asyncio import create_async_engine
from agentscope.memory import (
InMemoryMemory,
AsyncSQLAlchemyMemory,
RedisMemory,
)
from agentscope.message import Msg
# %%
# 内存记忆
# ~~~~~~~~~~~~~~~~~~~~~~~~
#
# 内存记忆提供了一种在内存中存储消息的简单方式。
# 结合 :ref:`state` 模块,它可以在不同用户和会话之间持久化记忆内容。
#
async def in_memory_example():
"""使用InMemoryMemory在内存中存储消息的示例。"""
memory = InMemoryMemory()
await memory.add(
Msg("Alice", "生成一份关于AgentScope的报告", "user"),
)
# 添加一条带有标记"hint"的提示消息
await memory.add(
[
Msg(
"system",
"<system-hint>首先创建一个计划来收集信息,然后逐步生成报告。</system-hint>",
"system",
),
],
marks="hint",
)
msgs = await memory.get_memory(mark="hint")
print("带有标记'hint'的消息:")
for msg in msgs:
print(f"- {msg}")
# 所有存储的消息都可以通过 ``state_dict`` 和 ``load_state_dict`` 方法导出和加载。
state = memory.state_dict()
print("记忆的状态字典:")
print(json.dumps(state, indent=2, ensure_ascii=False))
# 通过标记删除消息
deleted_count = await memory.delete_by_mark("hint")
print(f"删除了 {deleted_count} 条带有标记'hint'的消息。")
print("删除后的记忆状态字典:")
state = memory.state_dict()
print(json.dumps(state, indent=2, ensure_ascii=False))
asyncio.run(in_memory_example())
# %%
# 关系数据库记忆Relational Database Memory
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope 通过 SQLAlchemy 提供统一的接口来使用关系数据库,支持:
#
# - 多种数据库,如 SQLite、PostgreSQL、MySQL 等
# - 用户和会话管理
# - 生产环境中的连接池
#
# 具体来说这里我们以SQLite支持的记忆为例。
async def sqlalchemy_example() -> None:
"""使用 AsyncSQLAlchemyMemory 在 SQLite 数据库中存储消息的示例。"""
# 首先创建一个异步 SQLAlchemy 引擎
engine = create_async_engine("sqlite+aiosqlite:///./test_memory.db")
# 然后使用该引擎创建记忆
memory = AsyncSQLAlchemyMemory(
engine_or_session=engine,
# 可选传入指定user_id和session_id
user_id="user_1",
session_id="session_1",
)
await memory.add(
Msg("Alice", "生成一份关于AgentScope的报告", "user"),
)
await memory.add(
[
Msg(
"system",
"<system-hint>首先创建一个计划来收集信息,然后逐步生成报告。</system-hint>",
"system",
),
],
marks="hint",
)
msgs = await memory.get_memory(mark="hint")
print("带有标记'hint'的消息:")
for msg in msgs:
print(f"- {msg}")
# 完成后关闭引擎
await memory.close()
asyncio.run(sqlalchemy_example())
# %%
# 可选地,您也可以将 ``AsyncSQLAlchemyMemory`` 用作异步上下文管理器,退出上下文时会话将自动关闭。
async def sqlalchemy_context_example() -> None:
"""使用 AsyncSQLAlchemyMemory 作为异步上下文管理器的示例。"""
engine = create_async_engine("sqlite+aiosqlite:///./test_memory.db")
async with AsyncSQLAlchemyMemory(
engine_or_session=engine,
user_id="user_1",
session_id="session_1",
) as memory:
await memory.add(
Msg("Alice", "生成一份关于 AgentScope 的报告", "user"),
)
msgs = await memory.get_memory()
print("记忆中的所有消息:")
for msg in msgs:
print(f"- {msg}")
asyncio.run(sqlalchemy_context_example())
# %%
# 在生产环境中例如使用FastAPI时可以按如下方式启用连接池
#
# .. code-block:: python
# :caption: FastAPI中使用连接池的SQLAlchemy记忆
#
# from typing import AsyncGenerator
#
# from fastapi import FastAPI, Depends
# from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
#
# from agentscope.agent import ReActAgent
# from agentscope.pipeline import stream_printing_messages
#
#
# app = FastAPI()
#
# # 创建带连接池的异步SQLAlchemy引擎
# engine = create_async_engine(
# "sqlite+aiosqlite:///./test_memory.db",
# pool_size=10,
# max_overflow=20,
# pool_timeout=30,
# # ... 其他连接池设置
# )
#
# # 创建会话制造器
# async_session_marker = async_sessionmaker(
# engine,
# expire_on_commit=False,
# autocommit=False,
# autoflush=False,
# )
#
# async def get_db() -> AsyncGenerator[AsyncSession, None]:
# async with async_session_marker() as session:
# try:
# yield session
# await session.commit()
# except Exception:
# await session.rollback()
# raise
# finally:
# await session.close()
#
# @app.post("/chat")
# async def chat_endpoint(
# user_id: str,
# session_id: str,
# input: str,
# db_session: AsyncSession = Depends(get_db),
# ):
# # 智能体的一些设置
# ...
#
# # 使用SQLAlchemy记忆创建智能体
# agent = ReActAgent(
# # ...
# memory=AsyncSQLAlchemyMemory(
# engine_or_session=db_session,
# user_id=user_id,
# session_id=session_id,
# ),
# )
#
# # 处理与智能体的对话
# async for msg, _ in stream_printing_messages(
# agents=[agent],
# coroutine_task=agent(Msg("user", input, "user")),
# ):
# # 将消息返回给客户端
# ...
#
#
# NoSQL数据库记忆NoSQL Database Memory
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope还提供基于NoSQL数据库如Redis的记忆实现。
# 它也支持用户和会话管理,以及生产环境中的连接池。
#
# 首先我们可以按如下方式初始化Redis记忆
async def redis_memory_example() -> None:
"""使用 RedisMemory 在 Redis 中存储消息的示例。"""
# 使用fakeredis进行内存测试无需真实的 Redis 服务器
fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True)
# 创建 Redis 记忆
memory = RedisMemory(
# 使用fake redis进行演示
connection_pool=fake_redis.connection_pool,
# 也可以通过指定主机和端口连接到真实的Redis服务器
# host="localhost",
# port=6379,
# 可选地指定 user_id 和 session_id
user_id="user_1",
session_id="session_1",
)
# 向记忆中添加消息
await memory.add(
Msg(
"Alice",
"生成一份关于AgentScope的报告",
"user",
),
)
# 添加一条带有标记"hint"的提示消息
await memory.add(
Msg(
"system",
"<system-hint>首先创建一个计划来收集信息,然后逐步生成报告。</system-hint>",
"system",
),
marks="hint",
)
# 检索带有标记"hint"的消息
msgs = await memory.get_memory(mark="hint")
print("带有标记'hint'的消息:")
for msg in msgs:
print(f"- {msg}")
asyncio.run(redis_memory_example())
# %%
# 同样,`RedisMemory` 也可以在生产环境中使用连接池例如与FastAPI一起使用。
#
# .. code-block:: python
# :caption: FastAPI中使用连接池的Redis记忆
#
# from fastapi import FastAPI, HTTPException
# from redis.asyncio import ConnectionPool
# from contextlib import asynccontextmanager
#
# # 全局Redis连接池
# redis_pool: ConnectionPool | None = None
#
#
# # 使用lifespan事件管理Redis连接池
# @asynccontextmanager
# async def lifespan(app: FastAPI):
# global redis_pool
# redis_pool = ConnectionPool(
# host="localhost",
# port=6379,
# db=0,
# password=None,
# decode_responses=True,
# max_connections=10,
# encoding="utf-8",
# )
# print("✅ Redis连接已建立")
#
# yield
#
# await redis_pool.disconnect()
# print("✅ Redis连接已关闭")
#
#
# app = FastAPI(lifespan=lifespan)
#
#
# @app.post("/chat_endpoint")
# async def chat_endpoint(
# user_id: str, session_id: str, input: str
# ):
# """聊天端点"""
# global redis_pool
# if redis_pool is None:
# raise HTTPException(
# status_code=500,
# detail="Redis连接池未初始化。",
# )
#
# # 创建Redis记忆
# memory = RedisMemory(
# connection_pool=redis_pool,
# user_id=user_id,
# session_id=session_id,
# )
#
# ...
#
# # 完成后关闭Redis客户端连接
# client = memory.get_client()
# await client.aclose()
#
#
#
# 自定义记忆Customizing Memory
# ~~~~~~~~~~~~~~~~~~~~~~~~
#
# 要自定义您自己的记忆实现,只需从 ``MemoryBase`` 继承并实现以下方法:
#
# .. list-table::
# :header-rows: 1
#
# * - 方法
# - 描述
# * - ``add``
# - 向记忆中添加 ``Msg`` 对象
# * - ``delete``
# - 从记忆中删除 ``Msg`` 对象
# * - ``delete_by_mark``
# - 通过标记从记忆中删除 ``Msg`` 对象
# * - ``size``
# - 记忆的大小
# * - ``clear``
# - 清空记忆内容
# * - ``get_memory``
# - 以 ``Msg`` 对象列表的形式获取记忆内容
# * - ``update_messages_mark``
# - 更新记忆中消息的标记
# * - ``state_dict``
# - 获取记忆的状态字典
# * - ``load_state_dict``
# - 加载记忆的状态字典
#
# 延伸阅读
# ~~~~~~~~~~~~~~~~~~~~~~~~
# - :ref:`agent`
# - :ref:`long-term-memory`

View File

@@ -0,0 +1,403 @@
# -*- coding: utf-8 -*-
"""
.. _middleware:
中间件
===========================
AgentScope 提供了灵活的中间件系统,允许开发者拦截和修改各种操作的执行。
目前,中间件支持已在 ``Toolkit`` 类中实现,用于**工具执行**。
中间件系统遵循**洋葱模型**,每个中间件包裹在前一个中间件之外,形成层次结构。
这使得开发者可以:
- 在操作前进行**预处理**
- 在执行过程中**拦截和修改**响应
- 在操作完成后进行**后处理**
- 根据条件**跳过**操作执行
.. tip:: 未来版本的 AgentScope 将扩展中间件支持到其他组件,如智能体和模型。
"""
import asyncio
from typing import AsyncGenerator, Callable
from agentscope.message import TextBlock, ToolUseBlock
from agentscope.tool import ToolResponse, Toolkit
# %%
# 工具执行中间件
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# ``Toolkit`` 类通过 ``register_middleware`` 方法支持工具执行的中间件。
# 每个中间件可以拦截工具调用并修改输入或输出。
#
# 中间件签名
# ------------------------------
#
# 中间件函数应具有以下签名:
#
# .. code-block:: python
#
# async def middleware(
# kwargs: dict,
# next_handler: Callable,
# ) -> AsyncGenerator[ToolResponse, None]:
# # 从 kwargs 访问参数
# tool_call = kwargs["tool_call"]
#
# # 预处理
# # ...
#
# # 调用下一个中间件或工具函数
# async for response in await next_handler(**kwargs):
# # 后处理
# yield response
#
# .. list-table:: 中间件参数
# :header-rows: 1
#
# * - 参数
# - 类型
# - 描述
# * - ``kwargs``
# - ``dict``
# - 上下文参数。当前包含 ``tool_call`` (ToolUseBlock)。未来版本可能包含更多参数。
# * - ``next_handler``
# - ``Callable``
# - 一个可调用对象,接受 kwargs dict 并返回产生 AsyncGenerator[ToolResponse] 的协程
# * - **返回值**
# - ``AsyncGenerator[ToolResponse, None]``
# - 产生 ToolResponse 对象的异步生成器
#
# 基本示例
# ------------------------------
#
# 以下是一个记录工具调用的简单中间件:
#
async def logging_middleware(
kwargs: dict,
next_handler: Callable,
) -> AsyncGenerator[ToolResponse, None]:
"""记录工具执行的中间件。"""
# 从 kwargs 访问工具调用
tool_call = kwargs["tool_call"]
# 预处理:在工具执行前记录日志
print(f"[中间件] 调用工具:{tool_call['name']}")
print(f"[中间件] 输入:{tool_call['input']}")
# 调用下一个处理器(另一个中间件或实际工具)
async for response in await next_handler(**kwargs):
# 后处理:记录响应
print(f"[中间件] 响应:{response.content[0]['text']}")
yield response
# 在所有响应产生后执行
print(f"[中间件] 工具 {tool_call['name']} 完成")
# %%
# 让我们将这个中间件注册到工具包并测试它:
#
async def search_tool(query: str) -> ToolResponse:
"""一个简单的搜索工具。
Args:
query (`str`):
搜索查询。
Returns:
`ToolResponse`:
搜索结果。
"""
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"'{query}' 的搜索结果",
),
],
)
async def example_logging_middleware() -> None:
"""使用日志中间件的示例。"""
# 创建工具包并注册工具
toolkit = Toolkit()
toolkit.register_tool_function(search_tool)
# 注册中间件
toolkit.register_middleware(logging_middleware)
# 调用工具
result = await toolkit.call_tool_function(
ToolUseBlock(
type="tool_use",
id="1",
name="search_tool",
input={"query": "AgentScope"},
),
)
async for response in result:
print(f"\n[最终] {response.content[0]['text']}\n")
print("=" * 60)
print("示例 1日志中间件")
print("=" * 60)
asyncio.run(example_logging_middleware())
# %%
# 修改输入和输出
# ------------------------------
#
# 中间件还可以修改工具调用的输入和响应内容:
#
async def transform_middleware(
kwargs: dict,
next_handler: Callable,
) -> AsyncGenerator[ToolResponse, None]:
"""转换输入和输出的中间件。"""
# 从 kwargs 访问工具调用
tool_call = kwargs["tool_call"]
# 预处理:修改输入
original_query = tool_call["input"]["query"]
tool_call["input"]["query"] = f"[已转换] {original_query}"
async for response in await next_handler(**kwargs):
# 后处理:修改响应
original_text = response.content[0]["text"]
response.content[0]["text"] = f"{original_text} [已修改]"
yield response
async def example_transform_middleware() -> None:
"""转换中间件的示例。"""
toolkit = Toolkit()
toolkit.register_tool_function(search_tool)
toolkit.register_middleware(transform_middleware)
result = await toolkit.call_tool_function(
ToolUseBlock(
type="tool_use",
id="2",
name="search_tool",
input={"query": "中间件"},
),
)
async for response in result:
print(f"结果:{response.content[0]['text']}")
print("\n" + "=" * 60)
print("示例 2转换中间件")
print("=" * 60)
asyncio.run(example_transform_middleware())
# %%
# 授权中间件
# ------------------------------
#
# 可以使用中间件实现授权检查,如果未授权则跳过工具执行:
#
async def authorization_middleware(
kwargs: dict,
next_handler: Callable,
) -> AsyncGenerator[ToolResponse, None]:
"""检查授权的中间件。"""
# 从 kwargs 访问工具调用
tool_call = kwargs["tool_call"]
# 检查工具是否已授权(简单示例)
authorized_tools = {"search_tool"}
if tool_call["name"] not in authorized_tools:
# 跳过执行并直接返回错误
print(f"[授权] 工具 {tool_call['name']} 未授权")
yield ToolResponse(
content=[
TextBlock(
type="text",
text=f"错误:工具 '{tool_call['name']}' 未授权",
),
],
)
return
# 工具已授权,继续执行
print(f"[授权] 工具 {tool_call['name']} 已授权")
async for response in await next_handler(**kwargs):
yield response
async def unauthorized_tool(data: str) -> ToolResponse:
"""一个未授权的工具。
Args:
data (`str`):
一些数据。
Returns:
`ToolResponse`:
结果。
"""
return ToolResponse(
content=[TextBlock(type="text", text=f"处理 {data}")],
)
async def example_authorization_middleware() -> None:
"""授权中间件的示例。"""
toolkit = Toolkit()
toolkit.register_tool_function(search_tool)
toolkit.register_tool_function(unauthorized_tool)
toolkit.register_middleware(authorization_middleware)
# 尝试授权的工具
print("\n调用已授权的工具:")
result = await toolkit.call_tool_function(
ToolUseBlock(
type="tool_use",
id="3",
name="search_tool",
input={"query": "测试"},
),
)
async for response in result:
print(f"结果:{response.content[0]['text']}")
# 尝试未授权的工具
print("\n调用未授权的工具:")
result = await toolkit.call_tool_function(
ToolUseBlock(
type="tool_use",
id="4",
name="unauthorized_tool",
input={"data": "测试"},
),
)
async for response in result:
print(f"结果:{response.content[0]['text']}")
print("\n" + "=" * 60)
print("示例 3授权中间件")
print("=" * 60)
asyncio.run(example_authorization_middleware())
# %%
# 多个中间件(洋葱模型)
# ------------------------------
#
# 当注册多个中间件时,它们形成类似洋葱的结构。
# 执行顺序遵循洋葱模型:
#
# - **预处理**:按照中间件注册的顺序执行
# - **后处理**:按相反顺序执行(从内到外)
#
# 这是因为实际的工具响应对象会通过中间件链传递,
# 每个中间件都会原地修改它。
#
async def middleware_1(
kwargs: dict,
next_handler: Callable,
) -> AsyncGenerator[ToolResponse, None]:
"""第一个中间件。"""
# 从 kwargs 访问工具调用
tool_call = kwargs["tool_call"]
# 预处理
print("[M1] 预处理")
tool_call["input"]["query"] += " [M1]"
async for response in await next_handler(**kwargs):
# 后处理
response.content[0]["text"] += " [M1]"
print("[M1] 后处理")
yield response
async def middleware_2(
kwargs: dict,
next_handler: Callable,
) -> AsyncGenerator[ToolResponse, None]:
"""第二个中间件。"""
# 从 kwargs 访问工具调用
tool_call = kwargs["tool_call"]
# 预处理
print("[M2] 预处理")
tool_call["input"]["query"] += " [M2]"
async for response in await next_handler(**kwargs):
# 后处理
response.content[0]["text"] += " [M2]"
print("[M2] 后处理")
yield response
async def example_multiple_middleware() -> None:
"""多个中间件的示例。"""
toolkit = Toolkit()
toolkit.register_tool_function(search_tool)
# 按顺序注册中间件
toolkit.register_middleware(middleware_1)
toolkit.register_middleware(middleware_2)
result = await toolkit.call_tool_function(
ToolUseBlock(
type="tool_use",
id="5",
name="search_tool",
input={"query": "测试"},
),
)
async for response in result:
print(f"\n最终结果:{response.content[0]['text']}")
print("\n" + "=" * 60)
print("示例 4多个中间件洋葱模型")
print("=" * 60)
print("\n执行流程:")
print("M1 预处理 → M2 预处理 → 工具 → M2 后处理 → M1 后处理")
print()
asyncio.run(example_multiple_middleware())
# %%
# 使用场景
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# 中间件系统适用于各种场景:
#
# - **日志和监控**:跟踪工具使用情况和性能
# - **授权**:控制对特定工具的访问
# - **速率限制**:限制工具调用的频率
# - **缓存**:缓存重复调用的工具响应
# - **错误处理**:添加重试逻辑或优雅降级
# - **输入验证**:验证和清理工具输入
# - **输出转换**:格式化或过滤工具输出
# - **指标收集**:收集有关工具使用情况的统计信息
#
# .. note::
# - 中间件按注册顺序应用
# - 同一个 ``ToolResponse`` 对象通过中间件链传递并原地修改
# - 中间件可以通过不调用 ``next_handler`` 来完全跳过工具执行
# - 所有中间件必须是产生 ``ToolResponse`` 对象的异步生成器函数

View File

@@ -0,0 +1,235 @@
# -*- coding: utf-8 -*-
"""
.. _model:
模型
====================
在本教程中,我们介绍 AgentScope 中集成的模型 API、如何使用它们以及如何集成新的模型 API。
AgentScope 目前支持的模型 API 和模型提供商包括:
.. list-table::
:header-rows: 1
* - API
- 类
- 兼容
- 流式
- 工具
- 视觉
- 推理
* - OpenAI
- ``OpenAIChatModel``
- vLLM, DeepSeek
- ✅
- ✅
- ✅
- ✅
* - DashScope
- ``DashScopeChatModel``
-
- ✅
- ✅
- ✅
- ✅
* - Anthropic
- ``AnthropicChatModel``
-
- ✅
- ✅
- ✅
- ✅
* - Gemini
- ``GeminiChatModel``
-
- ✅
- ✅
- ✅
- ✅
* - Ollama
- ``OllamaChatModel``
-
- ✅
- ✅
- ✅
- ✅
.. note:: 当使用 vLLM 时,需要在部署时为不同模型配置相应的工具调用参数,例如 ``--enable-auto-tool-choice``、``--tool-call-parser`` 等参数。更多详情请参考 `vLLM 官方文档 <https://docs.vllm.ai/en/latest/features/tool_calling.html>`_。
.. note:: 兼容 OpenAI API 的模型(例如 vLLM 部署的模型),推荐使用 ``OpenAIChatModel``,并通过 ``client_kwargs={"base_url": "http://your-api-endpoint"}`` 参数指定 API 端点。例如:
.. code-block:: python
OpenAIChatModel(client_kwargs={"base_url": "http://localhost:8000/v1"})
.. note:: 模型的行为参数(如温度、最大长度等)可以通过 ``generate_kwargs`` 参数在构造函数中提前设定。例如:
.. code-block:: python
OpenAIChatModel(generate_kwargs={"temperature": 0.3, "max_tokens": 1000})
为了提供统一的模型接口,上述所有类均被统一为:
- ``__call__`` 函数的前三个参数是 ``messages````tools`` 和 ``tool_choice``,分别是输入消息,工具函数的 JSON schema以及工具选择的模式。
- 非流式返回时,返回类型是 ``ChatResponse`` 实例;流式返回时,返回的是 ``ChatResponse`` 的异步生成器。
.. note:: 不同的模型 API 在输入消息格式上有所不同AgentScope 通过 formatter 模块处理消息的转换,请参考 :ref:`format`。
``ChatResponse`` 包含大模型生成的推理/文本/工具使用内容、身份、创建时间和使用信息。
"""
import asyncio
import json
import os
from agentscope.message import TextBlock, ToolUseBlock, ThinkingBlock, Msg
from agentscope.model import ChatResponse, DashScopeChatModel
response = ChatResponse(
content=[
ThinkingBlock(
type="thinking",
thinking="我应该在 Google 上搜索 AgentScope。",
),
TextBlock(type="text", text="我将在 Google 上搜索 AgentScope。"),
ToolUseBlock(
type="tool_use",
id="642n298gjna",
name="google_search",
input={"query": "AgentScope"},
),
],
)
print(response)
# %%
# 以 ``DashScopeChatModel`` 为例,调用和返回结果如下:
async def example_model_call() -> None:
"""使用 DashScopeChatModel 的示例。"""
model = DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
stream=False,
)
res = await model(
messages=[
{"role": "user", "content": "你好!"},
],
)
# 您可以直接使用响应内容创建 ``Msg`` 对象
msg_res = Msg("Friday", res.content, "assistant")
print("LLM 返回结果:", res)
print("作为 Msg 的响应:", msg_res)
asyncio.run(example_model_call())
# %%
# 流式返回
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 要启用流式返回,请在模型的构造函数中将 ``stream`` 参数设置为 ``True``。
# 流式返回中,``__call__`` 方法将返回一个 **异步生成器**,该生成器迭代返回 ``ChatResponse`` 实例。
#
# .. note:: AgentScope 中的流式返回结果为 **累加式**,这意味着每个 chunk 中的内容包含所有之前的内容加上新生成的内容。
#
async def example_streaming() -> None:
"""使用流式模型的示例。"""
model = DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
stream=True,
)
generator = await model(
messages=[
{
"role": "user",
"content": "从 1 数到 20只报告数字不要任何其他信息。",
},
],
)
print("响应的类型:", type(generator))
i = 0
async for chunk in generator:
print(f"{i}")
print(f"\t类型: {type(chunk.content)}")
print(f"\t{chunk}\n")
i += 1
asyncio.run(example_streaming())
# %%
# 推理模型
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope 通过提供 ``ThinkingBlock`` 来支持推理模型。
#
async def example_reasoning() -> None:
"""使用推理模型的示例。"""
model = DashScopeChatModel(
model_name="qwen-turbo",
api_key=os.environ["DASHSCOPE_API_KEY"],
enable_thinking=True,
)
res = await model(
messages=[
{"role": "user", "content": "我是谁?"},
],
)
last_chunk = None
async for chunk in res:
last_chunk = chunk
print("最终响应:")
print(last_chunk)
asyncio.run(example_reasoning())
# %%
# 工具 API
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 不同的模型提供商在工具 API 方面有所不同,例如工具 JSON schema、工具调用/响应格式。
# 为了提供统一的接口AgentScope 通过以下方式解决了这个问题:
#
# - 提供了统一的工具调用结构 block :ref:`ToolUseBlock <tool-block>` 和工具响应结构 :ref:`ToolResultBlock <tool-block>`。
# - 在模型类的 ``__call__`` 方法中提供统一的工具接口 ``tools``,接受工具 JSON schema 列表,如下所示:
#
json_schemas = [
{
"type": "function",
"function": {
"name": "google_search",
"description": "在 Google 上搜索查询。",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索查询。",
},
},
"required": ["query"],
},
},
},
]
# %%
# 进一步阅读
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# - :ref:`message`
# - :ref:`prompt`
#

View File

@@ -0,0 +1,283 @@
# -*- coding: utf-8 -*-
"""
.. _pipeline:
管道 (Pipeline)
========================
对于多智能体编排AgentScope 提供了 ``agentscope.pipeline`` 模块
作为将智能体链接在一起的语法糖,具体包括
- **MsgHub**: 用于多个智能体之间消息的广播
- **sequential_pipeline** 和 **SequentialPipeline**: 以顺序方式执行多个智能体的函数式和类式实现
- **fanout_pipeline** 和 **FanoutPipeline**: 将相同输入分发给多个智能体的函数式和类式实现
- **stream_printing_messages**: 将智能体在回复过程中,调用 ``self.print`` 打印的消息转换为一个异步生成器
"""
import os, asyncio
from agentscope.formatter import DashScopeMultiAgentFormatter
from agentscope.message import Msg
from agentscope.model import DashScopeChatModel
from agentscope.agent import ReActAgent
from agentscope.pipeline import MsgHub, stream_printing_messages
# %%
# 使用 MsgHub 进行广播
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# ``MsgHub`` 类是一个 **异步上下文管理器**,它接收一个智能体列表作为其参与者。
# 当一个参与者生成回复消息时,将通过调用所有其他参与者的 ``observe`` 方法广播该消息。
# 这意味着在 ``MsgHub`` 上下文中,开发者无需手动将回复消息从一个智能体发送到另一个智能体。
#
# 这里我们创建四个智能体Alice、Bob、Charlie 和 David。
# 然后我们让 Alice、Bob 和 Charlie 通过自我介绍开始一个会议。需要注意的是 David 没有包含在这个会议中。
#
def create_agent(name: str, age: int, career: str) -> ReActAgent:
"""根据给定信息创建智能体对象。"""
return ReActAgent(
name=name,
sys_prompt=f"你是{name},一个{age}岁的{career}",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
),
formatter=DashScopeMultiAgentFormatter(),
)
alice = create_agent("Alice", 50, "老师")
bob = create_agent("Bob", 35, "工程师")
charlie = create_agent("Charlie", 28, "设计师")
david = create_agent("David", 30, "开发者")
# %%
# 然后我们创建一个 ``MsgHub`` 上下文,并让他们自我介绍:
#
# .. hint:: ``announcement`` 中的消息将在进入 ``MsgHub`` 上下文时广播给所有参与者。
#
async def example_broadcast_message():
"""使用 MsgHub 广播消息的示例。"""
# 创建消息中心
async with MsgHub(
participants=[alice, bob, charlie],
announcement=Msg(
"user",
"现在请简要介绍一下自己,包括你的姓名、年龄和职业。",
"user",
),
) as hub:
# 无需手动消息传递的群聊
await alice()
await bob()
await charlie()
asyncio.run(example_broadcast_message())
# %%
# 现在让我们检查 Bob、Charlie 和 David 是否收到了 Alice 的消息。
#
async def check_broadcast_message():
"""检查消息是否正确广播。"""
user_msg = Msg(
"user",
"你知道 Alice 是谁吗,她是做什么的?",
"user",
)
await bob(user_msg)
await charlie(user_msg)
await david(user_msg)
asyncio.run(check_broadcast_message())
# %%
# 现在我们观察到 Bob 和 Charlie 知道 Alice 和她的职业,而 David 对
# Alice 一无所知,因为他没有包含在 ``MsgHub`` 上下文中。
#
#
# 动态管理
# ---------------------------
# 此外,``MsgHub`` 支持通过以下方法动态管理参与者:
#
# - ``add``: 添加一个或多个智能体作为新参与者
# - ``delete``: 从参与者中移除一个或多个智能体,他们将不再接收广播消息
# - ``broadcast``: 向所有当前参与者广播消息
#
# .. note:: 新添加的参与者不会接收到之前的消息。
#
# .. code-block:: python
#
# async with MsgHub(participants=[alice]) as hub:
# # 添加新参与者
# hub.add(david)
#
# # 移除参与者
# hub.delete(alice)
#
# # 向所有当前参与者广播
# await hub.broadcast(
# Msg("system", "现在我们开始...", "system"),
# )
#
#
# 管道
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 管道是 AgentScope 中多智能体编排的一种语法糖。
#
# 目前AgentScope 提供三种管道,用于减轻开发者的负担:
#
# 1. **顺序管道 (Sequential Pipeline)**: 按预定义顺序逐个执行智能体
# 2. **扇出管道 (Fanout Pipeline)**: 将相同输入分发给多个智能体并收集它们的响应
# 3. **流式获取打印消息 (stream printing messages)**: 将智能体在回复过程中,调用 ``self.print`` 打印的消息转换为一个异步生成器
#
# 顺序管道
# ------------------------
# 顺序管道逐个执行智能体,前一个智能体的输出成为下一个智能体的输入。
#
# 例如,以下两个代码片段是等价的:
#
# .. code-block:: python
# :caption: 代码片段 1: 手动消息传递
#
# msg = None
# msg = await alice(msg)
# msg = await bob(msg)
# msg = await charlie(msg)
# msg = await david(msg)
#
#
# .. code-block:: python
# :caption: 代码片段 2: 使用顺序管道
#
# from agentscope.pipeline import sequential_pipeline
#
# msg = await sequential_pipeline(
# # 按顺序执行的智能体列表
# agents=[alice, bob, charlie, david],
# # 第一个输入消息,可以是 None
# msg=None
# )
#
# %%
# 扇出管道
# ------------------------
# 扇出管道将相同的输入消息同时分发给多个智能体并收集所有响应。当你想要收集对同一话题的不同观点或专业意见时,这非常有用。
#
# 例如,以下两个代码片段是等价的:
#
# .. code-block:: python
# :caption: 代码片段 3: 手动逐个调用智能体
#
# from copy import deepcopy
#
# msgs = []
# msg = None
# for agent in [alice, bob, charlie, david]:
# msgs.append(await agent(deepcopy(msg)))
#
#
# .. code-block:: python
# :caption: 代码片段 4: 使用扇出管道
#
# from agentscope.pipeline import fanout_pipeline
#
# msgs = await fanout_pipeline(
# # 要执行的智能体列表
# agents=[alice, bob, charlie, david],
# # 输入消息,可以是 None
# msg=None,
# enable_gather=False,
# )
#
# .. note::
# ``enable_gather`` 参数控制扇出管道的执行模式:
#
# - ``enable_gather=True`` (默认): 使用 ``asyncio.gather()`` **并发** 执行所有智能体。这为 I/O 密集型操作(如 API 调用)提供更好的性能,因为智能体并行运行。
# - ``enable_gather=False``: 逐个 **顺序** 执行智能体。当你需要确定性的执行顺序或想要避免并发请求压垮外部服务时,这很有用。
#
# 选择并发执行以获得更好的性能,或选择顺序执行以获得可预测的顺序和资源控制。
#
# .. tip::
# 通过结合 ``MsgHub`` 和 ``sequential_pipeline`` 或 ``fanout_pipeline``,你可以非常容易地创建更复杂的工作流。
# %%
# 流式获取打印消息
# ------------------------
# ``stream_printing_messages`` 函数将智能体在回复过程中调用 ``self.print`` 打印的消息转换为一个异步生成器。
# 可以帮助开发者快速以流式方式获取智能体的中间消息。
#
# 该函数接受一个或多个智能体和一个协程任务作为输入,并返回一个异步生成器。
# 该异步生成器返回一个二元组,包含执行协程任务过程中通过 ``await self.print(...)`` 打印的消息,以及一个布尔值,表示该消息是否为一组流式消息中的最后一个。
#
# 需要注意的是,生成器返回的元组中,布尔值表示该消息是否为一组流式消息中的最后一个,而非此次智能体调用的最后一条消息。
async def run_example_pipeline() -> None:
"""运行流式打印消息的示例。"""
agent = create_agent("Alice", 20, "student")
# 我们关闭agent的终端打印以避免输出混乱
agent.set_console_output_enabled(False)
async for msg, last in stream_printing_messages(
agents=[agent],
coroutine_task=agent(
Msg("user", "你好,你是谁?", "user"),
),
):
print(msg, last)
if last:
print()
asyncio.run(run_example_pipeline())
# %%
# 高级管道特性
# ~~~~~~~~~~~~~~~~~~~~~~~~~
#
# 此外,为了可重用性,我们还提供了基于类的实现:
#
# .. code-block:: python
# :caption: 使用 SequentialPipeline 类
#
# from agentscope.pipeline import SequentialPipeline
#
# # 创建管道对象
# pipeline = SequentialPipeline(agents=[alice, bob, charlie, david])
#
# # 调用管道
# msg = await pipeline(msg=None)
#
# # 使用不同输入复用管道
# msg = await pipeline(msg=Msg("user", "你好!", "user"))
#
#
# .. code-block:: python
# :caption: 使用 FanoutPipeline 类
#
# from agentscope.pipeline import FanoutPipeline
#
# # 创建管道对象
# pipeline = FanoutPipeline(agents=[alice, bob, charlie, david])
#
# # 调用管道
# msgs = await pipeline(msg=None)
#
# # 使用不同输入复用管道
# msgs = await pipeline(msg=Msg("user", "你好!", "user"))
#

View File

@@ -0,0 +1,273 @@
# -*- coding: utf-8 -*-
"""
.. _plan:
计划
=========================
AgentScope 中的计划Plan模块使智能体能够正式地将复杂任务分解为可管理的子任务并系统地执行它们。主要功能包括
- 支持 **手动计划规范**
- 全面的计划管理功能:
- **创建、修改、放弃和恢复** 计划
- 在多个计划之间 **切换**
- 通过临时暂停计划来处理用户查询或紧急任务,**优雅地处理中断**
- 计划执行的 **实时可视化和监控**
.. note:: 当前计划模块仅支持子任务按照顺序执行。
具体来说,计划模块的工作原理是
- 提供计划管理的工具函数
- 插入提示消息来指导ReAct智能体完成计划
下图说明了计划模块如何与ReAct智能体协作
.. figure:: ../../_static/images/plan.png
:width: 90%
:alt: 计划模块
:class: bordered-image
:align: center
计划模块如何与ReAct智能体协作
"""
import asyncio
import os
from agentscope.agent import ReActAgent
from agentscope.formatter import DashScopeChatFormatter
from agentscope.model import DashScopeChatModel
from agentscope.plan import PlanNotebook, Plan, SubTask
# %%
# PlanNotebook
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# ``PlanNotebook`` 类是计划模块的核心,负责提供
#
# - 管理计划,子任务的工具函数
# - 提供用于“引导智能体正确完成任务”的提示消息Hint message
#
# ``PlanNotebook`` 类使用以下参数实例化:
#
# .. list-table:: ``PlanNotebook`` 构造函数的参数
# :header-rows: 1
#
# * - 名称
# - 类型
# - 描述
# * - ``max_subtasks``
# - ``int | None``
# - 计划中允许的子任务最大数量,如果为 ``None`` 则无限制
# * - ``plan_to_hint``
# - ``Callable[[Plan | None], str | None] | None``
# - 基于当前计划的完成情况,生成对应提示消息的函数。如果未提供,将使用默认的 ``DefaultPlanToHint`` 对象。
# * - ``storage``
# - ``PlanStorageBase | None``
# - 计划的存储模块用于恢复保存历史计划。如果未提供将使用默认的内存In-memory存储。
#
# ``plan_to_hint`` 参数是 ``PlanNotebook`` 类的核心参数,也是开发者进行提示工程的接口。
# 作为可调用对象,接受当前计划作为输入,并返回一个字符串类型的提示消息。
# AgentScope 构建了一个默认的 ``DefaultPlanToHint`` 类,可以直接使用,同时我们鼓励开发者提供自己的 ``plan_to_hint`` 函数以获得更好的性能。
#
# ``storage`` 用于存储历史计划,允许智能体检索和恢复历史计划。
# 我们同样鼓励开发者通过继承 ``PlanStorageBase`` 类来实现自己的计划存储。如果未提供,将使用默认的内存存储。
#
# .. tip:: ``PlanStorageBase`` 类继承自 ``StateModule`` 类,因此 storage也会通过会话管理进行保存和加载。
#
# ``PlanNotebook`` 类的核心属性和方法总结如下:
#
# .. list-table:: ``PlanNotebook`` 类的核心属性和方法
# :header-rows: 1
#
# * - 类型
# - 名称
# - 描述
# * - 属性
# - ``current_plan``
# - 智能体正在执行的当前计划
# * -
# - ``storage``
# - 历史计划的存储,用于检索和恢复历史计划
# * -
# - ``plan_to_hint``
# - 一个可调用对象,以当前计划为输入并生成提示消息来指导智能体完成计划
# * - 函数
# - ``list_tools``
# - 列出 ``PlanNotebook`` 类提供的所有工具函数
# * -
# - ``get_current_hint``
# - 获取当前计划的提示消息,将调用 ``plan_to_hint`` 函数
# * -
# - | ``create_plan``,
# | ``view_subtasks``,
# | ``revise_current_plan``,
# | ``update_subtask_state``,
# | ``finish_subtask``,
# | ``finish_plan``,
# | ``view_historical_plans``,
# | ``recover_historical_plan``
# - 允许智能体管理计划和子任务的工具函数
# * -
# - ``register_plan_change_hook``
# - 注册一个钩子函数,当计划发生变化时将被调用,用于计划可视化和监控
# * -
# - ``remove_plan_change_hook``
# - 移除已注册的钩子函数
#
# ``list_tools`` 方法是获取所有工具函数的快速方法,这样您就可以将它们注册到智能体的工具包中。
plan_notebook = PlanNotebook()
async def list_tools() -> None:
"""列出PlanNotebook提供的工具函数。"""
print("PlanNotebook提供的工具")
for tool in plan_notebook.list_tools():
print(tool.__name__)
asyncio.run(list_tools())
# %%
# 与ReActAgent协作
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope中的 ``ReActAgent`` 已通过构造函数中的 ``plan_notebook`` 参数集成了计划模块。
# 一旦提供,智能体将
#
# - 配备计划管理工具函数,并且
# - 在每个推理步骤开始时插入提示消息
#
# 有两种方式在 ``ReActAgent`` 中使用计划模块:
#
# - 开发者指定计划:开发者可以通过调用 ``create_plan`` 工具函数手动创建计划,并使用该计划来初始化 ``ReActAgent`` 。
# - 智能体管理的计划执行:智能体将通过调用计划管理工具函数自己创建和管理计划。
#
# 手动计划规范
# ---------------------------------
# 通过调用 ``create_plan`` 工具函数手动创建计划非常简单。
# 以下是手动创建计划以对LLM赋能的智能体进行全面研究的示例。
#
async def manual_plan_specification() -> None:
"""手动计划规范示例。"""
await plan_notebook.create_plan(
name="智能体研究",
description="对基于LLM的智能体进行全面研究",
expected_outcome="一份Markdown格式的报告回答三个问题1. 什么是智能体2. 智能体的当前技术水平是什么3. 智能体的未来趋势是什么?",
subtasks=[
SubTask(
name="搜索智能体相关调研论文",
description=(
"在多个来源搜索调研论文,包括"
"Google Scholar、arXiv和Semantic Scholar。必须"
"在2021年后发表且引用数超过50。"
),
expected_outcome="Markdown格式的论文列表",
),
SubTask(
name="阅读和总结论文",
description="阅读前一步找到的论文,并总结关键点,包括定义、分类、挑战和关键方向。",
expected_outcome="Markdown格式的关键点总结",
),
SubTask(
name="研究大公司的最新进展",
description=(
"研究大公司的最新进展包括但不限于Google、Microsoft、OpenAI、"
"Anthropic、阿里巴巴和Meta。查找官方博客或新闻文章。"
),
expected_outcome="大公司的最新进展",
),
SubTask(
name="撰写报告",
description="基于前面的步骤撰写报告,并回答预期结果中的三个问题。",
expected_outcome=(
"一份Markdown格式的报告回答三个问题1. "
"什么是智能体2. 智能体的当前技术水平"
"是什么3. 智能体的未来趋势是什么?"
),
),
],
)
print("当前提示消息:\n")
msg = await plan_notebook.get_current_hint()
print(f"{msg.name}: {msg.content}")
asyncio.run(manual_plan_specification())
# %%
# 创建计划后,可以按如下方式使用计划笔记本初始化 ``ReActAgent``
agent = ReActAgent(
name="Friday",
sys_prompt="你是一个有用的助手。",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
),
formatter=DashScopeChatFormatter(),
plan_notebook=plan_notebook,
)
# %%
# 智能体自主管理
# ---------------------------------
# 智能体也可以通过调用计划管理工具函数自己创建和管理计划。
# 我们只需要按如下方式使用计划笔记本初始化 ``ReActAgent``
#
agent = ReActAgent(
name="Friday",
sys_prompt="你是一个有用的助手。",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
),
formatter=DashScopeChatFormatter(),
plan_notebook=PlanNotebook(),
)
# %%
# 之后,我们可以构建一个循环来与智能体交互,如下所示。
# 一旦用户的任务复杂比较复杂,智能体将自己创建计划并逐步执行计划。
#
# .. code-block:: python
# :caption: 与计划智能体建立对话
#
# async def interact_with_agent() -> None:
# """与计划智能体交互。"""
# user = UserAgent(name="user")
#
# msg = None
# while True:
# msg = await user(msg)
# if msg.get_text_content() == "exit":
# break
# msg = await agent(msg)
#
# asyncio.run(interact_with_agent())
#
# 可视化和监控
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope 通过钩子函数支持计划执行的实时可视化和监控。
#
# 当前计划被工具函数改变时,钩子函数将被触发,开发者可以在这些钩子函数中将当前的计划转发到对应的前端进行可视化或其他处理。
# 计划变化钩子函数的模板如下:
#
def plan_change_hook_template(self: PlanNotebook, plan: Plan) -> None:
"""计划变化钩子函数的模板。
Args:
self (`PlanNotebook`):
PlanNotebook实例。
plan (`Plan`):
当前计划实例(变化后)。
"""
# 将计划转发到前端进行可视化或其他处理

View File

@@ -0,0 +1,329 @@
# -*- coding: utf-8 -*-
"""
.. _prompt:
提示词格式化
=========================
AgentScope 中的格式化器formatter模块负责
- 将 Msg 对象转换为不同 LLM API 要求的格式,
- (可选)截断消息以适应 max_token 的限制,
- (可选)执行提示工程,例如对长对话进行总结。
后两个功能是可选的开发者也可以选择在记忆memory或智能体agent层面进行处理和实现。
在 AgentScope 中,有两种类型的格式化器:"ChatFormatter""MultiAgentFormatter",它们根据输入消息中的“身份实体”进行区分。
- **ChatFormatter**:专为标准的用户-助手场景(聊天机器人)设计,使用 ``role`` 字段来识别用户和助手。
- **MultiAgentFormatter**:专为多智能体场景设计,使用 ``name`` 字段来识别不同的实体,在格式化的过程中会将多智能体的对话历史合并为单个消息。
AgentScope 内置的格式化器如下所列
.. list-table:: AgentScope 中的内置格式化器
:header-rows: 1
* - API 提供商
- 用户-助手场景
- 多智能体场景
* - OpenAI
- ``OpenAIChatFormatter``
- ``OpenAIMultiAgentFormatter``
* - Anthropic
- ``AnthropicChatFormatter``
- ``AnthropicMultiAgentFormatter``
* - DashScope
- ``DashScopeChatFormatter``
- ``DashScopeMultiAgentFormatter``
* - Gemini
- ``GeminiChatFormatter``
- ``GeminiChatFormatter``
* - Ollama
- ``OllamaChatFormatter``
- ``OllamaMultiAgentFormatter``
* - DeepSeek
- ``DeepSeekChatFormatter``
- ``DeepSeekMultiAgentFormatter``
* - vLLM
- ``OpenAIFormatter``
- ``OpenAIFormatter``
.. tip:: OpenAI API 支持 `name` 字段,因此 `OpenAIFormatter` 也可以用于多智能体场景。也可以使用 `OpenAIMultiAgentFormatter` 代替,它会将对话历史合并为单个用户消息。
此外内置格式化器对于不同的消息块message blocks的支持情况如下表所示
.. list-table:: 内置格式化器中支持的消息块
:header-rows: 1
* - 格式化器
- tool_use/result
- image
- audio
- video
- thinking
* - ``OpenAIChatFormatter``
- ✅
- ✅
- ✅
- ❌
-
* - ``DashScopeChatFormatter``
- ✅
- ✅
- ✅
- ❌
-
* - ``DashScopeMultiAgentFormatter``
- ✅
- ✅
- ✅
- ❌
-
* - ``AnthropicChatFormatter``
- ✅
- ✅
- ❌
- ❌
- ✅
* - ``AnthropicMultiAgentFormatter``
- ✅
- ✅
- ❌
- ❌
- ✅
* - ``GeminiChatFormatter``
- ✅
- ✅
- ✅
- ✅
-
* - ``GeminiMultiAgentFormatter``
- ✅
- ✅
- ✅
- ✅
-
* - ``OllamaChatFormatter``
- ✅
- ✅
- ❌
- ❌
-
* - ``OllamaMultiAgentFormatter``
- ✅
- ✅
- ❌
- ❌
-
* - ``DeepSeekChatFormatter``
- ✅
- ❌
- ❌
- ❌
-
* - ``DeepSeekMultiAgentFormatter``
- ✅
- ❌
- ❌
- ❌
-
.. note:: 如 `官方文档 <https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#preserving-thinking-blocks>`_ 所述,只有 Anthropic 建议在输入的提示词中保留推理的部分thinking blocks。对于其它格式化器我们忽略输入消息中包含的 ``ThinkingBlock``。
面向 ReAct 的格式化
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
内置的 formatter 均面向 ReAct 智能体进行设计,其中输入消息由交替的 **对话历史** 和 **工具调用序列** 组成。
在用户-助手场景中,对话历史是用户和助手的消息,我们直接将它们转换为所期望的格式。
然而,在多智能体场景中,对话历史是来自不同智能体的消息列表,如下所示:
.. figure:: ../../_static/images/multiagent_msgs.png
:alt: 多智能体消息示例
:width: 85%
:align: center
*多智能体消息示例*
因此,我们必须将对话历史合并为带有标签 "<history>""</history>" 的单个用户消息。
以 DashScope 为例,格式化后的消息将如下所示:
"""
from agentscope.token import HuggingFaceTokenCounter
from agentscope.formatter import DashScopeMultiAgentFormatter
from agentscope.message import Msg, ToolResultBlock, ToolUseBlock, TextBlock
import asyncio, json
input_msgs = [
# 系统提示
Msg("system", "你是一个名为 Friday 的有用助手", "system"),
# 对话历史
Msg("Bob", "你好Alice你知道最近的图书馆在哪里吗", "assistant"),
Msg(
"Alice",
"抱歉我不知道。Charlie你有什么想法吗",
"assistant",
),
Msg(
"Charlie",
"没有,我们问问 Friday 吧。Friday帮我找到最近的图书馆。",
"assistant",
),
# 工具序列
Msg(
"Friday",
[
ToolUseBlock(
type="tool_use",
name="get_current_location",
id="1",
input={},
),
],
"assistant",
),
Msg(
"system",
[
ToolResultBlock(
type="tool_result",
name="get_current_location",
id="1",
output=[TextBlock(type="text", text="104.48, 36.30")],
),
],
"system",
),
Msg(
"Friday",
[
ToolUseBlock(
type="tool_use",
name="search_around",
id="2",
input={"location": [104.48, 36.30], "keyword": "library"},
),
],
"assistant",
),
Msg(
"system",
[
ToolResultBlock(
type="tool_result",
name="search_around",
id="2",
output=[TextBlock(type="text", text="[...]")],
),
],
"system",
),
# 对话历史继续
Msg("Friday", "最近的图书馆是...", "assistant"),
Msg("Bob", "谢谢Friday", "assistant"),
Msg("Alice", "我们一起去吧。", "assistant"),
]
async def run_formatter_example() -> list[dict]:
"""多智能体消息格式化示例。"""
formatter = DashScopeMultiAgentFormatter()
formatted_message = await formatter.format(input_msgs)
print("格式化后的消息:")
print(json.dumps(formatted_message, indent=4, ensure_ascii=False))
return formatted_message
formatted_message = asyncio.run(run_formatter_example())
# %%
# 具体来说,对话历史被格式化为:
#
print("第一段对话历史:")
print(formatted_message[1]["content"])
print("\n第二段对话历史:")
print(formatted_message[-1]["content"])
# %%
# 基于截断的格式化
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 通过 AgentScope 中的 token 模块,内置格式化器支持通过 **删除最旧的消息**(除了系统提示消息)在 token 超过限制时截断输入消息。
#
# 以 OpenAIFormatter 为例,我们首先计算输入消息的总 token 数。
#
async def run_token_counter() -> int:
"""计算输入消息的 token 数量。"""
# 我们使用 huggingface token 计数器用于 dashscope 模型。
token_counter = HuggingFaceTokenCounter(
"Qwen/Qwen2.5-VL-3B-Instruct",
use_mirror=False,
)
return await token_counter.count(formatted_message)
# %%
# 然后我们将最大 token 限制设置为比总 token 数少 20 个,并运行格式化器。
#
async def run_truncated_formatter() -> None:
"""带截断的消息格式化示例。"""
token_counter = HuggingFaceTokenCounter(
pretrained_model_name_or_path="Qwen/Qwen2.5-VL-3B-Instruct",
use_mirror=False,
)
formatter = DashScopeMultiAgentFormatter(
token_counter=token_counter,
max_tokens=n_tokens - 20,
)
truncated_formatted_message = await formatter.format(input_msgs)
n_truncated_tokens = await token_counter.count(truncated_formatted_message)
print("截断后的 token 数量:", n_truncated_tokens)
print("\n截断后的对话历史:")
print(truncated_formatted_message[1]["content"])
# %%
# 我们可以看到来自 Bob 和 Alice 的前两条消息被删除以适应 ``max_tokens`` 的限制。
#
#
# 自定义格式化器
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope 提供了两个基类 ``FormatterBase`` 和其子类 ``TruncatedFormatterBase``。
# 其中 ``TruncatedFormatterBase`` 类提供了 FIFOFirst In First Out截断策略所有内置格式化器都继承自它。
#
# .. list-table:: AgentScope 中格式化器的基类
# :header-rows: 1
#
# * - 类
# - 抽象方法
# - 描述
# * - ``FormatterBase``
# - ``format``
# - 将输入的 ``Msg`` 对象格式化为目标 API 所期望的格式
# * - ``TruncatedFormatterBase``
# - ``_format_agent_message``
# - 格式化智能体消息,在多智能体场景中可能包含多个身份
# * -
# - ``_format_tool_sequence``
# - 将工具使用和结果序列格式化为所期望的格式
# * -
# - ``_format`` (可选)
# - 将输入的 ``Msg`` 对象格式化为目标 API 所期望的格式
#
# .. tip:: ``TruncatedFormatterBase`` 中的 ``_format`` 将输入消息分组为智能体消息和工具序列,然后分别通过调用 ``_format_agent_message`` 和 ``_format_tool_sequence`` 来格式化它们。开发者可以重写两个函数来实现自己的格式化策略。
#
# .. tip:: 可选地,开发者可以重写 ``TruncatedFormatterBase`` 中的 ``_truncate`` 方法来实现自己的截断策略。
#
# 进一步阅读
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# - :ref:`token`
# - :ref:`model`
#

View File

@@ -0,0 +1,404 @@
# -*- coding: utf-8 -*-
"""
.. _rag:
RAG
==========================
AgentScope 提供了内置的 RAGRetrieval-Augmented Generation) 实现。本节将详细介绍
- 如何使用 AgentScope 中的 RAG 模块,
- 如何实现 **多模态** RAG
- 如何在 ``ReActAgent`` 中以两种不同的方式集成 RAG 模块:
- 智能体自主控制Agentic manner
- 通用方式Generic manner
我们在下列表格中总结了两种模式的优缺点:
.. list-table:: RAG 集成方式比较
:header-rows: 1
* - 集成方式
- 描述
- 优点
- 缺点
* - 智能体自主控制
- 以工具调用方式让智能体自主决定何时进行查询,查询什么关键字
- - 与 ReAct 算法契合,灵活性高
- 智能体能够根据当前的上下文改写查询关键词
- 避免在不必要时发生查询
- 对 LLM 模型能力要求较高
* - 通用方式
- 每次在 ``reply`` 函数开始时固定进行查询并将检索结果整合到提示prompt
- - 实现简单
- 对 LLM 模型能力要求低
- - 每次都会运行查询,因此会引入过多不必要的查询检索
- 查询数据库较大时,回复延迟较高
.. note:: 作为开源框架AgentScope 的目标是让开发过程更简单也更有趣。因此AgentScope 的设计中并不强制要求使用内置的 RAG 实现,同时支持、鼓励开发者集成现有的 RAG 实现或第三方 RAG 框架。
"""
import asyncio
import json
import os
from matplotlib import pyplot as plt
import agentscope
from agentscope.agent import ReActAgent
from agentscope.embedding import (
DashScopeTextEmbedding,
DashScopeMultiModalEmbedding,
)
from agentscope.formatter import DashScopeChatFormatter
from agentscope.message import Msg
from agentscope.model import DashScopeChatModel
from agentscope.rag import (
TextReader,
SimpleKnowledge,
QdrantStore,
Document,
ImageReader,
)
from agentscope.tool import Toolkit
# %%
# 使用 RAG 模块
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope 中的 RAG 模块由以下三个核心组件构成:
#
# - **Reader**负责从数据源读取数据并进行分块chunking
# - **Knowledge**:负责知识库查询检索和数据存储逻辑的算法实现
# - **Store**:负责与向量数据库交互的逻辑实现
#
# .. note:: 我们将持续在 AgentScope 中集成新的向量数据库和数据读取模块。详情请查看我们的 `开发路线图 <https://github.com/orgs/agentscope-ai/projects/2>`_同时也欢迎贡献代码
#
# 当前 AgentScope 中内置支持的 reader 包括:
#
for _ in agentscope.rag.__all__:
if _.endswith("Reader"):
print(f"- {_}")
# %%
# 这些 reader 的作用是读取数据,将数据分块并包装成 ``agentscope.rag.Document`` 对象。 ``Document`` 对象包含以下字段:
#
# - ``metadata``:数据块的元信息,包含数据内容(``content``)、数据 ID``doc_id``)、块 ID``chunk_id``)和总块数(``total_chunks``
# - ``embedding``: 数据块的向量表示,默认为 ``None``,在将数据块添加到知识库时会被填充
# - ``score``: 数据块的相关性分数,默认为 ``None``,在从知识库检索数据块时会被填充
#
# 以 ``TextReader`` 为例,通过如下代码读取文本字符串,并将文本分块为 ``Document`` 对象:
#
async def example_text_reader(print_docs: bool) -> list[Document]:
"""使用 TextReader 读取文本字符串,并将文本分块为 Document 对象。"""
# 创建 TextReader 对象
reader = TextReader(chunk_size=512, split_by="paragraph")
# 读取文本字符串
documents = await reader(
text=(
# 我们准备一些文本数据用于演示 RAG 功能。
"我的名字是李明今年28岁。\n"
"我居住在中国杭州,是一名算法工程师。我喜欢打篮球和玩游戏。\n"
"我父亲的名字是李强,是一名医生,我的母亲是陈芳芳,是一名教师,她总是指导我学习。\n"
"我现在在北京大学攻读博士学位,研究方向是人工智能。\n"
"我最好的朋友是王伟,我们从小一起长大,现在他是一名律师。"
),
)
if print_docs:
print(f"文本被分块为 {len(documents)} 个 Document 对象:")
for idx, doc in enumerate(documents):
print(f"Document {idx}:")
print("\tScore: ", doc.score)
print(
"\tMetadata: ",
json.dumps(doc.metadata, indent=2, ensure_ascii=False),
"\n",
)
return documents
docs = asyncio.run(example_text_reader(print_docs=True))
# %%
# 由于并不存在一个 “one-for-all” 的数据读取和分块方法,特别像是 PDF 和 Word 这类复杂格式的文档。
# 因此AgentScope 鼓励开发者根据自己的数据格式实现自定义的 reader。
# 只需要继承 ``BaseReader`` 类,并实现 ``__call__`` 方法即可。
#
# 在数据分块后,接下来需要将数据块添加到知识库中。
# 在 AgentScope 中,知识库的初始化需要提供 **嵌入模型** 和 **向量存储** (即向量数据库) 的对象。
# AgentScope 目前内置支持基于 `Qdrant <https://qdrant.tech/>`_ 实现的向量存储,以及一个知识库的基础实现 ``SimpleKnowledge``。
# 具体使用方式如下:
#
# .. note::
#
# - 我们正在 AgentScope 中集成新的向量数据库,详情请查看我们的 `开发路线图 <https://github.com/orgs/agentscope-ai/projects/2>`_。欢迎贡献代码
# - Qdrant 的实现通过 ``location`` 参数支持多种不同的部署方式,包括内存模式,本地模式和云端模式。详情请参考 `Qdrant 文档 <https://qdrant.tech/>`_。
#
async def build_knowledge_base() -> SimpleKnowledge:
"""构建知识库。"""
# 读取 documents 数据
documents = await example_text_reader(print_docs=False)
# 创建一个内存中的 Qdrant 向量存储,以及使用 DashScopeTextEmbedding 作为嵌入模型,初始化知识库
knowledge = SimpleKnowledge(
# 提供一个 embedding 模型用于将文本转换为向量
embedding_model=DashScopeTextEmbedding(
api_key=os.environ["DASHSCOPE_API_KEY"],
model_name="text-embedding-v4",
dimensions=1024,
),
# 选择 Qdrant 作为向量存储
embedding_store=QdrantStore(
location=":memory:", # 使用内存模式
collection_name="test_collection",
dimensions=1024, # 嵌入向量的维度必须与嵌入模型输出的维度一致
),
)
# 将 documents 添加到知识库中
await knowledge.add_documents(documents)
# 从知识库中检索数据
docs = await knowledge.retrieve(
query="李明的父亲是谁?",
limit=3,
score_threshold=0.5,
)
print("检索到的 Document 对象:")
for doc in docs:
print(doc, "\n")
return knowledge
knowledge = asyncio.run(build_knowledge_base())
# %%
# AgentScope 中的知识库类提供两个核心方法:``add_documents`` 和 ``retrieve``,分别用于添加数据块和搜索检索数据块。
#
# 此外AgentScope 提供了 ``retrieve_knowledge`` 方法,它将 ``retrieve`` 方法封装成一个智能体能够直接调用的工具函数。开发者可以直接使用该工具函数装备智能体。
#
# 自定义 RAG 组件
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope 支持并鼓励开发者自定义 RAG 组件,包括 reader、知识库和向量数据库。
# 具体来说,我们提供了以下基类用于自定义:
#
# .. list-table:: RAG 基类
# :header-rows: 1
#
# * - 基类
# - 描述
# - 抽象方法
# * - ``ReaderBase``
# - 所有 reader 的基类
# - ``__call__``
# * - ``VDBStoreBase``
# - 向量数据库的基类
# - | ``add``
# | ``search``
# | ``get_client`` (可选)
# | ``delete`` (可选)
# * - ``KnowledgeBase``
# - 知识库的基类
# - | ``retrieve``
# | ``add_documents``
#
# ``VDBStoreBase`` 中的 ``get_client`` 方法允许开发者访问底层向量数据库的完整功能。
# 这样,他们就可以基于向量数据库实现更高级的功能,例如建立索引、高级搜索等。
#
# 与 ReActAgent 集成
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 接下来我们将演示如何以智能体自主控制agentic和通用generic两种方式将 RAG 模块与 AgentScope 中的 ``ReActAgent`` 集成。
#
# 智能体自主控制
# --------------------------------
# 在智能体自主控制的方式中ReAct 智能体可以自主决定何时检索知识以及检索的查询内容。
# 将 RAG 模块与 AgentScope 中的 ``ReActAgent`` 集成非常简单,只需将知识库的 ``retrieve_knowledge`` 方法注册为工具,并为该工具提供适当的描述即可。
async def example_agentic_manner() -> None:
"""以智能体自主控制方式将 RAG 模块与 ReActAgent 集成的示例。"""
# 创建一个 ReAct 智能体
toolkit = Toolkit()
# 使用 DashScope 作为模型创建 ReAct 智能体
agent = ReActAgent(
name="Friday",
sys_prompt="You're a helpful assistant named Friday.",
model=DashScopeChatModel(
api_key=os.environ["DASHSCOPE_API_KEY"],
model_name="qwen-max",
),
formatter=DashScopeChatFormatter(),
toolkit=toolkit,
)
print("第一次回复: ")
# 第一次我们进行一些交流,提供“李明”这个名字作为上下文内容
await agent(
Msg(
"user",
"李明是我最好的朋友。",
"user",
),
)
# 将 retrieve_knowledge 方法注册为工具箱中的工具函数
toolkit.register_tool_function(
knowledge.retrieve_knowledge,
func_description=( # 为工具提供清晰的描述
"用于检索与给定查询相关的文档的工具。" "当你需要查找有关李明的信息时使用此工具。"
),
)
print("\n\n第二次回复: ")
# 第二次回复中,我们希望智能体能够将查询中“他父亲”改写得更具体,例如
# “李明的父亲是谁?”或“李明的父亲”
await agent(
Msg(
"user",
"你知道他父亲是谁吗?",
"user",
),
)
asyncio.run(example_agentic_manner())
# %%
# 在上面的例子中,我们模拟了正常与智能体交流过程。第一次的交流我们提供了“李明”的名字作为上下文内容。
# 第二次提问时,我们的问题是“你知道他父亲是谁吗?”,
# 我们希望智能体能够利用上下文历史信息改写查询,使其更具体,更好的进行检索,例如改写为“李明的父亲是谁?”或“李明的父亲”。
#
# 更进一步,结合 :ref:`plan` 模块,我们可以让智能体实现更加复杂的查询改写和多轮检索。
#
# 通用方式
# --------------------------------
# ``ReActAgent`` 还以一种更加通用的方式集成了 RAG 模块,
# 它在每次 ``reply`` 函数开始执行时检索知识,并将检索到的知识附加到用户消息的提示中。
#
# 只需设置 ``ReActAgent`` 的 ``knowledge`` 参数,智能体就会在每次回复开始时自动检索知识。
#
async def example_generic_manner() -> None:
"""以通用方式将 RAG 模块与 ReActAgent 集成的示例。"""
# 创建一个 ReAct 智能体
agent = ReActAgent(
name="Friday",
sys_prompt="You're a helpful assistant named Friday.",
model=DashScopeChatModel(
api_key=os.environ["DASHSCOPE_API_KEY"],
model_name="qwen-max",
),
formatter=DashScopeChatFormatter(),
# 将知识库传递给智能体
knowledge=knowledge,
)
await agent(
Msg(
"user",
"你知道李明的父亲是谁吗?",
"user",
),
)
print("\n查看智能体记忆中检索信息如何插入:")
content = (await agent.memory.get_memory())[1].content
print(json.dumps(content, indent=2, ensure_ascii=False))
asyncio.run(example_generic_manner())
# %%
# 多模态 RAG
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope 中的 RAG 模块原生支持多模态,因为
#
# - AgentScope 支持多模态嵌入 API例如 ``DashScopeMultimodalEmbedding``。
# - ``Document`` 类的 ``metadata`` 中,``content`` 字段的类型是 ``TextBlock | ImageBlock | VideoBlock``,因此可以存储文本、图片和视频等多模态数据。
#
# 因此,我们可以直接使用多模态 reader 和嵌入模型来构建多模态知识库,如下所示。
#
# 首先我们准备一张本地的图片这张图片上包含了文本“My name is Ming Li”。
# 准备一张包含文本“My name is Ming Li”的图片。
path_image = "./example.png"
plt.figure(figsize=(8, 3))
plt.text(0.5, 0.5, "My name is Ming Li", ha="center", va="center", fontsize=30)
plt.axis("off")
plt.savefig(path_image, bbox_inches="tight", pad_inches=0.1)
plt.close()
# %%
# 然后我们可以构建一个多模态知识库,构建过程与纯文本知识库类似。只是将 reader 和嵌入模型替换为多模态版本即可。
# 在下面的例子中,我们使用了 ``ImageReader`` 和 ``DashScopeMultiModalEmbedding``。
# 同时,这里我们使用多模态模型 ``qwen3-vl-plus`` 作为智能体的语言模型。
#
async def example_multimodal_rag() -> None:
"""使用多模态 RAG 的示例。"""
# 使用 ImageReader 读取图片
reader = ImageReader()
docs = await reader(image_url=path_image)
# 创建一个知识库
knowledge = SimpleKnowledge(
embedding_model=DashScopeMultiModalEmbedding(
api_key=os.environ["DASHSCOPE_API_KEY"],
model_name="multimodal-embedding-v1",
dimensions=1024,
),
embedding_store=QdrantStore(
location=":memory:",
collection_name="test_collection",
dimensions=1024,
),
)
await knowledge.add_documents(docs)
agent = ReActAgent(
name="Friday",
sys_prompt="You're a helpful assistant named Friday.",
model=DashScopeChatModel(
api_key=os.environ["DASHSCOPE_API_KEY"],
model_name="qwen3-vl-plus",
),
formatter=DashScopeChatFormatter(),
knowledge=knowledge,
)
await agent(
Msg(
"user",
"你知道我的名字吗?",
"user",
),
)
# 让我们看看检索到的图片数据是否已经加入了智能体的记忆中
print("\n查看智能体记忆中检索信息如何插入:")
content = (await agent.memory.get_memory())[1].content
print(json.dumps(content, indent=2, ensure_ascii=False))
asyncio.run(example_multimodal_rag())
# %%
# 进一步阅读
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# - :ref:`embedding`
# - :ref:`plan`
#

View File

@@ -0,0 +1,486 @@
# -*- coding: utf-8 -*-
"""
.. _realtime:
实时智能体
====================
**实时智能体Realtime Agent** 用于处理实时交互场景,例如语音对话或实时聊天会话。
AgentScope 中的实时智能体具有以下特性:
- 集成 OpenAI、DashScope、Gemini 等实时模型 API
- 统一的事件接口,简化与不同实时模型的交互
- 支持工具调用能力
- 支持多智能体交互
.. note:: 实时智能体目前处于活跃开发阶段,欢迎社区贡献、讨论和反馈!如果开发者对实时智能体感兴趣,欢迎加入讨论和开发。
"""
import asyncio
import os
from agentscope.agent import RealtimeAgent
from agentscope.realtime import (
DashScopeRealtimeModel,
OpenAIRealtimeModel,
GeminiRealtimeModel,
)
# %%
# 创建实时模型
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# AgentScope 目前支持以下实时模型 API
#
# .. list-table::
# :header-rows: 1
# :widths: 15 25 25 15 20
#
# * - 提供商
# - 类名
# - 支持的模型
# - 输入模态
# - 工具支持
# * - DashScope
# - ``DashScopeRealtimeModel``
# - ``qwen3-omni-flash-realtime``
# - 文本、音频、图像
# - 否
# * - OpenAI
# - ``OpenAIRealtimeModel``
# - ``gpt-4o-realtime-preview``
# - 文本、音频
# - 是
# * - Gemini
# - ``GeminiRealtimeModel``
# - ``gemini-2.5-flash-native-audio-preview-09-2025``
# - 文本、音频、图像
# - 是
#
#
# 以下是初始化不同实时模型的示例:
#
# .. code-block:: python
# :caption: 初始化不同实时模型的示例
#
# # DashScope 实时模型
# dashscope_model = DashScopeRealtimeModel(
# model_name="qwen3-omni-flash-realtime",
# api_key=os.getenv("DASHSCOPE_API_KEY"),
# voice="Cherry", # 可选项: "Cherry", "Serena", "Ethan", "Chelsie"
# enable_input_audio_transcription=True,
# )
#
# # OpenAI 实时模型
# openai_model = OpenAIRealtimeModel(
# model_name="gpt-4o-realtime-preview",
# api_key=os.getenv("OPENAI_API_KEY"),
# voice="alloy", # 可选项: "alloy", "echo", "marin", "cedar"
# enable_input_audio_transcription=True,
# )
#
# # Gemini 实时模型
# gemini_model = GeminiRealtimeModel(
# model_name="gemini-2.5-flash-native-audio-preview-09-2025",
# api_key=os.getenv("GEMINI_API_KEY"),
# voice="Puck", # 可选项: "Puck", "Charon", "Kore", "Fenrir"
# enable_input_audio_transcription=True,
# )
#
#
#
# 实时模型提供以下核心方法:
#
# .. list-table::
# :header-rows: 1
# :widths: 30 70
#
# * - 方法
# - 描述
# * - ``connect(outgoing_queue, instructions, tools)``
# - 建立与实时模型 API 的 WebSocket 连接
# * - ``disconnect()``
# - 关闭 WebSocket 连接
# * - ``send(data)``
# - 向实时模型发送音频/文本/图像数据进行处理
#
# ``connect()`` 方法中的 ``outgoing_queue`` 参数是一个 asyncio 队列,
# 用于将实时模型的事件转发到外部(例如智能体或前端)。
#
#
# 模型事件接口
# -----------------------
#
# AgentScope 提供统一的 ``agentscope.realtime.ModelEvents`` 接口,
# 简化与不同实时模型的交互。支持以下事件:
#
# .. note:: ModelEvents 中的 "session" 指的是实时模型与模型 API 之间的
# WebSocket 连接会话,而非前端与后端之间的会话。
#
# .. list-table::
# :header-rows: 1
# :widths: 40 60
#
# * - 事件
# - 描述
# * - ``ModelEvents.ModelSessionCreatedEvent``
# - 会话创建成功
# * - ``ModelEvents.ModelSessionEndedEvent``
# - 会话已结束
# * - ``ModelEvents.ModelResponseCreatedEvent``
# - 模型开始生成响应
# * - ``ModelEvents.ModelResponseDoneEvent``
# - 模型完成响应生成
# * - ``ModelEvents.ModelResponseAudioDeltaEvent``
# - 流式音频数据块
# * - ``ModelEvents.ModelResponseAudioDoneEvent``
# - 音频响应完成
# * - ``ModelEvents.ModelResponseAudioTranscriptDeltaEvent``
# - 流式音频转录文本块
# * - ``ModelEvents.ModelResponseAudioTranscriptDoneEvent``
# - 音频转录完成
# * - ``ModelEvents.ModelResponseToolUseDeltaEvent``
# - 流式工具调用参数
# * - ``ModelEvents.ModelResponseToolUseDoneEvent``
# - 工具调用参数完成
# * - ``ModelEvents.ModelInputTranscriptionDeltaEvent``
# - 流式用户输入转录文本块
# * - ``ModelEvents.ModelInputTranscriptionDoneEvent``
# - 用户输入转录完成
# * - ``ModelEvents.ModelInputStartedEvent``
# - 检测到用户音频输入开始VAD
# * - ``ModelEvents.ModelInputDoneEvent``
# - 检测到用户音频输入结束VAD
# * - ``ModelEvents.ModelErrorEvent``
# - 发生错误
#
#
#
# 创建实时智能体
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# ``RealtimeAgent`` 作为桥接层,负责:
#
# - 将实时模型的 ``ModelEvents`` 转换为 ``ServerEvents``,发送给前端和其他智能体
# - 接收来自前端或其他智能体的 ``ClientEvents``,并转发给实时模型 API
# - 管理智能体的生命周期和事件队列
#
# 服务端和客户端事件
# -------------------------
#
# AgentScope 提供统一的 ``ServerEvents`` 和 ``ClientEvents``
# 用于后端与前端之间的通信:
#
# **ServerEvents**(后端 → 前端):
#
# .. list-table::
# :header-rows: 1
# :widths: 40 60
#
# * - 事件
# - 描述
# * - ``ServerEvents.ServerSessionCreatedEvent``
# - 后端创建会话
# * - ``ServerEvents.ServerSessionUpdatedEvent``
# - 后端更新会话
# * - ``ServerEvents.ServerSessionEndedEvent``
# - 后端结束会话
# * - ``ServerEvents.AgentReadyEvent``
# - 智能体准备接收输入
# * - ``ServerEvents.AgentEndedEvent``
# - 智能体已结束
# * - ``ServerEvents.AgentResponseCreatedEvent``
# - 智能体开始生成响应
# * - ``ServerEvents.AgentResponseDoneEvent``
# - 智能体完成响应生成
# * - ``ServerEvents.AgentResponseAudioDeltaEvent``
# - 智能体流式音频块
# * - ``ServerEvents.AgentResponseAudioDoneEvent``
# - 音频响应完成
# * - ``ServerEvents.AgentResponseAudioTranscriptDeltaEvent``
# - 智能体响应的流式转录
# * - ``ServerEvents.AgentResponseAudioTranscriptDoneEvent``
# - 转录完成
# * - ``ServerEvents.AgentResponseToolUseDeltaEvent``
# - 流式工具调用数据
# * - ``ServerEvents.AgentResponseToolUseDoneEvent``
# - 工具调用完成
# * - ``ServerEvents.AgentResponseToolResultEvent``
# - 工具执行结果
# * - ``ServerEvents.AgentInputTranscriptionDeltaEvent``
# - 用户输入的流式转录
# * - ``ServerEvents.AgentInputTranscriptionDoneEvent``
# - 输入转录完成
# * - ``ServerEvents.AgentInputStartedEvent``
# - 用户音频输入开始
# * - ``ServerEvents.AgentInputDoneEvent``
# - 用户音频输入结束
# * - ``ServerEvents.AgentErrorEvent``
# - 发生错误
#
# **ClientEvents**(前端 → 后端):
#
# .. list-table::
# :header-rows: 1
# :widths: 40 60
#
# * - 事件
# - 描述
# * - ``ClientEvents.ClientSessionCreateEvent``
# - 创建指定配置的新会话
# * - ``ClientEvents.ClientSessionEndEvent``
# - 结束当前会话
# * - ``ClientEvents.ClientResponseCreateEvent``
# - 请求智能体立即生成响应
# * - ``ClientEvents.ClientResponseCancelEvent``
# - 中断智能体的当前响应
# * - ``ClientEvents.ClientTextAppendEvent``
# - 追加文本输入
# * - ``ClientEvents.ClientAudioAppendEvent``
# - 追加音频输入
# * - ``ClientEvents.ClientAudioCommitEvent``
# - 提交音频输入(标志输入结束)
# * - ``ClientEvents.ClientImageAppendEvent``
# - 追加图像输入
# * - ``ClientEvents.ClientToolResultEvent``
# - 发送工具执行结果
#
# 初始化实时智能体
# ------------------------------
#
# 以下是创建和使用实时智能体的示例:
async def example_realtime_agent() -> None:
"""创建和使用实时智能体的示例。"""
agent = RealtimeAgent(
name="Friday",
sys_prompt="你是一个名为 Friday 的助手。",
model=DashScopeRealtimeModel(
model_name="qwen3-omni-flash-realtime",
api_key=os.getenv("DASHSCOPE_API_KEY"),
),
)
# 创建队列接收来自智能体的消息
outgoing_queue = asyncio.Queue()
# 智能体现在已准备好处理输入
# 在独立任务中处理输出消息
async def handle_agent_messages():
while True:
event = await outgoing_queue.get()
# 处理事件(例如通过 WebSocket 发送到前端)
print(f"智能体事件: {event.type}")
# 启动消息处理任务
asyncio.create_task(handle_agent_messages())
# 启动智能体(建立连接)
await agent.start(outgoing_queue)
# 完成后停止智能体
await agent.stop()
# %%
# 启动实时对话
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 下面演示如何在用户和实时智能体之间建立实时对话。
#
# 这里以 FastAPI 为例,展示如何搭建实时对话的后端框架。
#
# **后端设置(服务端):**
#
# 后端需要:
#
# 1. 创建 WebSocket 端点接受前端连接
# 2. 在会话开始时创建 ``RealtimeAgent``
# 3. 将前端的 ``ClientEvents`` 转发给智能体
# 4. 将智能体的 ``ServerEvents`` 转发给前端
#
# .. code-block:: python
#
# from fastapi import FastAPI, WebSocket
# from agentscope.agent import RealtimeAgent
# from agentscope.realtime import (
# DashScopeRealtimeModel,
# ClientEvents,
# ServerEvents,
# )
#
# app = FastAPI()
#
# @app.websocket("/ws/{user_id}/{session_id}")
# async def websocket_endpoint(
# websocket: WebSocket,
# user_id: str,
# session_id: str,
# ):
# await websocket.accept()
#
# # 创建智能体消息队列
# frontend_queue = asyncio.Queue()
#
# # 创建智能体
# agent = RealtimeAgent(
# name="Assistant",
# sys_prompt="你是一个有用的助手。",
# model=DashScopeRealtimeModel(
# model_name="qwen3-omni-flash-realtime",
# api_key=os.getenv("DASHSCOPE_API_KEY"),
# ),
# )
#
# # 启动智能体
# await agent.start(frontend_queue)
#
# # 将智能体消息转发到前端
# async def send_to_frontend():
# while True:
# msg = await frontend_queue.get()
# await websocket.send_json(msg.model_dump())
#
# asyncio.create_task(send_to_frontend())
#
# # 接收前端消息并转发给智能体
# while True:
# data = await websocket.receive_json()
# client_event = ClientEvents.from_json(data)
# await agent.handle_input(client_event)
#
# **前端设置(客户端):**
#
# 前端需要:
#
# 1. 建立与后端的 WebSocket 连接
# 2. 发送 ``CLIENT_SESSION_CREATE`` 事件初始化会话
# 3. 捕获麦克风音频,通过 ``CLIENT_AUDIO_APPEND`` 事件发送
# 4. 接收并处理 ``ServerEvents``(例如播放音频、显示转录文本)
#
# .. code-block:: javascript
#
# // 连接 WebSocket
# const ws = new WebSocket('ws://localhost:8000/ws/user1/session1');
#
# ws.onopen = () => {
# // 创建会话
# ws.send(JSON.stringify({
# type: 'client_session_create',
# config: {
# instructions: '你是一个有用的助手。',
# user_name: 'User1'
# }
# }));
# };
#
# // 处理来自后端的消息
# ws.onmessage = (event) => {
# const data = JSON.parse(event.data);
# if (data.type === 'response_audio_delta') {
# // 播放音频块
# playAudio(data.delta);
# }
# };
#
# // 发送音频数据
# function sendAudioChunk(audioData) {
# ws.send(JSON.stringify({
# type: 'client_audio_append',
# session_id: 'session1',
# audio: audioData, // base64 编码
# format: { encoding: 'pcm16', sample_rate: 16000 }
# }));
# }
#
# 完整的工作示例请参见 AgentScope 仓库中的
# ``examples/agent/realtime_voice_agent/``。
# %%
# 多智能体实时对话
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# AgentScope 通过 ``ChatRoom`` 类支持多智能体实时交互。
#
# 请注意目前大多数实时模型 API 仅支持单用户交互,但 AgentScope 的架构设计支持多智能体和多用户,
# 当 API 能力扩展时即可应用到多智能体场景。
#
# 实时聊天室
# ----------------------------
#
# AgentScope 引入 ``ChatRoom`` 类来管理共享对话空间中的多个实时智能体。
# ChatRoom 提供:
#
# - 集中管理多个 ``RealtimeAgent`` 实例
# - 智能体之间的自动消息广播
# - 统一的前端通信消息队列
# - 房间内所有智能体的生命周期管理
#
# 使用 ChatRoom
# --------------
#
# ``ChatRoom`` 的用法与 ``RealtimeAgent`` 类似:
#
async def example_chat_room() -> None:
"""使用 ChatRoom 和多个实时智能体的示例。"""
from agentscope.pipeline import ChatRoom
from agentscope.agent import RealtimeAgent
from agentscope.realtime import DashScopeRealtimeModel
# 创建多个智能体
agent1 = RealtimeAgent(
name="Agent1",
sys_prompt="你是 Agent1一个有用的助手。",
model=DashScopeRealtimeModel(
model_name="qwen3-omni-flash-realtime",
api_key=os.getenv("DASHSCOPE_API_KEY"),
),
)
agent2 = RealtimeAgent(
name="Agent2",
sys_prompt="你是 Agent2一个有用的助手。",
model=DashScopeRealtimeModel(
model_name="qwen3-omni-flash-realtime",
api_key=os.getenv("DASHSCOPE_API_KEY"),
),
)
# 创建包含多个智能体的聊天室
chat_room = ChatRoom(agents=[agent1, agent2])
# 创建队列接收来自所有智能体的消息
outgoing_queue = asyncio.Queue()
# 启动聊天室
await chat_room.start(outgoing_queue)
# 处理来自前端的输入
# 聊天室会广播给所有智能体
from agentscope.realtime import ClientEvents
client_event = ClientEvents.ClientTextAppendEvent(
session_id="session1",
text="大家好!",
)
await chat_room.handle_input(client_event)
# 完成后停止聊天室
await chat_room.stop()
# %%
# 发展规划
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# 实时智能体功能目前为实验性质,正在积极开发中。未来计划包括:
#
# - 支持更多实时模型 API
# - 增强对话历史的记忆管理
# - 多用户语音交互支持
# - 改进 VAD语音活动检测配置
# - 更好的错误处理和恢复机制
#
#

View File

@@ -0,0 +1,215 @@
# -*- coding: utf-8 -*-
"""
.. _state:
状态/会话管理
=================================
在 AgentScope 中,**"状态"** 是指智能体在运行中某一时刻的数据快照,包括其当前的系统提示、记忆、上下文、装备的工具以及其他 **随时间变化** 的信息。
为了管理应用程序的状态AgentScope 设计实现了 **自动状态注册** 和 **会话级状态管理**,具有以下特性:
- 支持所有继承自 ``StateModule`` 的变量的 **自动状态注册**
- 支持使用自定义序列化/反序列化方法的 **手动状态注册**
- 支持 **会话/应用程序级别管理**
"""
import asyncio
import json
import os
from agentscope.agent import ReActAgent
from agentscope.formatter import DashScopeChatFormatter
from agentscope.memory import InMemoryMemory
from agentscope.message import Msg
from agentscope.model import DashScopeChatModel
from agentscope.module import StateModule
from agentscope.session import JSONSession
from agentscope.tool import Toolkit
# %%
# 状态模块
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# ``StateModule`` 类是 AgentScope 中状态管理的基础,提供三个基本函数:
#
# .. list-table:: ``StateModule`` 的方法
# :header-rows: 1
#
# * - 方法
# - 参数
# - 描述
# * - ``register_state``
# - | ``attr_name``,
# | ``custom_to_json`` (可选),
# | ``custom_from_json`` (可选)
# - 将属性注册为其状态,带有可选的序列化/反序列化函数。
# * - ``state_dict``
# -
# - 获取当前对象的状态字典
# * - ``load_state_dict``
# - | ``state_dict``,
# | ``strict`` (可选)
# - 将状态字典加载到当前对象
#
# 在 ``StateModule`` 的对象中,以下所有属性都将被视为其状态的一部分:
#
# - 继承自 ``StateModule`` 的 **属性**
# - 通过 ``register_state`` 方法注册的 **属性**
#
# 注意 ``StateModule`` 支持 **嵌套** 序列化和反序列化,例如下面的示例中,``ClassB`` 中包含一个 ``ClassA`` 的实例:
#
class ClassA(StateModule):
def __init__(self) -> None:
super().__init__()
self.cnt = 123
# 将 cnt 属性注册为状态
self.register_state("cnt")
class ClassB(StateModule):
def __init__(self) -> None:
super().__init__()
# 属性 "a" 继承自 StateModule将自动视为状态的一部分
self.a = ClassA()
# 手动将属性 "c" 注册为状态
self.c = "Hello, world!"
self.register_state("c")
obj_b = ClassB()
print("obj_b.a 的状态:")
print(obj_b.a.state_dict())
print("\nobj_b 的状态:")
print(json.dumps(obj_b.state_dict(), indent=4))
# %%
# 我们可以观察到 ``obj_b`` 的状态自动包含了其属性 ``a`` 的状态。
#
# 在 AgentScope 中,``AgentBase``、``MemoryBase``、``LongTermMemoryBase`` 和 ``Toolkit`` 类都继承自 ``StateModule``,因此支持自动和嵌套状态管理。
#
# 创建一个智能体
agent = ReActAgent(
name="Friday",
sys_prompt="你是一个名为 Friday 的助手。",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
),
formatter=DashScopeChatFormatter(),
memory=InMemoryMemory(),
toolkit=Toolkit(),
)
initial_state = agent.state_dict()
print("智能体的初始状态:")
print(json.dumps(initial_state, indent=4, ensure_ascii=False))
# %%
# 然后我们通过生成回复消息来改变其状态:
#
async def example_agent_state() -> None:
"""智能体状态管理示例。"""
await agent(Msg("user", "你好,智能体!", "user"))
print("生成回复后智能体的状态:")
print(json.dumps(agent.state_dict(), indent=4, ensure_ascii=False))
asyncio.run(example_agent_state())
# %%
# 现在我们将智能体的状态恢复到初始状态:
#
agent.load_state_dict(initial_state)
print("加载初始状态后:")
print(json.dumps(agent.state_dict(), indent=4, ensure_ascii=False))
# %%
# 会话管理
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# 会话Session是应用程序中状态的集合例如多个智能体。
#
# AgentScope 提供了 ``SessionBase`` 类,包含两个用于会话管理的抽象方法:``save_session_state`` 和 ``load_session_state``。
# 开发者可以继承该类来实现自己的状态保存方案,例如对接到自己的数据库或文件系统。
#
# 在 AgentScope 中,我们提供了基于 JSON 和文件系统的的会话类 ``JSONSession``
# 它会将状态保存到会化 ID 命名的 JSON 文件中,也可以从中加载状态。
#
# 保存会话状态
# -----------------------------------------
#
# 通过生成回复消息改变智能体状态
asyncio.run(example_agent_state())
print("\n智能体的状态:")
print(json.dumps(agent.state_dict(), indent=4, ensure_ascii=False))
# %%
# 然后我们将其保存到会话文件中:
session = JSONSession(
save_dir="./", # 保存所有session文件的目录
)
async def example_session() -> None:
"""会话管理示例。"""
# 可以保存多个状态,只需要输入的对象为 `StateModule` 的子类。
await session.save_session_state(
session_id="user_bob",
agent=agent,
)
print("保存的状态:")
with open("./user_bob.json", "r", encoding="utf-8") as f:
print(json.dumps(json.load(f), indent=4, ensure_ascii=False))
asyncio.run(example_session())
# %%
# 加载会话状态
# -----------------------------------------
# 然后我们可以从会话文件中加载状态:
#
async def example_load_session() -> None:
"""加载会话状态示例。"""
# 清空智能体状态
await agent.memory.clear()
print("当前智能体状态:")
print(json.dumps(agent.state_dict(), indent=4, ensure_ascii=False))
# 从会话文件中加载状态
await session.load_session_state(
session_id="user_bob",
# 这里使用的关键词参数必须与 `save_session_state` 中的参数一致
agent=agent,
)
print("加载会话状态后智能体的状态:")
print(json.dumps(agent.state_dict(), indent=4, ensure_ascii=False))
asyncio.run(example_load_session())
# %%
# 此时我们可以观察到智能体的状态已经恢复到之前保存的状态。
#

View File

@@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
"""
.. _studio:
AgentScope Studio
=========================
AgentScope Studio 是一个本地部署的 Web 应用程序,它
- 为智能体应用程序的开发提供 **项目管理**
- 为运行中的应用程序提供 **可视化** 追踪
- 内置一个为 "Friday" 的 **智能体**,支持用户二次开发
.. note:: Studio 正在快速开发中,更多功能即将推出!
.. figure:: ../../_static/images/studio_home.webp
:width: 100%
:alt: AgentScope Studio 主页
:class: bordered-image
:align: center
AgentScope Studio 主页
快速开始
~~~~~~~~~~~~~~~~~~~~~~~~
AgentScope Studio 通过 ``npm`` 安装:
.. code-block:: bash
npm install -g @agentscope/studio
使用以下命令启动 Studio
.. code-block:: bash
as_studio
要将应用程序连接到 Studio请在 ``agentscope.init`` 函数中使用 ``studio_url`` 参数:
.. code-block:: python
import agentscope
agentscope.init(studio_url="http://localhost:3000")
# 应用程序代码
...
然后可以在 Studio 中看到该应用程序,如下所示:
.. figure:: ../../_static/images/studio_project.webp
:width: 100%
:alt: 项目管理
:class: bordered-image
:align: center
AgentScope Studio 中的项目管理
关于应用程序的详细信息,例如 token 使用情况、模型调用和追踪信息,都可以在 Studio 中查看。
.. figure:: ../../_static/images/studio_run.webp
:width: 100%
:alt: AgentScope Studio 运行页面
:class: bordered-image
:align: center
AgentScope Studio 中的应用程序可视化
Friday 智能体
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Friday 是由 AgentScope 构建的实验性本地部署智能体,旨在
- 回答关于 AgentScope 开发的问题,
- 为开发者提供便捷的二次开发环境,
- 集成 AgentScope 中所有可用功能以构建更强大的智能体,以及
- 持续测试和集成 AgentScope 中的高级功能。
.. note:: 我们非常欢迎开源社区贡献并改进 Friday欢迎在我们的 `GitHub 仓库 <https://github.com/agentscope-ai/agentscope>`_ 上提出问题或拉取请求。
我们正在持续改进 Friday目前它集成了 AgentScope 中的以下功能:
.. list-table::
:header-rows: 1
* - 功能
- 状态
- 进一步阅读
- 描述
* - 元工具
- ✅
- :ref:`tool`
- 分组工具管理,允许智能体自己更改装备的工具。
* - 智能体钩子
- ✅
- :ref:`hook`
- 使用钩子将打印消息转发到前端。
* - 智能体中断
- ✅
- :ref:`agent`
- 允许用户通过后处理中断智能体的回复过程。
* - 截断提示
- ✅
- :ref:`prompt`
- 支持使用预设的最大 token 限制截断提示。
* - 状态和会话管理
- ✅
- :ref:`state`
- 智能体的自动状态管理和会话管理,在不同运行之间维护状态。
* - 长期记忆
- 🚧
- :ref:`memory`
- 支持长期记忆管理。
"""

View File

@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
"""
.. _token:
Token 计数
=========================
AgentScope 在 ``agentscope.token`` 模块下提供了 token 计数功能,用于计算给定消息中
的 token 数量,允许开发者在调用 LLM API 前预估 token 数量。
具体而言,可用的 token 计数器如下:
.. list-table::
:header-rows: 1
* - LLM API
- 类
- 实现方式
- 支持图像数据
- 支持工具
* - Anthropic
- ``AnthropicTokenCounter``
- 官方 API
- ✅
- ✅
* - OpenAI
- ``OpenAITokenCounter``
- 本地计算
- ✅
- ✅
* - Gemini
- ``GeminiTokenCounter``
- 官方 API
- ✅
- ✅
* - HuggingFace
- ``HuggingFaceTokenCounter``
- 基于Tokenizer计算
- 取决于模型
- 取决于模型
.. tip:: 格式化器模块已集成了 token 计数器以支持提示截断。更多详细信息请参考 :ref:`prompt` 部分。
.. note::
- 对于 DashScope 模型,目前 dashscope 库不提供 token 计数 API。因此我们建议使用 HuggingFace token 计数器代替。
- 对于 OpenAI 模型,由于官方未提供 token 计数 API因此可能存在与官方计算结果不一致的情况。
下面展示使用 OpenAI token 计数器计算 token 数量的示例:
"""
import asyncio
from agentscope.token import OpenAITokenCounter
async def example_token_counting():
# 示例消息
messages = [
{"role": "user", "content": "Hello!"},
{"role": "assistant", "content": "Hi, how can I help you?"},
]
# OpenAI token 计数
openai_counter = OpenAITokenCounter(model_name="gpt-4.1")
n_tokens = await openai_counter.count(messages)
print(f"Token 数量: {n_tokens}")
asyncio.run(example_token_counting())
# %%
# 进一步阅读
# ------------------------------
#
# - :ref:`prompt`
#

View File

@@ -0,0 +1,457 @@
# -*- coding: utf-8 -*-
"""
.. _tool:
工具
=========================
为了确保准确可靠的工具解析AgentScope 全面支持工具 API 的使用,具有以下特性:
- 支持从文档字符串 **自动** 解析工具函数
- 支持 **同步和异步** 工具函数
- 支持 **流式** 工具响应(同步或异步生成器)
- 支持对工具 JSON Schema 的 **动态扩展**
- 支持用户实时 **中断** 工具的执行
- 支持智能体的 **自主工具管理**
所有上述功能都由 AgentScope 中的 ``Toolkit`` 类实现,该类负责管理工具函数及其执行。
.. tip:: MCP模型上下文协议的支持请参考 :ref:`mcp` 部分。
"""
import asyncio
import inspect
import json
from typing import Any, AsyncGenerator
from pydantic import BaseModel, Field
import agentscope
from agentscope.message import TextBlock, ToolUseBlock
from agentscope.tool import ToolResponse, Toolkit, execute_python_code
# %%
# 工具函数
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 在 AgentScope 中,工具函数是一个 Python 的可调用对象,它
#
# - 返回一个 ``ToolResponse`` 对象或产生 ``ToolResponse`` 对象的生成器(可以是异步或同步)
# - 具有描述工具功能和参数的文档字符串
#
# 工具函数的模板如下:
def tool_function(a: int, b: str) -> ToolResponse:
"""{函数描述}
Args:
a (int):
{第一个参数的描述}
b (str):
{第二个参数的描述}
"""
# %%
# .. tip:: 实例方法和类方法也可以用作工具函数,``Toolkit`` 中将自动忽略 ``self`` 和 ``cls`` 参数。
#
# AgentScope 在 ``agentscope.tool`` 模块下提供了几个内置工具函数,如 ``execute_python_code``、``execute_shell_command`` 和文本文件读写函数。
#
print("内置工具函数:")
for _ in agentscope.tool.__all__:
if _ not in ["Toolkit", "ToolResponse"]:
print(_)
# %%
# 工具模块Toolkit
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# ``Toolkit`` 类设计用于管理工具函数,从文档字符串中提取它们的 JSON Schema并为工具执行提供统一接口。
#
# 基本用法
# ------------------------------
# ``Toolkit`` 类的基本功能是注册工具函数并执行它们。
#
# 准备一个自定义工具函数
async def my_search(query: str, api_key: str) -> ToolResponse:
"""一个简单的示例工具函数。
Args:
query (str):
搜索查询。
api_key (str):
用于身份验证的 API 密钥。
"""
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"正在使用 API 密钥 '{api_key}' 搜索 '{query}'",
),
],
)
# 在工具模块中注册工具函数
toolkit = Toolkit()
toolkit.register_tool_function(my_search)
# %%
# 注册工具函数后,可以通过调用 ``get_json_schemas`` 方法获取其 JSON Schema。
#
print("工具 JSON Schemas")
print(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))
# %%
# ``Toolkit`` 还允许开发者为工具函数预设参数,这对于 API 密钥或其他敏感信息特别有用。
#
# 先清空工具模块
toolkit.clear()
# 使用预设关键字参数注册工具函数
toolkit.register_tool_function(my_search, preset_kwargs={"api_key": "xxx"})
print("带预设参数的工具 JSON Schemas")
print(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))
# %%
# 预设参数后,该参数将从 JSON schema 中被移除,并在工具调用时自动传递给该工具函数。
#
# 在 ``Toolkit`` 中,``call_tool_function`` 方法以 ``ToolUseBlock`` 作为输入执行指定的工具函数,统一返回一个 **异步生成器**,该生成器产生 ``ToolResponse`` 对象。
#
# .. note:: AgentScope 中,流式返回的工具函数应该是 **“累积的”**,即当前块的内容应包含之前所有块的内容。
#
async def example_tool_execution() -> None:
"""工具调用执行示例。"""
res = await toolkit.call_tool_function(
ToolUseBlock(
type="tool_use",
id="123",
name="my_search",
input={"query": "AgentScope"},
),
)
# 非流式返回的工具函数只有一个 ToolResponse 返回
print("工具响应:")
async for tool_response in res:
print(tool_response)
asyncio.run(example_tool_execution())
# %%
# 动态扩展 JSON Schema
# --------------------------------------
#
# Toolkit 允许通过调用 ``set_extended_model`` 方法动态扩展工具函数的 JSON schemas。
# 这种功能允许开发者在不修改工具函数原始定义的情况下,向工具函数添加更多参数。
#
# .. tip:: 相关场景包括动态 :ref:`structured-output` 和 CoT思维链推理
#
# .. note:: 要扩展的工具函数应该接受可变关键字参数(``**kwargs``),以便附加字段可以传递给它。
#
# 以 CoT 推理为例,我们可以用 ``thinking`` 字段扩展所有工具函数,允许智能体总结当前状态然后决定下一步做什么。
#
# 示例工具函数
def tool_function(**kwargs: Any) -> ToolResponse:
"""一个工具函数"""
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"接收到的参数:{kwargs}",
),
],
)
# 添加一个思考字段,以便智能体在给出其他参数之前可以思考。
class ThinkingModel(BaseModel):
"""用于附加字段的 Pydantic 模型。"""
thinking: str = Field(
description="总结当前状态并决定下一步做什么。",
)
# 注册
toolkit.set_extended_model("my_search", ThinkingModel)
print("扩展后的 JSON Schema")
print(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))
# %%
# 中断工具执行
# ------------------------------
# ``Toolkit`` 类支持 **异步工具函数** 的 **执行中断**,并提供 **面向智能体的后处理机制**。
# 这种中断基于 asyncio 取消机制实现,其后处理过程根据工具函数的返回类型而有所不同。
#
# .. note:: 对于同步(工具)函数,它们的执行无法通过 asyncio 取消来中断。因此其中断在智能体内而不是工具模块内处理。
# 有关更多信息,请参考 :ref:`agent` 部分。
#
# 具体来说,如果工具函数返回 ``ToolResponse`` 对象,将产生一个带有中断消息的 ``ToolResponse`` 对象。
# 这样智能体可以观察到这一中断并相应地处理它。
# 此外,该 ``ToolResponse`` 对象中的 ``is_interrupted`` 将设置为 ``True``,外部调用者可以决定是否将 ``CancelledError`` 异常抛出到外层。
#
# 可以被中断的异步工具函数示例如下:
#
async def non_streaming_function() -> ToolResponse:
"""一个可以被中断的非流式工具函数。"""
await asyncio.sleep(1) # 模拟长时间运行的任务
# 为演示目的模拟中断
raise asyncio.CancelledError()
# 由于取消,以下代码不会被执行
return ToolResponse(
content=[
TextBlock(
type="text",
text="运行成功!",
),
],
)
async def example_tool_interruption() -> None:
"""工具中断示例。"""
toolkit = Toolkit()
toolkit.register_tool_function(non_streaming_function)
res = await toolkit.call_tool_function(
ToolUseBlock(
type="tool_use",
id="123",
name="non_streaming_function",
input={},
),
)
async for tool_response in res:
print("工具响应:")
print(tool_response)
print("中断标志:")
print(tool_response.is_interrupted)
asyncio.run(example_tool_interruption())
# %%
# 对于流式工具函数,``Toolkit`` 将把中断消息附加到中断发生时的 ``ToolResponse`` 上。
# 通过这种方式,智能体可以观察到工具在中断前返回的内容。
#
# 中断流式工具函数的示例如下:
#
async def streaming_function() -> AsyncGenerator[ToolResponse, None]:
"""一个可以被中断的流式工具函数。"""
# 模拟一块响应
yield ToolResponse(
content=[
TextBlock(
type="text",
text="1234",
),
],
stream=True,
)
# 模拟中断
raise asyncio.CancelledError()
# 由于取消,以下代码不会被执行
yield ToolResponse(
content=[
TextBlock(
type="text",
text="123456789",
),
],
)
async def example_streaming_tool_interruption() -> None:
"""流式工具中断示例。"""
toolkit = Toolkit()
toolkit.register_tool_function(streaming_function)
res = await toolkit.call_tool_function(
ToolUseBlock(
type="tool_use",
id="xxx",
name="streaming_function",
input={},
),
)
i = 0
async for tool_response in res:
print(f"{i}")
print(tool_response)
print("中断标志:", tool_response.is_interrupted, "\n")
i += 1
asyncio.run(example_streaming_tool_interruption())
# %%
# 自动工具管理
# -------------------------------------
# .. image:: https://img.alicdn.com/imgextra/i3/O1CN013cvRpO27MfesMsTeh_!!6000000007783-2-tps-840-521.png
# :width: 100%
# :align: center
# :alt: 自动工具管理
#
#
# ``Toolkit`` 类通过引入 **工具组** Group 的概念,以及名为 ``reset_equipped_tools`` 的 **元工具函数** Meta Tool 来支持 **自动工具管理** 。
#
# 工具组是一组相关工具函数的集合,例如浏览器使用工具、地图服务工具等,它们将被一起管理。工具组有激活和非激活两种状态,
# 只有工具组被激活,其中的工具函数才对智能体可见,即可以通过 ``toolkit.get_json_schemas()`` 方法访问。
#
# 注意有一个名为 ``basic`` 的特殊组,它始终处于激活状态,注册工具时如果未指定组名,则工具函数将默认添加到此组。
#
# .. tip:: ``basic`` 组确保开发者不需要“组管理”的功能时,工具的基本使用不会受到影响。
#
# 现在我们尝试创建一个名为 ``browser_use`` 的工具组,其中包含一些网页浏览工具。
#
# 我们创建一些浏览器操作相关的工具
def navigate(url: str) -> ToolResponse:
"""导航到网页。
Args:
url (str):
要导航到的网页的 URL。
"""
pass
def click_element(element_id: str) -> ToolResponse:
"""点击网页上的元素。
Args:
element_id (str):
要点击的元素的 ID。
"""
pass
toolkit = Toolkit()
# 创建一个名为 browser_use 的工具组
toolkit.create_tool_group(
group_name="browser_use",
description="用于网页浏览的工具函数。",
active=False,
# 使用这些工具时的注意事项
notes="""1. 使用 ``navigate`` 打开网页。
2. 当需要用户身份验证时,请向用户询问凭据
3. ...""",
)
toolkit.register_tool_function(navigate, group_name="browser_use")
toolkit.register_tool_function(click_element, group_name="browser_use")
# 我们也可以注册一些基本工具
toolkit.register_tool_function(execute_python_code)
# %%
# 此时 ``browser_use`` 未被激活,如果我们检查工具 JSON schema只能看到 ``execute_python_code`` 工具:
print("此时对智能体可见的工具函数 JSON Schemas")
print(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))
# %%
# 使用 ``update_tool_groups`` 方法激活或停用工具组:
toolkit.update_tool_groups(group_names=["browser_use"], active=True)
print("激活后对智能体可见的工具函数 JSON Schemas")
print(json.dumps(toolkit.get_json_schemas(), indent=4, ensure_ascii=False))
# %%
# 此外,``Toolkit`` 提供了一个名为 ``reset_equipped_tools`` 的元工具函数,它会将所有组名(除了 "basic")作为一个 bool 型的参数,
# 让智能体调用该工具来决定要激活哪些工具组:
#
# .. note:: 在 ``ReActAgent`` 类的实现中,只需要在构造函数中将 ``enable_meta_tool`` 设置为 ``True`` 即可启用元工具函数。
#
# 注册元工具函数
toolkit.register_tool_function(toolkit.reset_equipped_tools)
reset_equipped = next(
tool
for tool in toolkit.get_json_schemas()
if tool["function"]["name"] == "reset_equipped_tools"
)
print("``reset_equipped_tools`` 函数的 JSON schema")
print(
json.dumps(
reset_equipped,
indent=4,
ensure_ascii=False,
),
)
# %%
# 当智能体调用 ``reset_equipped_tools`` 时,对应工具组将被激活,同时返回的结果中将包含工具的使用注意事项。
#
async def mock_agent_reset_tools() -> None:
"""模拟智能体调用 reset_equipped_tools 函数。"""
res = await toolkit.call_tool_function(
ToolUseBlock(
type="tool_use",
id="456",
name="reset_equipped_tools",
input={
"browser_use": True, # 激活浏览器使用工具组
},
),
)
async for tool_response in res:
print("工具响应中的文字返回:")
print(tool_response.content[0]["text"])
asyncio.run(mock_agent_reset_tools())
# %%
# 此外,``Toolkit`` 还通过 ``get_activated_notes`` 函数提供已经被激活了的工具组的 notes开发者也可以将其组装到智能体的系统提示中从而达到动态管理工具的作用。
#
# .. tip:: 自动工具管理功能已在 ``ReActAgent`` 类中实现,有关更多详细信息,请参考 :ref:`agent` 部分。
#
# 再创建一个工具组
toolkit.create_tool_group(
group_name="map_service",
description="谷歌地图服务工具。",
active=True,
notes="""1. 使用 ``get_location`` 获取地点的位置。
2. ...""",
)
print("激活工具组的汇总注意事项:")
print(toolkit.get_activated_notes())
# %%
# 进一步阅读
# ---------------------
# - :ref:`agent`
# - :ref:`state`
# - :ref:`mcp`
#

View File

@@ -0,0 +1,213 @@
# -*- coding: utf-8 -*-
"""
.. _tracing:
追踪
==============================
AgentScope 实现了基于 OpenTelemetry 的追踪来监控和调试
智能体应用程序的执行,具有以下特性
- 为 LLM、工具、智能体、格式化器等提供内置追踪
- 支持错误和异常追踪
- 在 AgentScope Studio 中提供原生追踪 **可视化**
- 支持连接到 **第三方平台**,如阿里云云监控、`Arize-Phoenix <https://github.com/Arize-ai/phoenix>`_、`Langfuse <https://langfuse.com/>`_ 等
设置
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. note:: 连接到 :ref:`studio` 或第三方平台应该在应用程序开始时通过调用 ``agentscope.init`` 函数完成。
AgentScope Studio
---------------------------------------
.. figure:: ../../_static/images/studio_tracing.webp
:width: 100%
:alt: AgentScope Studio 追踪页面
:class: bordered-image
:align: center
*AgentScope Studio 中的追踪页面*
当连接到 AgentScope Studio 时,只需在 ``agentscope.init`` 函数中提供 ``studio_url`` 参数。
.. code-block:: python
import agentscope
agentscope.init(studio_url="http://xxx:port")
第三方平台
---------------------------------------
要连接到第三方追踪平台,请在 ``agentscope.init`` 函数中设置 ``tracing_url`` 参数。
``tracing_url`` 是您的 OpenTelemetry 收集器或任何支持 OTLPOpenTelemetry 协议)的服务器 URL。
.. code-block:: python
import agentscope
# 连接到 OpenTelemetry 兼容的后端
agentscope.init(tracing_url="https://your-tracing-backend:port/traces")
以阿里云云监控、Arize-Phoenix 和 Langfuse 为例:
**阿里云云监控Alibaba Cloud CloudMonitor**:全托管可观测平台。
.. code-block:: python
:caption: 连接到阿里云云监控
agentscope.init(tracing_url="https://tracing-cn-hangzhou.arms.aliyuncs.com/adapt_xxx/api/otlp/traces")
.. tip::
**获取 Endpoint** 在 `ARMS 控制台 <https://arms.console.aliyun.com/>`_ 的 **接入中心** > **OpenTelemetry** 中,
根据实际部署地域选择对应的 **公网接入点**。可通过环境变量 ``OTEL_SERVICE_NAME`` 自定义应用名称。
阿里云云监控可通过 `LoongSuite <https://github.com/alibaba/loongsuite-python-agent>`_ 探针提供零侵入的自动化接入。
更多信息请参考 `云监控文档 <https://help.aliyun.com/zh/cms/cloudmonitor-2-0/user-guide/model-application>`_。
**Arize-Phoenix**:需要在环境变量中设置 ``PHOENIX_API_KEY``。
.. code-block:: python
:caption: 连接到 Arize Phoenix
# Arize Phoenix 集成
import os
PHOENIX_API_KEY = os.environ.get("PHOENIX_API_KEY")
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"api_key={PHOENIX_API_KEY}"
agentscope.init(tracing_url="https://app.phoenix.arize.com/v1/traces")
**LangFuse**:需要在环境变量中设置 ``LANGFUSE_PUBLIC_KEY`` 和 ``LANGFUSE_SECRET_KEY``。
授权头是使用这些密钥构建的。
.. code-block:: python
:caption: 连接到 LangFuse
import os, base64
LANGFUSE_PUBLIC_KEY = os.environ["LANGFUSE_PUBLIC_KEY"]
LANGFUSE_SECRET_KEY = os.environ["LANGFUSE_SECRET_KEY"]
LANGFUSE_AUTH_STRING = f"{LANGFUSE_PUBLIC_KEY}:{LANGFUSE_SECRET_KEY}"
LANGFUSE_AUTH = base64.b64encode(LANGFUSE_AUTH_STRING.encode("utf-8")).decode("ascii")
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"Authorization=Basic {LANGFUSE_AUTH}"
# 欧盟数据区域
agentscope.init(tracing_url="https://cloud.langfuse.com/api/public/otel/v1/traces")
# 美国数据区域
# agentscope.init(tracing_url="https://us.cloud.langfuse.com/api/public/otel/v1/traces")
自定义追踪
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
如前所述AgentScope 中的追踪功能是基于 OpenTelemetry 实现的。
这意味着 AgentScope 中的追踪与开发者基于 OpenTelemetry SDK 自己实现的的追踪代码**完全兼容**。
此外AgentScope 内置了以下装饰器来追踪相应的模块,它们对不同类的特殊属性,以及返回值做了相应的特殊处理:
- ``@trace_llm``:追踪 ``ChatModelBase`` 子类的 ``__call__`` 函数
- ``@trace_reply``:追踪 ``AgentBase`` 子类的 ``reply`` 函数
- ``@trace_format``:追踪 ``FormatterBase`` 子类的 ``format`` 函数
- ``@trace``:追踪一般函数
追踪大语言模型
----------------------------------------
``@trace_llm`` 装饰器用于追踪 ``ChatModelBase`` 类的 ``__call__`` 函数。
.. code-block:: python
:caption: 追踪新的 ChatModel 类
class ExampleChatModel(ChatModelBase):
\"\"\"示例模型\"\"\"
...
@trace_llm
async def __call__(
self,
*args: Any,
**kwargs: Any,
) -> AsyncGenerator[ChatResponse, None] | ChatResponse:
\"\"\"LLM 调用\"\"\"
...
追踪智能体
----------------------------------------
``@trace_reply`` 装饰器用于追踪智能体的 `reply` 函数。
.. code-block:: python
:caption: 追踪新的 Agent 类
class ExampleAgent(AgentBase):
\"\"\"示例智能体类\"\"\"
@trace_reply
async def reply(self, *args: Any, **kwargs: Any) -> Msg:
\"\"\"回复消息。\"\"\"
...
追踪格式化器
----------------------------------------
``@trace_format`` 装饰器用于格式化器实现并追踪 `format` 函数。
.. code-block:: python
:caption: 追踪新的 Formatter 类
class ExampleFormatter(FormatterBase):
\"\"\"简单的示例格式化器类\"\"\"
@trace_format
async def format(self, *args: Any, **kwargs: Any) -> list[dict]:
\"\"\"示例格式化\"\"\"
一般函数追踪
----------------------------------------
``@trace`` 装饰器与上述装饰器不同,它是一个通用的追踪装饰器,可以应用于任何函数。
它需要一个 `name` 参数来标识被追踪的函数,并且可以追踪各种类型的函数,包括:
- 同步函数
- 同步生成器函数
- 异步函数
- 异步生成器函数
.. code-block:: python
:caption: 一般追踪示例
# 1. 同步函数
@trace(name='simple_function')
def simple_function(name: str, age: int) -> str:
\"\"\"带有自动追踪的简单函数。\"\"\"
return f"你好, {name}! 你的年龄是 {age} 岁。"
# 2. 同步生成器函数
@trace(name='number_generator')
def number_generator(n: int) -> Generator[int, None, None]:
\"\"\"生成从 0 到 n-1 的数字。\"\"\"
for i in range(n):
yield i
# 3. 异步函数
@trace(name='async_function')
async def async_function(data: dict) -> dict:
\"\"\"异步处理数据。\"\"\"
return {"processed": data}
# 4. 异步生成器函数
@trace(name='async_stream')
async def async_stream(n: int) -> AsyncGenerator[str, None]:
\"\"\"异步生成数据流。\"\"\"
for i in range(n):
yield f"data_{i}"
"""

View File

@@ -0,0 +1,242 @@
# -*- coding: utf-8 -*-
"""
.. _tts:
TTS
====================
AgentScope 为多个 API 提供商的文本转语音TTS模型提供了统一接口。
本章节演示如何在 AgentScope 中使用 TTS 模型。
AgentScope 支持以下 TTS API
.. list-table:: 内置 TTS 模型
:header-rows: 1
* - API
- 类
- 流式输入
- 非流式输入
- 流式输出
- 非流式输出
* - DashScope 实时 API
- ``DashScopeRealtimeTTSModel``
- ✅
- ✅
- ✅
- ✅
* - DashScope CosyVoice 实时 API
- ``DashScopeCosyVoiceRealtimeTTSModel``
- ✅
- ✅
- ✅
- ✅
* - DashScope API
- ``DashScopeTTSModel``
- ❌
- ✅
- ✅
- ✅
* - DashScope CosyVoice API
- ``DashScopeCosyVoiceTTSModel``
- ❌
- ✅
- ✅
- ✅
* - OpenAI API
- ``OpenAITTSModel``
- ❌
- ✅
- ✅
- ✅
* - Gemini API
- ``GeminiTTSModel``
- ❌
- ✅
- ✅
- ✅
.. note:: AgentScope TTS 模型中的流式输入和输出都是累积式的。
**选择合适的模型:**
- **使用非实时 TTS**:当已有完整文本时(例如预先编写的响应、完整的 LLM 输出)
- **使用实时 TTS**:当文本是逐步生成时(例如 LLM 的流式返回),以获得更低的延迟
"""
import asyncio
import os
from agentscope.agent import ReActAgent, UserAgent
from agentscope.formatter import DashScopeChatFormatter
from agentscope.message import Msg
from agentscope.model import DashScopeChatModel
from agentscope.tts import (
DashScopeRealtimeTTSModel,
DashScopeTTSModel,
)
# %%
# 非实时 TTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 非实时 TTS 模型处理完整的文本输入,使用起来最简单,可以直接调用它们的 ``synthesize()`` 方法。
#
# 以 DashScope TTS 模型为例:
async def example_non_realtime_tts() -> None:
"""使用非实时 TTS 模型的基本示例。"""
# DashScope TTS 示例
tts_model = DashScopeTTSModel(
api_key=os.environ.get("DASHSCOPE_API_KEY", ""),
model_name="qwen3-tts-flash",
voice="Cherry",
stream=False, # 非流式输出
)
msg = Msg(
name="assistant",
content="你好,这是 DashScope TTS。",
role="assistant",
)
tts_response = await tts_model.synthesize(msg)
# tts_response.content 包含一个带有 base64 编码音频数据的音频块
print("音频数据长度:", len(tts_response.content["source"]["data"]))
asyncio.run(example_non_realtime_tts())
# %%
# **流式输出以降低延迟:**
#
# 当 ``stream=True`` 时,模型会逐步返回音频块,允许
# 您在合成完成前开始播放。这减少了感知延迟。
#
async def example_non_realtime_tts_streaming() -> None:
"""使用带流式输出的非实时 TTS 模型的示例。"""
# 使用流式输出的 DashScope TTS 示例
tts_model = DashScopeTTSModel(
api_key=os.environ.get("DASHSCOPE_API_KEY", ""),
model_name="qwen3-tts-flash",
voice="Cherry",
stream=True, # 启用流式输出
)
msg = Msg(
name="assistant",
content="你好,这是带流式输出的 DashScope TTS。",
role="assistant",
)
# 合成并接收用于流式输出的异步生成器
async for tts_response in await tts_model.synthesize(msg):
# 处理到达的每个音频块
print("接收到的音频块长度:", len(tts_response.content["source"]["data"]))
asyncio.run(example_non_realtime_tts_streaming())
# %%
# 实时 TTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 实时 TTS 模型专为文本增量生成的场景设计,
# 例如流式 LLM 响应。这通过在完整文本准备好之前
# 开始音频合成,实现尽可能低的延迟。
#
# **核心概念:**
#
# - **有状态处理**:实时 TTS 为单个流式会话维护状态,
# 由 ``msg.id`` 标识。一次只能有一个流式会话处于活动状态。
# - **两种方法**
#
# - ``push(msg)``:非阻塞方法,提交文本块并立即返回。
# 如果有可用的部分音频,可能会返回。
# - ``synthesize(msg)``:阻塞方法,完成会话并返回
# 所有剩余的音频。当 ``stream=True`` 时,返回异步生成器。
#
# .. code-block:: python
#
# async def example_realtime_tts_streaming():
# tts_model = DashScopeRealtimeTTSModel(
# api_key=os.environ.get("DASHSCOPE_API_KEY", ""),
# model_name="qwen3-tts-flash-realtime",
# voice="Cherry",
# stream=False,
# )
#
# # 实时 tts 模型接收累积的文本块
# res = await tts_model.push(msg_chunk_1) # 非阻塞
# res = await tts_model.push(msg_chunk_2) # 非阻塞
# ...
# res = await tts_model.synthesize(final_msg) # 阻塞,获取所有剩余音频
#
# 在初始化时设置 ``stream=True`` 时,``synthesize()`` 方法返回 ``TTSResponse`` 对象的异步生成器,允许您在音频块到达时处理它们。
#
#
# 与 ReActAgent 集成
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AgentScope 智能体在提供 TTS 模型时,可以自动将其响应合成为语音。
# 这与实时和非实时 TTS 模型都能无缝协作。
#
# **工作原理:**
#
# 1. 智能体生成文本响应(可能从 LLM 流式传输)
# 2. TTS 模型自动将文本合成为音频
# 3. 合成的音频附加到 ``Msg`` 对象的 ``speech`` 字段
# 4. 音频在智能体的 ``self.print()`` 方法期间播放
#
async def example_agent_with_tts() -> None:
"""使用带 TTS 的 ReActAgent 的示例。"""
# 创建启用了 TTS 的智能体
agent = ReActAgent(
name="Assistant",
sys_prompt="你是一个有用的助手。",
model=DashScopeChatModel(
api_key=os.environ["DASHSCOPE_API_KEY"],
model_name="qwen-max",
stream=True,
),
formatter=DashScopeChatFormatter(),
# 启用 TTS
tts_model=DashScopeRealtimeTTSModel(
api_key=os.getenv("DASHSCOPE_API_KEY"),
model_name="qwen3-tts-flash-realtime",
voice="Cherry",
),
)
user = UserAgent("User")
# 像正常情况一样构建对话
msg = None
while True:
msg = await agent(msg)
msg = await user(msg)
if msg.get_text_content() == "exit":
break
# %%
# 自定义 TTS 模型
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 可以通过继承 ``TTSModelBase`` 来创建自定义 TTS 实现。
# 基类为实时和非实时 TTS 模型提供了灵活的接口。
# 我们使用属性 ``supports_streaming_input`` 来指示 TTS 模型是否为实时模型。
#
# 对于实时 TTS 模型,需要实现 ``connect``、``close``、``push`` 和 ``synthesize`` 方法来处理 API 的生命周期和流式输入。
#
# 而对于非实时 TTS 模型,只需实现 ``synthesize`` 方法。
#
# 进一步阅读
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# - :ref:`agent` - 了解更多关于 AgentScope 中的智能体
# - :ref:`message` - 理解 AgentScope 中的消息格式
# - API 参考::class:`agentscope.tts.TTSModelBase`
#

View File

@@ -0,0 +1,249 @@
# -*- coding: utf-8 -*-
"""
.. _tuner:
Tuner
=================
AgentScope 提供了 ``tuner`` 模块用于通过强化学习RL训练智能体应用。
本教程将带你系统了解如何利用 ``tuner`` 提升智能体在特定任务上的表现,包括:
- 介绍 ``tuner`` 的核心组件
- 演示调优流程所需的关键代码实现
- 展示调优流程的配置与运行方法
主要组件
~~~~~~~~~~~~~~~~~~~
``tuner`` 模块为智能体训练工作流引入了三大核心组件:
- **任务数据集**:用于训练和评估智能体的任务集合。
- **工作流函数**:封装被调优智能体应用的决策逻辑。
- **评判函数**:评估智能体在特定任务上的表现,并为调优过程提供奖励信号。
此外,``tuner`` 还提供了若干用于自定义调优流程的配置类,包括:
- **TunerModelConfig**:用于指定被调优模型的相关配置。
- **AlgorithmConfig**:用于指定强化学习算法(如 GRPO、PPO 等)及其参数。
实现流程
~~~~~~~~~~~~~~~~~~~
本节以一个简单的数学智能体为例,演示如何用 ``tuner`` 进行训练。
任务数据集
--------------------
任务数据集包含用于训练和评估的任务集合。
``tuner`` 的任务数据集采用 Huggingface `datasets <https://huggingface.co/docs/datasets/quickstart>`_ 格式,并通过 ``datasets.load_dataset`` 加载。例如:
.. code-block:: text
my_dataset/
├── train.jsonl # 训练样本
└── test.jsonl # 测试样本
假设 `train.jsonl` 内容如下:
.. code-block:: json
{"question": "2 + 2 等于多少?", "answer": "4"}
{"question": "4 + 4 等于多少?", "answer": "8"}
在开始调优前,你可以用如下方法来确定你的数据集能够被正确加载:
.. code-block:: python
from agentscope.tuner import DatasetConfig
dataset = DatasetConfig(path="my_dataset", split="train")
dataset.preview(n=2)
# 输出前两个样本以验证数据集加载正确
# [
# {
# "question": "2 + 2 等于多少?",
# "answer": "4"
# },
# {
# "question": "4 + 4 等于多少?",
# "answer": "8"
# }
# ]
工作流函数
--------------------
工作流函数定义了智能体与环境的交互方式和决策过程。所有工作流函数需遵循 ``agentscope.tuner.WorkflowType`` 的输入/输出签名。
以下是一个用 ReAct 智能体回答数学问题的简单工作流函数示例:
"""
from typing import Dict, Optional
from agentscope.agent import ReActAgent
from agentscope.formatter import OpenAIChatFormatter
from agentscope.message import Msg
from agentscope.model import ChatModelBase
from agentscope.tuner import WorkflowOutput
async def example_workflow_function(
task: Dict,
model: ChatModelBase,
auxiliary_models: Optional[Dict[str, ChatModelBase]] = None,
) -> WorkflowOutput:
"""一个用于调优的工作流函数示例。
Args:
task (`Dict`): 任务信息。
model (`ChatModelBase`): 智能体使用的对话模型。
auxiliary_models (`Optional[Dict[str, ChatModelBase]]`):
用于辅助的额外对话模型,一般用于多智能体场景下模拟其他非训练智能体的行为。
Returns:
`WorkflowOutput`: 工作流生成的输出。
"""
agent = ReActAgent(
name="react_agent",
sys_prompt="你是一个善于解决数学问题的智能体。",
model=model,
formatter=OpenAIChatFormatter(),
)
response = await agent.reply(
msg=Msg(
"user",
task["question"],
role="user",
), # 从任务中提取问题
)
return WorkflowOutput( # 返回响应结果
response=response,
)
# %%
# 你可以直接用任务字典和日常调试使用的 ``DashScopeChatModel`` / ``OpenAIChatModel`` 运行此工作流函数,从而在正式训练前测试其流程的正确性。例如:
import asyncio
import os
from agentscope.model import DashScopeChatModel
task = {"question": "123 加 456 等于多少?", "answer": "579"}
model = DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
)
workflow_output = asyncio.run(example_workflow_function(task, model))
assert isinstance(
workflow_output.response,
Msg,
), "在此示例中,响应应为 Msg 实例。"
print("\n工作流响应:", workflow_output.response.get_text_content())
# %%
#
# 评判函数
# --------------------
# 评判函数用于评估智能体在特定任务上的表现,并为调优过程提供奖励信号。
# 所有评判函数需遵循 ``agentscope.tuner.JudgeType`` 的输入/输出签名。
# 下面是一个简单的评判函数示例,通过比较智能体响应与标准答案给出奖励:
from typing import Any
from agentscope.tuner import JudgeOutput
async def example_judge_function(
task: Dict,
response: Any,
auxiliary_models: Optional[Dict[str, ChatModelBase]] = None,
) -> JudgeOutput:
"""仅用于演示的简单评判函数。
Args:
task (`Dict`): 任务信息。
response (`Any`): WorkflowOutput 的响应字段。
auxiliary_models (`Optional[Dict[str, ChatModelBase]]`):
用于 LLM-as-a-Judge 的辅助模型。
Returns:
`JudgeOutput`: 评判函数分配的奖励。
"""
ground_truth = task["answer"]
reward = 1.0 if ground_truth in response.get_text_content() else 0.0
return JudgeOutput(reward=reward)
# 本地测试函数的正确性:
judge_output = asyncio.run(
example_judge_function(
task,
workflow_output.response,
),
)
print(f"评判奖励: {judge_output.reward}")
# %%
# 评判函数同样可以按照上述案例中展示的方式在正式训练前进行本地测试,以确保其逻辑正确。
#
# .. tip::
# 你可以在评判函数中利用已有的 `MetricBase <https://github.com/agentscope-ai/agentscope/blob/main/src/agentscope/evaluate/_metric_base.py>`_ 实现,计算更复杂的指标,并将其组合为复合奖励。
#
# 配置并运行
# ~~~~~~~~~~~~~~~
# 最后,你可以用 ``tuner`` 模块配置并运行调优流程。
# 在开始调优前,请确保环境已安装 `Trinity-RFT <https://github.com/agentscope-ai/Trinity-RFT>`_这是 ``tuner`` 的依赖。
#
# 下面是调优流程的配置与启动示例:
#
# .. note::
# 此示例仅供演示。完整可运行示例请参考 `Tune ReActAgent <https://github.com/agentscope-ai/agentscope/tree/main/examples/tuner/react_agent>`_
#
# .. code-block:: python
#
# from agentscope.tuner import tune, AlgorithmConfig, DatasetConfig, TunerModelConfig
# # 你的工作流 / 评判函数 ...
#
# if __name__ == "__main__":
# dataset = DatasetConfig(path="my_dataset", split="train")
# model = TunerModelConfig(model_path="Qwen/Qwen3-0.6B", max_model_len=16384)
# algorithm = AlgorithmConfig(
# algorithm_type="multi_step_grpo",
# group_size=8,
# batch_size=32,
# learning_rate=1e-6,
# )
# tune(
# workflow_func=example_workflow_function,
# judge_func=example_judge_function,
# model=model,
# train_dataset=dataset,
# algorithm=algorithm,
# )
#
# 这里用 ``DatasetConfig`` 配置训练数据集,用 ``TunerModelConfig`` 配置可训练模型相关参数,用 ``AlgorithmConfig`` 指定强化学习算法及其超参数。
#
# .. tip::
# ``tune`` 函数基于 `Trinity-RFT <https://github.com/agentscope-ai/Trinity-RFT>`_ 实现,内部会将输入参数转换为 YAML 配置。
# 高级用户可忽略 ``model``、``train_dataset``、``algorithm`` 参数,直接通过 ``config_path`` 指定 YAML 配置文件。
# 推荐使用配置文件方式以便更细粒度地控制训练过程,充分利用 Trinity-RFT 的高级功能。
# 你可参考 Trinity-RFT 的 `配置指南 <https://agentscope-ai.github.io/Trinity-RFT/en/main/tutorial/trinity_configs.html>`_ 了解更多配置选项。
#
# 你可以将上述代码保存为 ``main.py``,并用如下命令运行:
#
# .. code-block:: bash
#
# ray start --head
# python main.py
#
# 检查点和日志会自动保存到当前工作目录下的 ``checkpoints/AgentScope`` 目录,每次运行会以时间戳为后缀保存到子目录。
# tensorboard 日志可在检查点目录下的 ``monitor/tensorboard`` 中找到。
#
# .. code-block:: text
#
# your_workspace/
# └── checkpoints/
# └──AgentScope/
# └── Experiment-20260104185355/ # 每次运行以时间戳保存
# ├── monitor/
# │ └── tensorboard/ # tensorboard 日志
# └── global_step_x/ # 第 x 步保存的模型检查点
#
# .. tip::
# 更多调优样例请参考 AgentScope-Samples 库中的 `tuner 目录 <https://github.com/agentscope-ai/agentscope-samples/tree/main/tuner>`_

Some files were not shown because too many files have changed in this diff Show More