This commit is contained in:
raykkk
2025-10-17 21:40:45 +08:00
commit 7d0451131f
155 changed files with 14873 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
# ReAct Agent Example
This example showcases a **ReAct** agent in AgentScope. Specifically, the ReAct agent will discuss with the user in
an alternative manner, i.e., chatbot style. It is equipped with a suite of tools to assist in answering user queries.
> 💡 Tip: Try ``Ctrl+C`` to interrupt the agent's reply to experience the realtime steering/interruption feature!
## Quick Start
Ensure you have installed agentscope and set ``DASHSCOPE_API_KEY`` in your environment variables.
Run the following commands to set up and run the example:
```bash
python main.py
```
> Note:
> - The example is built with DashScope chat model. If you want to change the model used in this example, don't
> forget to change the formatter at the same time! The corresponding relationship between built-in models and
> formatters are list in [our tutorial](https://doc.agentscope.io/tutorial/task_prompt.html#id1)
> - For local models, ensure the model service (like Ollama) is running before starting the agent.

View File

@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
"""The main entry point of the ReAct agent example."""
import asyncio
import os
from agentscope.agent import ReActAgent, UserAgent
from agentscope.formatter import DashScopeChatFormatter
from agentscope.memory import InMemoryMemory
from agentscope.model import DashScopeChatModel
from agentscope.tool import (
Toolkit,
execute_python_code,
execute_shell_command,
view_text_file,
)
async def main() -> None:
"""The main entry point for the ReAct agent example."""
toolkit = Toolkit()
toolkit.register_tool_function(execute_shell_command)
toolkit.register_tool_function(execute_python_code)
toolkit.register_tool_function(view_text_file)
agent = ReActAgent(
name="Friday",
sys_prompt="You are a helpful assistant named Friday.",
model=DashScopeChatModel(
api_key=os.environ.get("DASHSCOPE_API_KEY"),
model_name="qwen-max",
enable_thinking=False,
stream=True,
),
formatter=DashScopeChatFormatter(),
toolkit=toolkit,
memory=InMemoryMemory(),
)
user = UserAgent("User")
msg = None
while True:
msg = await user(msg)
if msg.get_text_content() == "exit":
break
msg = await agent(msg)
asyncio.run(main())

View File

@@ -0,0 +1 @@
agentscope[full]>=1.0.5

View File

@@ -0,0 +1,238 @@
# Demo of a dialog system with conversation management
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
![Python](https://img.shields.io/badge/language-Python-blue)
![Node.js](https://img.shields.io/badge/node.js-v23.9.0-green)
![React](https://img.shields.io/badge/react-v19.1.0-green)
This sample shows how to build a dialog system within the AgentScope Runtime framework.
It contains following features:
- User authentication
- Conversation management: user can start a new conversation or continue a previous one.
- Storage of conversations: on SQLite.
- agent deployment management: the agent is deployed as a service.
<img src="assets/screenshot4.jpg" alt="screenshot3" width="30%">
<img src="assets/screenshot2.jpg" alt="screenshot1" width="30%">
<img src="assets/screenshot3.jpg" alt="screenshot2" width="30%">
## 🌳 Project Structure
```bash
├── backend # Backend directory, contains server-side scripts and logic
│ ├── agent_server.py # Script implementing agent-related server functionalities
│ └── web_server.py # Script acting as the web server, handling HTTP requests
├── frontend # Frontend directory, contains client-side code and resources
│ ├── public # Public folder, used for storing static files that are directly served
│ │ ├── index.html # Entry HTML file for the frontend app
│ │ └── manifest.json # Manifest file describing the web app's metadata, such as name and icons
│ ├── src # Source code folder, contains React components and associated files
│ │ ├── App.css # Stylesheet for the main app component
│ │ ├── App.jsx # JavaScript file for the main app component, written in JSX for React
│ │ ├── App.test.js # Test file for the App component, used for unit testing
│ │ ├── index.css # Global stylesheet affecting the overall appearance of the application
│ │ ├── index.js # Entry point for the React application, renders content into `index.html`
│ │ ├── reportWebVitals.js # Script for reporting web performance metrics
│ │ └── setupTests.js # Configuration file for setting up tests, typically using a testing library
│ ├── package.json # Project dependencies file, lists all npm dependencies and scripts
│ ├── postcss.config.js # Configuration file for PostCSS, used to process CSS with plugins
│ └── tailwind.config.js # Configuration file for Tailwind CSS, customizing styles and themes
└── README.md # Project documentation file, provides basic information and usage instructions
```
## 📖 Overview
This demo demonstrates how to build a chatbot with conversation management using AgentScope Runtime. It includes features such as:
- Multi-user chat support
- Session management
- Real-time messaging
- Local deployment capabilities
The implementation separates concerns between agent logic (backend) and user interface (frontend) for better maintainability.
## ⚙️ Components
### Backend
- `agent_server.py`: Implements the chatbot agent logic and conversation management
- `web_server.py`: Provides web service endpoints for frontend communication
### Frontend
- React-based chat interface
- Tailwind CSS for styling
- Real-time message updates
- Multi-user session support
## 🌵Architecture
The architecture of the demo is depicted in the following diagram:
```mermaid
graph TD;
U[User]
subgraph frontend[Frontend]
FLI[handleLogin]
FLO[handleLogout]
FC[createNewConversation]
FL[loadConversation]
FCS[fetchConversations]
FS[sendMessage]
end
subgraph backend[Backend]
subgraph WS[web_server]
FCS<-->|/api/users/user_id/conversations:GET|WGUC[get_user_conversations]
FL <-->|/api/conversations/conversation_id:GET|WGC[get_converstaion]
FLI<-->|/api/login:POST|WLI[login]
FC<-->|/api/users/user_id/conversations:POST|WCC[create_conversation]
FS<-->|/api/conversations/conversation_id/messages:POST|WSM[send_message]
end
C((Converstaion))
WS<-->DB[SQLite]
WS <-->C
WS <-->UU((User_id))
subgraph AS[agent_service]
ALM[LLMAgent]
ALD[LocalDeployManager]
ASS[InMemorySessionHistoryService]
end
WSM <--> AS
end
U<-->|Request|frontend
```
## 🚃 Dataflow
```mermaid
flowchart LR
A[User Access Application] --> B{Is User Logged In?}
B -->|No| C[Show Login Page]
C --> D[Enter Username/Password]
D --> E[Submit Login Request]
E --> F[Backend Validates Credentials]
F -->|Valid| G[Return User Data]
G --> H[Fetch User Conversations]
H --> I[Display Chat Interface]
F -->|Invalid| J[Show Error Message]
B -->|Yes| I
I --> K{Select Conversation?}
K -->|Create New| L[Create New Conversation]
L --> M[Add Welcome Message]
M --> N[Update Conversation List]
K -->|Select Existing| O[Load Conversation]
O --> P[Fetch Messages]
P --> Q[Display Messages]
Q --> R[Type Message]
R --> S[Send Message]
S --> T[Save User Message]
T --> U[Update UI with User Message]
U --> V[Call AI Service]
V --> W[Process AI Response]
W --> X[Save AI Response]
X --> Y[Update UI with AI Response]
I --> Z[Logout]
Z --> A
style A fill:#FFE4B5
style B fill:#87CEEB
style C fill:#DDA0DD
style F fill:#98FB98
style I fill:#FFA07A
style S fill:#FFD700
style V fill:#87CEFA
```
## 🚀 Getting Started
### Prerequisites
- Python 3.11+
- Node.js
- DashScope API key: you can apply for one at https://dashscope.console.aliyun.com/.
### Install
#### Prepare the database and env
Copy the database file `ai_assistant.db`.
```bash
cd backend
cp ai_assistant_example.db ai_assistant.db
```
You can modify the database file according to your needs.
It contains two initial accounts: user1 and user2.
Copy the `.env.template` to `.env`
```bash
cp .env.template .env
```
The `DASH_API_KEY` is the API key of DashScope.
#### Install the python packages
```bash
pip install -r requirements.txt
```
#### Install the npm packages
```bash
cd ..
cd frontend
npm install
cd ..
```
### Run
#### Run the agent server
Open a terminal and run the agent server.
```bash
cd backend
python agent_server.py
```
It will listen on 8090.
#### Run the web server
Open another terminal and run the web server
```bash
python web_server.py
```
It will listen on 5100
#### Run the frontend
Open another terminal and run the frontend.
```bash
cd frontend
npm run start
```
It will listen on 3000. Open your browser and go to http://localhost:3000.
### Usage
1. Login in with initial account, e.g. user1 and password123.
2. (Optional) select a conversation or create a new one.
3. Type a message in the input box and click the "Send" button. e.g. what is your name.
## 🛠️ Features
- Local deployment capabilities
- Multi-user support
- Session management
- Real-time chat interface
- Tailwind CSS styling
## Getting Help
If you have any questions or encounter any problems with this demo, please report them through [GitHub issues]().
## 📄 License
This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details.
## 🍬 Disclaimers
This is not an officially supported product. This project is intended for demonstration purposes only and is not suitable for production use.

View File

@@ -0,0 +1,6 @@
DASHSCOPE_API_KEY=
DASHSCOPE_BASE_URL=
SERVER_PORT=8080
SERVER_ENDPOINT=agent
SERVER_HOST=localhost
USER_MANAGER_STORAGE=user.json

View File

@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
import asyncio
import os
from agentscope_runtime.engine import LocalDeployManager, Runner
from agentscope_runtime.engine.agents.llm_agent import LLMAgent
from agentscope_runtime.engine.llms import QwenLLM
from agentscope_runtime.engine.services.context_manager import ContextManager
from agentscope_runtime.engine.services.session_history_service import (
InMemorySessionHistoryService,
)
def local_deploy():
asyncio.run(_local_deploy())
async def _local_deploy():
from dotenv import load_dotenv
load_dotenv()
server_port = int(os.environ.get("SERVER_PORT", "8090"))
server_endpoint = os.environ.get("SERVER_ENDPOINT", "agent")
llm_agent = LLMAgent(
model=QwenLLM(),
name="llm_agent",
description="A simple LLM agent to generate a short ",
)
session_history_service = InMemorySessionHistoryService()
context_manager = ContextManager(
session_history_service=session_history_service,
)
runner = Runner(
agent=llm_agent,
context_manager=context_manager,
)
deploy_manager = LocalDeployManager(host="localhost", port=server_port)
try:
deployment_info = await runner.deploy(
deploy_manager,
endpoint_path=f"/{server_endpoint}",
)
print("✅ Service deployed successfully!")
print(f" URL: {deployment_info['url']}")
print(f" Endpoint: {deployment_info['url']}/{server_endpoint}")
print("\nAgent Service is running in the background.")
while True:
await asyncio.sleep(1)
except (KeyboardInterrupt, asyncio.CancelledError):
# This block will be executed when you press Ctrl+C.
print("\nShutdown signal received. Stopping the service...")
if deploy_manager.is_running:
await deploy_manager.stop()
print("✅ Service stopped.")
except Exception as e:
print(f"An error occurred: {e}")
if deploy_manager.is_running:
await deploy_manager.stop()
if __name__ == "__main__":
local_deploy()

View File

@@ -0,0 +1,5 @@
flask>=3.1.2
flask_cors>=6.0.1
agentscope-runtime>=0.1.5
agentscope-runtime[agentscope]
flask_sqlalchemy>=3.1.1

View File

@@ -0,0 +1,467 @@
# -*- coding: utf-8 -*-
import json
import logging
import os
from datetime import datetime
from typing import Tuple, Optional, Union, Dict, Any, Generator
import requests
from dotenv import load_dotenv
from flask import Flask, jsonify, request
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import check_password_hash, generate_password_hash
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
load_dotenv()
app = Flask(__name__)
CORS(app, resources={r"/*": {"origins": "*"}})
# Configure database
basedir = os.path.abspath(os.path.dirname(__file__))
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" + os.path.join(
basedir,
"ai_assistant.db",
)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db: SQLAlchemy = SQLAlchemy(app)
# Database models
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(120), nullable=False)
name = db.Column(db.String(100), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Relationships
conversations = db.relationship(
"Conversation",
backref="user",
lazy=True,
cascade="all, delete-orphan",
)
def set_password(self, password: str) -> None:
self.password_hash = generate_password_hash(password)
def check_password(self, password: str) -> bool:
return check_password_hash(self.password_hash, password)
class Conversation(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(
db.DateTime,
default=datetime.utcnow,
onupdate=datetime.utcnow,
)
# Relationships
messages = db.relationship(
"Message",
backref="conversation",
lazy=True,
cascade="all, delete-orphan",
)
class Message(db.Model):
id = db.Column(db.Integer, primary_key=True)
text = db.Column(db.Text, nullable=False)
sender = db.Column(db.String(20), nullable=False) # 'user' or 'ai'
conversation_id = db.Column(
db.Integer,
db.ForeignKey("conversation.id"),
nullable=False,
)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Create database tables
def create_tables() -> None:
db.create_all()
# Create sample users (if none exist)
if not User.query.first():
user1 = User(username="user1", name="Bruce")
user1.set_password("password123")
user2 = User(username="user2", name="John")
user2.set_password("password456")
db.session.add(user1)
db.session.add(user2)
db.session.commit()
# functions
def parse_sse_line(
line: bytes,
) -> Tuple[Optional[str], Optional[Union[str, int]]]:
line = line.decode("utf-8").strip()
if line.startswith("data: "):
return "data", line[6:]
elif line.startswith("event:"):
return "event", line[7:]
elif line.startswith("id: "):
return "id", line[4:]
elif line.startswith("retry:"):
return "retry", int(line[7:])
return None, None
def sse_client(
url: str,
data: Optional[Dict[str, Any]] = None,
) -> Generator[str, None, None]:
headers = {
"Accept": "text/event-stream",
"Cache-Control": "no-cache",
}
if data is not None:
response = requests.post(
url,
stream=True,
headers=headers,
json=data,
)
else:
response = requests.get(
url,
stream=True,
headers=headers,
)
for line in response.iter_lines():
if line:
field, value = parse_sse_line(line)
if field == "data":
try:
data = json.loads(value)
if (
data["object"] == "content"
and data["delta"] is True
and data["type"] == "text"
):
yield data["text"]
except json.JSONDecodeError:
pass
def call_runner(
query: str,
query_user_id: str,
query_session_id: str,
) -> Generator[str, None, None]:
server_port = int(os.environ.get("SERVER_PORT", "8090"))
server_endpoint = os.environ.get("SERVER_ENDPOINT", "agent")
server_host = os.environ.get("SERVER_HOST", "localhost")
url = f"http://{server_host}:{server_port}/{server_endpoint}"
data_arg: Dict[str, Any] = {
"input": [
{
"role": "user",
"content": [
{
"type": "text",
"text": query,
},
],
},
],
"session_id": query_session_id,
"user_id": query_user_id,
}
for content in sse_client(url, data=data_arg):
yield content
# API routes
# User login
@app.route("/api/login", methods=["POST"])
def login():
data = request.get_json()
username = data.get("username")
password = data.get("password")
if not username or not password:
return jsonify({"error": "Username and password cannot be empty"}), 400
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
return (
jsonify(
{
"id": user.id,
"username": user.username,
"name": user.name,
"created_at": user.created_at.isoformat(),
},
),
200,
)
else:
return jsonify({"error": "Invalid username or password"}), 401
# Get all user conversations
@app.route("/api/users/<int:user_id>/conversations", methods=["GET"])
def get_user_conversations(user_id):
User.query.get_or_404(user_id)
conversations = (
Conversation.query.filter_by(user_id=user_id)
.order_by(
Conversation.updated_at.desc(),
)
.all()
)
result = []
for conv in conversations:
# Get the last message as preview
last_message = (
Message.query.filter_by(
conversation_id=conv.id,
)
.order_by(
Message.created_at.desc(),
)
.first()
)
preview = last_message.text if last_message else ""
result.append(
{
"id": conv.id,
"title": conv.title,
"user_id": conv.user_id,
"preview": preview,
"created_at": conv.created_at.isoformat(),
"updated_at": conv.updated_at.isoformat(),
},
)
return jsonify(result), 200
# Create new conversation
@app.route("/api/users/<int:user_id>/conversations", methods=["POST"])
def create_conversation(user_id):
User.query.get_or_404(user_id)
data = request.get_json()
title = data.get(
"title",
f'Conversation {datetime.now().strftime("%Y-%m-%d %H:%M")}',
)
conversation = Conversation(title=title, user_id=user_id)
db.session.add(conversation)
db.session.commit()
# Create welcome message
welcome_message = Message(
text="Hello! I am your AI assistant. How can I help you today?",
sender="ai",
conversation_id=conversation.id,
)
db.session.add(welcome_message)
db.session.commit()
return (
jsonify(
{
"id": conversation.id,
"title": conversation.title,
"user_id": conversation.user_id,
"created_at": conversation.created_at.isoformat(),
"updated_at": conversation.updated_at.isoformat(),
},
),
201,
)
# Get conversation details and messages
@app.route("/api/conversations/<int:conversation_id>", methods=["GET"])
def get_conversation(conversation_id):
conversation = Conversation.query.get_or_404(conversation_id)
messages = (
Message.query.filter_by(
conversation_id=conversation_id,
)
.order_by(
Message.created_at.asc(),
)
.all()
)
messages_data = []
for msg in messages:
messages_data.append(
{
"id": msg.id,
"text": msg.text,
"sender": msg.sender,
"created_at": msg.created_at.isoformat(),
},
)
return (
jsonify(
{
"id": conversation.id,
"title": conversation.title,
"user_id": conversation.user_id,
"messages": messages_data,
"created_at": conversation.created_at.isoformat(),
"updated_at": conversation.updated_at.isoformat(),
},
),
200,
)
# Send message
@app.route(
"/api/conversations/<int:conversation_id>/messages",
methods=["POST"],
)
def send_message(conversation_id):
conversation = Conversation.query.get_or_404(conversation_id)
data = request.get_json()
text = data.get("text")
sender = data.get("sender", "user")
if not text:
return jsonify({"error": "Message content cannot be empty"}), 400
# Create user message
user_message = Message(
text=text,
sender=sender,
conversation_id=conversation_id,
)
db.session.add(user_message)
# Update conversation title (if it's the first user message)
if sender == "user" and len(conversation.messages) <= 1:
conversation.title = text[:20] + ("..." if len(text) > 20 else "")
db.session.commit()
if sender == "user":
ai_response_text = ""
question = text
conversation_id_str = str(conversation_id)
for item in call_runner(
question,
conversation_id_str,
conversation_id_str,
):
ai_response_text += item
ai_message = Message(
text=ai_response_text,
sender="ai",
conversation_id=conversation_id,
)
db.session.add(ai_message)
db.session.commit()
return (
jsonify(
{
"id": user_message.id,
"text": user_message.text,
"sender": user_message.sender,
"created_at": user_message.created_at.isoformat(),
},
),
201,
)
# Delete conversation
@app.route("/api/conversations/<int:conversation_id>", methods=["DELETE"])
def delete_conversation(conversation_id):
conversation = Conversation.query.get_or_404(conversation_id)
db.session.delete(conversation)
db.session.commit()
return jsonify({"message": "Conversation deleted successfully"}), 200
# Update conversation title
@app.route("/api/conversations/<int:conversation_id>", methods=["PUT"])
def update_conversation(conversation_id):
conversation = Conversation.query.get_or_404(conversation_id)
data = request.get_json()
if "title" in data:
conversation.title = data["title"]
db.session.commit()
return (
jsonify(
{
"id": conversation.id,
"title": conversation.title,
"user_id": conversation.user_id,
"created_at": conversation.created_at.isoformat(),
"updated_at": conversation.updated_at.isoformat(),
},
),
200,
)
# Get user information
@app.route("/api/users/<int:user_id>", methods=["GET"])
def get_user(user_id):
user = User.query.get_or_404(user_id)
return (
jsonify(
{
"id": user.id,
"username": user.username,
"name": user.name,
"created_at": user.created_at.isoformat(),
},
),
200,
)
# Error handling
@app.errorhandler(404)
def not_found(error):
logger.error(error)
return jsonify({"error": "Resource not found"}), 404
@app.errorhandler(500)
def internal_error(error):
logger.error(error)
db.session.rollback()
return jsonify({"error": "Internal server error"}), 500
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=5100)

View File

@@ -0,0 +1,39 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@tailwindcss/postcss": "^4.1.11",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"antd": "^5.26.6",
"autoprefixer": "^10.4.21",
"lucide-react": "^0.525.0",
"postcss": "^8.5.6",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-scripts": "5.0.1",
"tailwindcss": "^3.4.17",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Chatbot demo"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Chatbot Demo</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "Chatbot",
"name": "Chatbot Demo",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,60 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f8fafc;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
/* Ant Design overrides for清新风格 */
.ant-tabs-tab {
font-weight: 500;
}
.ant-list-item {
background: white;
border-radius: 8px;
margin-bottom: 8px;
padding: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
}
.ant-list-item:hover {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07);
transform: translateY(-1px);
}
.ant-card {
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.ant-btn-primary {
background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);
border: none;
}
.ant-btn-primary:hover {
background: linear-gradient(135deg, #0284c7 0%, #0369a1 100%);
}
.ant-progress-inner {
border-radius: 4px;
}
.ant-tag {
border-radius: 12px;
padding: 0 12px;
}

View File

@@ -0,0 +1,391 @@
import React, { useState, useEffect, useRef } from 'react';
import { MessageCircle, User, Send, Plus, LogOut, Menu, X, Bot } from 'lucide-react';
const App = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [currentUser, setCurrentUser] = useState(null);
const [conversations, setConversations] = useState([]);
const [activeConversation, setActiveConversation] = useState(null);
const [message, setMessage] = useState('');
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [loading, setLoading] = useState(false);
const messagesEndRef = useRef(null);
// API base URL
const API_BASE = 'http://localhost:5100/api';
// Auto scroll to bottom of messages
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [activeConversation?.messages]);
// Fetch user conversations
const fetchConversations = async (userId) => {
try {
const response = await fetch(`${API_BASE}/users/${userId}/conversations`);
if (response.ok) {
const data = await response.json();
setConversations(data);
if (data.length > 0 && !activeConversation) {
// Load the first conversation
loadConversation(data[0].id);
}
}
} catch (error) {
console.error('Error fetching conversations:', error);
}
};
// Load conversation details
const loadConversation = async (conversationId) => {
try {
const response = await fetch(`${API_BASE}/conversations/${conversationId}`);
if (response.ok) {
const data = await response.json();
setActiveConversation(data);
}
} catch (error) {
console.error('Error loading conversation:', error);
}
};
// Login function
const handleLogin = async (e) => {
e.preventDefault();
setLoading(true);
try {
const response = await fetch(`${API_BASE}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
});
if (response.ok) {
const userData = await response.json();
setCurrentUser(userData);
setIsLoggedIn(true);
await fetchConversations(userData.id);
} else {
const errorData = await response.json();
alert(errorData.error || 'Login failed');
}
} catch (error) {
console.error('Login error:', error);
alert('Network error. Please try again.');
} finally {
setLoading(false);
}
};
// Logout function
const handleLogout = () => {
setIsLoggedIn(false);
setCurrentUser(null);
setUsername('');
setPassword('');
setConversations([]);
setActiveConversation(null);
setIsMenuOpen(false);
};
// Create new conversation
const createNewConversation = async () => {
if (!currentUser) return;
setLoading(true);
try {
const response = await fetch(`${API_BASE}/users/${currentUser.id}/conversations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ title: 'New Conversation' }),
});
if (response.ok) {
const newConversation = await response.json();
setConversations(prev => [newConversation, ...prev]);
await loadConversation(newConversation.id);
}
} catch (error) {
console.error('Error creating conversation:', error);
} finally {
setLoading(false);
setIsMenuOpen(false);
}
};
// Send message
const sendMessage = async () => {
if (!message.trim() || !activeConversation) return;
setLoading(true);
try {
// Send user message
const userMessageResponse = await fetch(`${API_BASE}/conversations/${activeConversation.id}/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: message, sender: 'user' }),
});
if (userMessageResponse.ok) {
const userMessage = await userMessageResponse.json();
// Update UI with user message
const updatedConversation = {
...activeConversation,
messages: [...activeConversation.messages, userMessage],
title: activeConversation.messages.length === 1 ? message.slice(0, 20) + (message.length > 20 ? '...' : '') : activeConversation.title
};
setActiveConversation(updatedConversation);
setMessage('');
// Fetch updated conversation to get AI response
await loadConversation(activeConversation.id);
}
} catch (error) {
console.error('Error sending message:', error);
} finally {
setLoading(false);
}
};
// Format timestamp
const formatTime = (timestamp) => {
return new Date(timestamp).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
};
// Login Page
if (!isLoggedIn) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-xl w-full max-w-md p-8">
<div className="text-center mb-8">
<div className="mx-auto bg-indigo-100 rounded-full p-4 w-16 h-16 flex items-center justify-center mb-4">
<Bot className="w-8 h-8 text-indigo-600" />
</div>
<h1 className="text-3xl font-bold text-gray-800 mb-2">AI Assistant</h1>
<p className="text-gray-600">Intelligent conversations, always at your service</p>
</div>
<form onSubmit={handleLogin} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Username</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
placeholder="Enter username"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
placeholder="Enter password"
required
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-indigo-600 text-white py-3 rounded-lg font-medium hover:bg-indigo-700 transition-colors focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50"
>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
<p className="text-sm text-gray-600 mb-2">Demo accounts:</p>
<p className="text-xs text-gray-500">Username: user1, Password: password123</p>
<p className="text-xs text-gray-500">Username: user2, Password: password456</p>
</div>
</div>
</div>
);
}
// Main App
return (
<div className="h-screen bg-gray-50 flex flex-col">
{/* Header */}
<header className="bg-white shadow-sm border-b border-gray-200 px-4 py-3 flex items-center justify-between">
<div className="flex items-center space-x-3">
<button
onClick={() => setIsMenuOpen(true)}
className="p-2 rounded-lg hover:bg-gray-100 transition-colors"
>
<Menu className="w-5 h-5 text-gray-600" />
</button>
<div className="flex items-center space-x-2">
<div className="bg-indigo-100 rounded-full p-2">
<Bot className="w-5 h-5 text-indigo-600" />
</div>
<h1 className="text-lg font-semibold text-gray-800">AI Assistant</h1>
</div>
</div>
<button
onClick={handleLogout}
className="p-2 rounded-lg hover:bg-gray-100 transition-colors"
>
<LogOut className="w-5 h-5 text-gray-600" />
</button>
</header>
<div className="flex flex-1 overflow-hidden">
{/* Sidebar */}
{isMenuOpen && (
<div className="fixed inset-0 z-50 lg:relative lg:inset-auto lg:z-auto">
<div className="absolute inset-0 bg-black bg-opacity-50 lg:hidden" onClick={() => setIsMenuOpen(false)} />
<div className="absolute left-0 top-0 h-full w-80 bg-white shadow-xl lg:relative lg:shadow-none z-10">
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-800">Conversations</h2>
<button
onClick={() => setIsMenuOpen(false)}
className="lg:hidden p-2 rounded-lg hover:bg-gray-100"
>
<X className="w-5 h-5 text-gray-600" />
</button>
</div>
<div className="p-4">
<button
onClick={createNewConversation}
disabled={loading}
className="w-full bg-indigo-600 text-white py-3 rounded-lg font-medium hover:bg-indigo-700 transition-colors flex items-center justify-center space-x-2 mb-4 disabled:opacity-50"
>
<Plus className="w-4 h-4" />
<span>New Conversation</span>
</button>
</div>
<div className="flex-1 overflow-y-auto">
{conversations.map((conversation) => (
<div
key={conversation.id}
onClick={() => {
loadConversation(conversation.id);
setIsMenuOpen(false);
}}
className={`p-4 border-b border-gray-100 cursor-pointer hover:bg-gray-50 transition-colors ${
activeConversation?.id === conversation.id ? 'bg-indigo-50 border-l-4 border-l-indigo-500' : ''
}`}
>
<div className="flex items-start space-x-3">
<MessageCircle className="w-5 h-5 text-gray-400 mt-0.5" />
<div className="flex-1 min-w-0">
<h3 className="font-medium text-gray-900 truncate">{conversation.title}</h3>
<p className="text-sm text-gray-500 truncate">
{conversation.preview || 'New conversation'}
</p>
</div>
</div>
</div>
))}
</div>
<div className="p-4 border-t border-gray-200">
<div className="flex items-center space-x-3">
<div className="bg-gray-200 rounded-full p-2">
<User className="w-4 h-4 text-gray-600" />
</div>
<div>
<p className="font-medium text-gray-900">{currentUser?.name}</p>
<p className="text-sm text-gray-500">@{currentUser?.username}</p>
</div>
</div>
</div>
</div>
</div>
)}
{/* Main Chat Area */}
<div className="flex-1 flex flex-col">
{activeConversation ? (
<>
{/* Chat Header */}
<div className="bg-white border-b border-gray-200 px-4 py-3">
<h2 className="font-semibold text-gray-800">{activeConversation.title}</h2>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{activeConversation.messages.map((msg) => (
<div
key={msg.id}
className={`flex ${msg.sender === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-xs lg:max-w-md px-4 py-3 rounded-2xl ${
msg.sender === 'user'
? 'bg-indigo-600 text-white rounded-br-md'
: 'bg-white text-gray-800 border border-gray-200 rounded-bl-md shadow-sm'
}`}
>
<p className="text-sm">{msg.text}</p>
<p className={`text-xs mt-1 ${msg.sender === 'user' ? 'text-indigo-100' : 'text-gray-500'}`}>
{formatTime(msg.created_at)}
</p>
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
{/* Input Area */}
<div className="bg-white border-t border-gray-200 p-4">
<div className="flex items-center space-x-3">
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && !loading && sendMessage()}
disabled={loading}
className="flex-1 px-4 py-3 border border-gray-300 rounded-full focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all disabled:opacity-50"
placeholder="Type a message..."
/>
<button
onClick={sendMessage}
disabled={!message.trim() || loading}
className="bg-indigo-600 text-white p-3 rounded-full hover:bg-indigo-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
{loading ? (
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : (
<Send className="w-5 h-5" />
)}
</button>
</div>
</div>
</>
) : (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<MessageCircle className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-medium text-gray-600 mb-2">Select or Create a Conversation</h3>
<p className="text-gray-500">Choose an existing conversation or create a new one to get started</p>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default App;

View File

@@ -0,0 +1,8 @@
import { render, screen } from "@testing-library/react";
import App from "./App";
test("renders learn react link", () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -0,0 +1,27 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}

View File

@@ -0,0 +1,12 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom";

View File

@@ -0,0 +1,26 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
".public/index.html"
],
theme: {
extend: {
colors: {
primary: {
50: "#f0f9ff",
100: "#e0f2fe",
200: "#bae6fd",
300: "#7dd3fc",
400: "#38bdf8",
500: "#0ea5e9",
600: "#0284c7",
700: "#0369a1",
800: "#075985",
900: "#0c4a6e",
}
}
},
},
plugins: [],
};

View File

@@ -0,0 +1,24 @@
# MultiAgent Conversation
This example demonstrates how to build a multi-agent conversation workflow using ``MsgHub`` in AgentScope,
where multiple agents broadcast messages to each other in a shared conversation space.
## Setup
The example is built upon the DashScope LLM API in [main.py](https://github.com/agentscope-ai/agentscope/blob/main/examples/workflows/multiagent_conversation/main.py). You can switch to other LLMs by modifying the ``model`` and ``formatter`` parameters in the code.
To run the example, first install the latest version of AgentScope, then run:
```bash
python examples/workflows/multiagent_conversation/main.py
```
## Main Workflow
- Create multiple participant agents with different attributes (e.g., Alice, Bob, Charlie).
- Agents introduce themselves and interact in the message hub.
- Supports dynamic addition and removal of agents, as well as broadcasting messages.
> Note: The example is built with DashScope chat model. If you want to change the model in this example, don't forget
> to change the formatter at the same time! The corresponding relationship between built-in models and formatters are
> list in [our tutorial](https://doc.agentscope.io/tutorial/task_prompt.html#id1)

View File

@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
"""The example of how to construct multi-agent conversation with MsgHub and
pipeline in AgentScope."""
import asyncio
import os
from agentscope.agent import ReActAgent
from agentscope.formatter import DashScopeMultiAgentFormatter
from agentscope.message import Msg
from agentscope.model import DashScopeChatModel
from agentscope.pipeline import MsgHub, sequential_pipeline
def create_participant_agent(
name: str,
age: int,
career: str,
character: str,
) -> ReActAgent:
"""Create a participant agent with a specific name, age, and character."""
return ReActAgent(
name=name,
sys_prompt=(
f"You're a {age}-year-old {career} named {name} and you're "
f"a {character} person."
),
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
stream=True,
),
# Use multiagent formatter because the multiple entities will
# occur in the prompt of the LLM API call
formatter=DashScopeMultiAgentFormatter(),
)
async def main() -> None:
"""Run a multi-agent conversation workflow."""
# Create multiple participant agents with different characteristics
alice = create_participant_agent("Alice", 30, "teacher", "friendly")
bob = create_participant_agent("Bob", 14, "student", "rebellious")
charlie = create_participant_agent("Charlie", 28, "doctor", "thoughtful")
# Create a conversation where participants introduce themselves within
# a message hub
async with MsgHub(
participants=[alice, bob, charlie],
# The greeting message will be sent to all participants at the start
announcement=Msg(
"system",
"Now you meet each other with a brief self-introduction.",
"system",
),
) as hub:
# Quick construct a pipeline to run the conversation
await sequential_pipeline([alice, bob, charlie])
# Or by the following way:
# await alice()
# await bob()
# await charlie()
# Delete a participant agent from the hub and fake a broadcast message
print("##### We fake Bob's departure #####")
hub.delete(bob)
await hub.broadcast(
Msg(
"bob",
"I have to start my homework now, see you later!",
"assistant",
),
)
await alice()
await charlie()
# ...
asyncio.run(main())

View File

@@ -0,0 +1 @@
agentscope[full]>=1.0.5

View File

@@ -0,0 +1,24 @@
# MultiAgent 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](https://aclanthology.org/2024.emnlp-main.992/), 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.
## Setup
The example is built upon DashScope LLM API in [main.py](https://github.com/agentscope-ai/agentscope/blob/main/examples/workflows/multiagent_debate/main.py).
You can also change to the other LLMs by modifying the ``model`` and ``formatter`` parameters in the code.
To run the example, first install the latest version of AgentScope, then run:
```bash
python examples/workflows/multiagent_debate/main.py
```
> Note: The example is built with DashScope chat model. If you want to change the model in this example, don't forget
> to change the formatter at the same time! The corresponding relationship between built-in models and formatters are
> list in [our tutorial](https://doc.agentscope.io/tutorial/task_prompt.html#id1)

View File

@@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
"""The multi-agent debate workflow example in AgentScope."""
import asyncio
import os
from agentscope.agent import ReActAgent
from agentscope.formatter import (
DashScopeChatFormatter,
DashScopeMultiAgentFormatter,
)
from agentscope.message import Msg
from agentscope.model import DashScopeChatModel
from agentscope.pipeline import MsgHub
from pydantic import BaseModel, Field
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 not necessary 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}. Use Chinese to answer the question",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.environ["DASHSCOPE_API_KEY"],
stream=True,
),
formatter=DashScopeChatFormatter(),
)
alice, bob = [create_solver_agent(name) for name in ["Alice", "Bob"]]
# Create a moderator agent
moderator = ReActAgent(
name="Aggregator",
sys_prompt=(
"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:\n"
"```\n"
"{topic}\n"
"```\n"
"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=True,
),
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,
)
print("【STRUCTURED_OUTPUT】: ", msg_judge.metadata)
if msg_judge.metadata.get("finished"):
print(
"The debate is finished, and the correct answer is: ",
msg_judge.metadata.get("correct_answer"),
)
break
asyncio.run(run_multiagent_debate())

View File

@@ -0,0 +1 @@
agentscope[full]>=1.0.5