This commit is contained in:
CakeCN
2024-12-03 16:21:19 +08:00
commit 4198ca63b1
975 changed files with 333413 additions and 0 deletions

View File

@@ -0,0 +1,523 @@
Metadata-Version: 2.1
Name: agentscope
Version: 0.0.6a3
Summary: AgentScope: A Flexible yet Robust Multi-Agent Platform.
Home-page: https://github.com/modelscope/agentscope
Download-URL: https://github.com/modelscope/agentscope/archive/v0.0.6alpha3.tar.gz
Author: SysML team of Alibaba Tongyi Lab
Author-email: gaodawei.gdw@alibaba-inc.com
License: Apache License 2.0
Keywords: deep-learning,multi agents,agents
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: networkx
Requires-Dist: black
Requires-Dist: docstring_parser
Requires-Dist: pydantic
Requires-Dist: loguru==0.6.0
Requires-Dist: tiktoken
Requires-Dist: Pillow
Requires-Dist: requests
Requires-Dist: chardet
Requires-Dist: inputimeout
Requires-Dist: openai>=1.3.0
Requires-Dist: numpy
Requires-Dist: Flask==3.0.0
Requires-Dist: Flask-Cors==4.0.0
Requires-Dist: Flask-SocketIO==5.3.6
Requires-Dist: flask_sqlalchemy
Requires-Dist: flake8
Requires-Dist: dashscope==1.14.1
Requires-Dist: openai>=1.3.0
Requires-Dist: ollama>=0.1.7
Requires-Dist: google-generativeai>=0.4.0
Requires-Dist: zhipuai
Requires-Dist: litellm
Requires-Dist: psutil
Requires-Dist: scipy
Provides-Extra: distribute
Requires-Dist: networkx; extra == "distribute"
Requires-Dist: black; extra == "distribute"
Requires-Dist: docstring_parser; extra == "distribute"
Requires-Dist: pydantic; extra == "distribute"
Requires-Dist: loguru==0.6.0; extra == "distribute"
Requires-Dist: tiktoken; extra == "distribute"
Requires-Dist: Pillow; extra == "distribute"
Requires-Dist: requests; extra == "distribute"
Requires-Dist: chardet; extra == "distribute"
Requires-Dist: inputimeout; extra == "distribute"
Requires-Dist: openai>=1.3.0; extra == "distribute"
Requires-Dist: numpy; extra == "distribute"
Requires-Dist: Flask==3.0.0; extra == "distribute"
Requires-Dist: Flask-Cors==4.0.0; extra == "distribute"
Requires-Dist: Flask-SocketIO==5.3.6; extra == "distribute"
Requires-Dist: flask_sqlalchemy; extra == "distribute"
Requires-Dist: flake8; extra == "distribute"
Requires-Dist: dashscope==1.14.1; extra == "distribute"
Requires-Dist: openai>=1.3.0; extra == "distribute"
Requires-Dist: ollama>=0.1.7; extra == "distribute"
Requires-Dist: google-generativeai>=0.4.0; extra == "distribute"
Requires-Dist: zhipuai; extra == "distribute"
Requires-Dist: litellm; extra == "distribute"
Requires-Dist: psutil; extra == "distribute"
Requires-Dist: scipy; extra == "distribute"
Requires-Dist: grpcio==1.60.0; extra == "distribute"
Requires-Dist: grpcio-tools==1.60.0; extra == "distribute"
Requires-Dist: protobuf==4.25.0; extra == "distribute"
Requires-Dist: expiringdict; extra == "distribute"
Requires-Dist: dill; extra == "distribute"
Provides-Extra: dev
Requires-Dist: networkx; extra == "dev"
Requires-Dist: black; extra == "dev"
Requires-Dist: docstring_parser; extra == "dev"
Requires-Dist: pydantic; extra == "dev"
Requires-Dist: loguru==0.6.0; extra == "dev"
Requires-Dist: tiktoken; extra == "dev"
Requires-Dist: Pillow; extra == "dev"
Requires-Dist: requests; extra == "dev"
Requires-Dist: chardet; extra == "dev"
Requires-Dist: inputimeout; extra == "dev"
Requires-Dist: openai>=1.3.0; extra == "dev"
Requires-Dist: numpy; extra == "dev"
Requires-Dist: Flask==3.0.0; extra == "dev"
Requires-Dist: Flask-Cors==4.0.0; extra == "dev"
Requires-Dist: Flask-SocketIO==5.3.6; extra == "dev"
Requires-Dist: flask_sqlalchemy; extra == "dev"
Requires-Dist: flake8; extra == "dev"
Requires-Dist: dashscope==1.14.1; extra == "dev"
Requires-Dist: openai>=1.3.0; extra == "dev"
Requires-Dist: ollama>=0.1.7; extra == "dev"
Requires-Dist: google-generativeai>=0.4.0; extra == "dev"
Requires-Dist: zhipuai; extra == "dev"
Requires-Dist: litellm; extra == "dev"
Requires-Dist: psutil; extra == "dev"
Requires-Dist: scipy; extra == "dev"
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: pre-commit; extra == "dev"
Provides-Extra: full
Requires-Dist: networkx; extra == "full"
Requires-Dist: black; extra == "full"
Requires-Dist: docstring_parser; extra == "full"
Requires-Dist: pydantic; extra == "full"
Requires-Dist: loguru==0.6.0; extra == "full"
Requires-Dist: tiktoken; extra == "full"
Requires-Dist: Pillow; extra == "full"
Requires-Dist: requests; extra == "full"
Requires-Dist: chardet; extra == "full"
Requires-Dist: inputimeout; extra == "full"
Requires-Dist: openai>=1.3.0; extra == "full"
Requires-Dist: numpy; extra == "full"
Requires-Dist: Flask==3.0.0; extra == "full"
Requires-Dist: Flask-Cors==4.0.0; extra == "full"
Requires-Dist: Flask-SocketIO==5.3.6; extra == "full"
Requires-Dist: flask_sqlalchemy; extra == "full"
Requires-Dist: flake8; extra == "full"
Requires-Dist: dashscope==1.14.1; extra == "full"
Requires-Dist: openai>=1.3.0; extra == "full"
Requires-Dist: ollama>=0.1.7; extra == "full"
Requires-Dist: google-generativeai>=0.4.0; extra == "full"
Requires-Dist: zhipuai; extra == "full"
Requires-Dist: litellm; extra == "full"
Requires-Dist: psutil; extra == "full"
Requires-Dist: scipy; extra == "full"
Requires-Dist: grpcio==1.60.0; extra == "full"
Requires-Dist: grpcio-tools==1.60.0; extra == "full"
Requires-Dist: protobuf==4.25.0; extra == "full"
Requires-Dist: expiringdict; extra == "full"
Requires-Dist: dill; extra == "full"
Requires-Dist: docker; extra == "full"
Requires-Dist: pymongo; extra == "full"
Requires-Dist: pymysql; extra == "full"
Requires-Dist: bs4; extra == "full"
Requires-Dist: beautifulsoup4; extra == "full"
Requires-Dist: feedparser; extra == "full"
Requires-Dist: sphinx; extra == "full"
Requires-Dist: sphinx-autobuild; extra == "full"
Requires-Dist: sphinx_rtd_theme; extra == "full"
Requires-Dist: myst-parser; extra == "full"
Requires-Dist: sphinxcontrib-mermaid; extra == "full"
Requires-Dist: pytest; extra == "full"
Requires-Dist: pytest-cov; extra == "full"
Requires-Dist: pre-commit; extra == "full"
Requires-Dist: gradio==4.19.1; extra == "full"
Requires-Dist: modelscope_studio==0.0.5; extra == "full"
Requires-Dist: llama-index==0.10.30; extra == "full"
English | [**中文**](https://github.com/modelscope/agentscope/blob/main/README_ZH.md)
# AgentScope
<h1 align="left">
<img src="https://img.alicdn.com/imgextra/i2/O1CN01cdjhVE1wwt5Auv7bY_!!6000000006373-0-tps-1792-1024.jpg" width="600" alt="agentscope-logo">
</h1>
Start building LLM-empowered multi-agent applications in an easier way.
[![](https://img.shields.io/badge/cs.MA-2402.14034-B31C1C?logo=arxiv&logoColor=B31C1C)](https://arxiv.org/abs/2402.14034)
[![](https://img.shields.io/badge/python-3.9+-blue)](https://pypi.org/project/agentscope/)
[![](https://img.shields.io/badge/pypi-v0.0.6a2-blue?logo=pypi)](https://pypi.org/project/agentscope/)
[![](https://img.shields.io/badge/Docs-English%7C%E4%B8%AD%E6%96%87-blue?logo=markdown)](https://modelscope.github.io/agentscope/#welcome-to-agentscope-tutorial-hub)
[![](https://img.shields.io/badge/Docs-API_Reference-blue?logo=markdown)](https://modelscope.github.io/agentscope/)
[![](https://img.shields.io/badge/ModelScope-Demos-4e29ff.svg?logo=data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjI0IDEyMS4zMyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KCTxwYXRoIGQ9Im0wIDQ3Ljg0aDI1LjY1djI1LjY1aC0yNS42NXoiIGZpbGw9IiM2MjRhZmYiIC8+Cgk8cGF0aCBkPSJtOTkuMTQgNzMuNDloMjUuNjV2MjUuNjVoLTI1LjY1eiIgZmlsbD0iIzYyNGFmZiIgLz4KCTxwYXRoIGQ9Im0xNzYuMDkgOTkuMTRoLTI1LjY1djIyLjE5aDQ3Ljg0di00Ny44NGgtMjIuMTl6IiBmaWxsPSIjNjI0YWZmIiAvPgoJPHBhdGggZD0ibTEyNC43OSA0Ny44NGgyNS42NXYyNS42NWgtMjUuNjV6IiBmaWxsPSIjMzZjZmQxIiAvPgoJPHBhdGggZD0ibTAgMjIuMTloMjUuNjV2MjUuNjVoLTI1LjY1eiIgZmlsbD0iIzM2Y2ZkMSIgLz4KCTxwYXRoIGQ9Im0xOTguMjggNDcuODRoMjUuNjV2MjUuNjVoLTI1LjY1eiIgZmlsbD0iIzYyNGFmZiIgLz4KCTxwYXRoIGQ9Im0xOTguMjggMjIuMTloMjUuNjV2MjUuNjVoLTI1LjY1eiIgZmlsbD0iIzM2Y2ZkMSIgLz4KCTxwYXRoIGQ9Im0xNTAuNDQgMHYyMi4xOWgyNS42NXYyNS42NWgyMi4xOXYtNDcuODR6IiBmaWxsPSIjNjI0YWZmIiAvPgoJPHBhdGggZD0ibTczLjQ5IDQ3Ljg0aDI1LjY1djI1LjY1aC0yNS42NXoiIGZpbGw9IiMzNmNmZDEiIC8+Cgk8cGF0aCBkPSJtNDcuODQgMjIuMTloMjUuNjV2LTIyLjE5aC00Ny44NHY0Ny44NGgyMi4xOXoiIGZpbGw9IiM2MjRhZmYiIC8+Cgk8cGF0aCBkPSJtNDcuODQgNzMuNDloLTIyLjE5djQ3Ljg0aDQ3Ljg0di0yMi4xOWgtMjUuNjV6IiBmaWxsPSIjNjI0YWZmIiAvPgo8L3N2Zz4K)](https://modelscope.cn/studios?name=agentscope&page=1&sort=latest)
[![](https://img.shields.io/badge/license-Apache--2.0-black)](./LICENSE)
[![](https://img.shields.io/badge/Contribute-Welcome-green)](https://modelscope.github.io/agentscope/tutorial/contribute.html)
If you find our work helpful, please kindly
cite [our paper](https://arxiv.org/abs/2402.14034).
Welcome to join our community on
| [Discord](https://discord.gg/eYMpfnkG8h) | DingTalk |
|----------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
| <img src="https://gw.alicdn.com/imgextra/i1/O1CN01hhD1mu1Dd3BWVUvxN_!!6000000000238-2-tps-400-400.png" width="100" height="100"> | <img src="https://img.alicdn.com/imgextra/i2/O1CN01tuJ5971OmAqNg9cOw_!!6000000001747-0-tps-444-460.jpg" width="100" height="100"> |
----
## News
- <img src="https://img.alicdn.com/imgextra/i3/O1CN01SFL0Gu26nrQBFKXFR_!!6000000007707-2-tps-500-500.png" alt="new" width="30" height="30"/>**[2024-06-14]** A new prompt tuning module is available in AgentScope to help developers generate and optimize the agents' system prompts! Refer to our [tutorial](https://modelscope.github.io/agentscope/en/tutorial/209-prompt_opt.html) for more details!
- <img src="https://img.alicdn.com/imgextra/i3/O1CN01SFL0Gu26nrQBFKXFR_!!6000000007707-2-tps-500-500.png" alt="new" width="30" height="30"/>**[2024-06-11]** The RAG functionality is available for agents in **AgentScope** now! [**A quick introduction to RAG in AgentScope**](https://modelscope.github.io/agentscope/en/tutorial/210-rag.html) can help you equip your agent with external knowledge!
- <img src="https://img.alicdn.com/imgextra/i3/O1CN01SFL0Gu26nrQBFKXFR_!!6000000007707-2-tps-500-500.png" alt="new" width="30" height="30"/>**[2024-06-09]** We release **AgentScope** v0.0.5 now! In this new version, [**AgentScope Workstation**](https://modelscope.github.io/agentscope/en/tutorial/209-gui.html) (the online version is running on [agentscope.io](https://agentscope.io)) is open-sourced with the refactored [**AgentScope Studio**](https://modelscope.github.io/agentscope/en/tutorial/209-gui.html)!
<h5 align="center">
<img src="https://img.alicdn.com/imgextra/i1/O1CN01RXAVVn1zUtjXVvuqS_!!6000000006718-1-tps-3116-1852.gif" width="600" alt="agentscope-logo">
</h5>
- <img src="https://img.alicdn.com/imgextra/i3/O1CN01SFL0Gu26nrQBFKXFR_!!6000000007707-2-tps-500-500.png" alt="new" width="30" height="30"/>**[2024-05-24]** We are pleased to announce that features related to the **AgentScope Workstation** will soon be open-sourced! The online website services are temporarily offline. The online website service will be upgraded and back online shortly. Stay tuned...
- <img src="https://img.alicdn.com/imgextra/i3/O1CN01SFL0Gu26nrQBFKXFR_!!6000000007707-2-tps-500-500.png" alt="new" width="30" height="30"/>**[2024-05-15]** A new **Parser Module** for **formatted response** is added in AgentScope! Refer to our [tutorial](https://modelscope.github.io/agentscope/en/tutorial/203-parser.html) for more details. The [`DictDialogAgent`](https://github.com/modelscope/agentscope/blob/main/src/agentscope/agents/dict_dialog_agent.py) and [werewolf game](https://github.com/modelscope/agentscope/tree/main/examples/game_werewolf) example are updated simultaneously.
<https://github.com/qbc2016/AgentScope/assets/22984042/22d45aee-3470-4923-850f-348a5b0faaa7>
- <img src="https://img.alicdn.com/imgextra/i3/O1CN01SFL0Gu26nrQBFKXFR_!!6000000007707-2-tps-500-500.png" alt="new" width="30" height="30"/>**[2024-05-14]** Dear AgentScope users, we are conducting a survey on **AgentScope Workstation & Copilot** user experience. We currently need your valuable feedback to help us improve the experience of AgentScope's Drag & Drop multi-agent application development and Copilot. Your feedback is valuable and the survey will take about 3~5 minutes. Please click [URL](https://survey.aliyun.com/apps/zhiliao/vgpTppn22) to participate in questionnaire surveys. Thank you very much for your support and contribution!
- <img src="https://img.alicdn.com/imgextra/i3/O1CN01SFL0Gu26nrQBFKXFR_!!6000000007707-2-tps-500-500.png" alt="new" width="30" height="30"/>**[2024-05-14]** AgentScope supports **gpt-4o** as well as other OpenAI vision models now! Try gpt-4o with its [model configuration](./examples/model_configs_template/openai_chat_template.json) and new example [Conversation with gpt-4o](./examples/conversation_with_gpt-4o)!
- **[2024-04-30]** We release **AgentScope** v0.0.4 now!
- **[2024-04-27]** [AgentScope Workstation](https://agentscope.aliyun.com/) is now online! You are welcome to try building your multi-agent application simply with our *drag-and-drop platform* and ask our *copilot* questions about AgentScope!
- **[2024-04-19]** AgentScope supports Llama3 now! We provide [scripts](https://github.com/modelscope/agentscope/blob/main/examples/model_llama3) and example [model configuration](https://github.com/modelscope/agentscope/blob/main/examples/model_llama3) for quick set-up. Feel free to try llama3 in our examples!
- **[2024-04-06]** We release **AgentScope** v0.0.3 now!
- **[2024-04-06]** New examples [Gomoku](https://github.com/modelscope/agentscope/blob/main/examples/game_gomoku), [Conversation with ReAct Agent](https://github.com/modelscope/agentscope/blob/main/examples/conversation_with_react_agent), [Conversation with RAG Agent](https://github.com/modelscope/agentscope/blob/main/examples/conversation_with_RAG_agents) and [Distributed Parallel Optimization](https://github.com/modelscope/agentscope/blob/main/examples/distributed_parallel_optimization) are available now!
- **[2024-03-19]** We release **AgentScope** v0.0.2 now! In this new version,
AgentScope supports [ollama](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#supported-models)(A local CPU inference engine), [DashScope](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#supported-models) and Google [Gemini](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#supported-models) APIs.
- **[2024-03-19]** New examples ["Autonomous Conversation with Mentions"](https://github.com/modelscope/agentscope/blob/main/examples/conversation_with_mentions) and ["Basic Conversation with LangChain library"](https://github.com/modelscope/agentscope/blob/main/examples/conversation_with_langchain) are available now!
- **[2024-03-19]** The [Chinese tutorial](https://modelscope.github.io/agentscope/zh_CN/index.html) of AgentScope is online now!
- **[2024-02-27]** We release **AgentScope v0.0.1** now, which is also
available in [PyPI](https://pypi.org/project/agentscope/)!
- **[2024-02-14]** We release our paper "AgentScope: A Flexible yet Robust
Multi-Agent Platform" in [arXiv](https://arxiv.org/abs/2402.14034) now!
---
## What's AgentScope?
AgentScope is an innovative multi-agent platform designed to empower developers
to build multi-agent applications with large-scale models.
It features three high-level capabilities:
- 🤝 **Easy-to-Use**: Designed for developers, with [fruitful components](https://modelscope.github.io/agentscope/en/tutorial/204-service.html#),
[comprehensive documentation](https://modelscope.github.io/agentscope/en/index.html), and broad compatibility. Besides, [AgentScope Workstation](https://agentscope.aliyun.com/) provides a *drag-and-drop programming platform* and a *copilot* for beginners of AgentScope!
- ✅ **High Robustness**: Supporting customized fault-tolerance controls and
retry mechanisms to enhance application stability.
- 🚀 **Actor-Based Distribution**: Building distributed multi-agent
applications in a centralized programming manner for streamlined development.
**Supported Model Libraries**
AgentScope provides a list of `ModelWrapper` to support both local model
services and third-party model APIs.
| API | Task | Model Wrapper | Configuration | Some Supported Models |
|------------------------|-----------------|---------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|-----------------------------------------------------------------|
| OpenAI API | Chat | [`OpenAIChatWrapper`](https://github.com/modelscope/agentscope/blob/main/src/agentscope/models/openai_model.py) |[guidance](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#openai-api) <br> [template](https://github.com/modelscope/agentscope/blob/main/examples/model_configs_template/openai_chat_template.json) | gpt-4o, gpt-4, gpt-3.5-turbo, ... |
| | Embedding | [`OpenAIEmbeddingWrapper`](https://github.com/modelscope/agentscope/blob/main/src/agentscope/models/openai_model.py) | [guidance](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#openai-api) <br> [template](https://github.com/modelscope/agentscope/blob/main/examples/model_configs_template/openai_embedding_template.json) | text-embedding-ada-002, ... |
| | DALL·E | [`OpenAIDALLEWrapper`](https://github.com/modelscope/agentscope/blob/main/src/agentscope/models/openai_model.py) | [guidance](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#openai-api) <br> [template](https://github.com/modelscope/agentscope/blob/main/examples/model_configs_template/openai_dall_e_template.json) | dall-e-2, dall-e-3 |
| DashScope API | Chat | [`DashScopeChatWrapper`](https://github.com/modelscope/agentscope/blob/main/src/agentscope/models/dashscope_model.py) | [guidance](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#dashscope-api) <br> [template](https://github.com/modelscope/agentscope/blob/main/examples/model_configs_template/dashscope_chat_template.json) | qwen-plus, qwen-max, ... |
| | Image Synthesis | [`DashScopeImageSynthesisWrapper`](https://github.com/modelscope/agentscope/blob/main/src/agentscope/models/dashscope_model.py) | [guidance](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#dashscope-api) <br>[template](https://github.com/modelscope/agentscope/blob/main/examples/model_configs_template/dashscope_image_synthesis_template.json) | wanx-v1 |
| | Text Embedding | [`DashScopeTextEmbeddingWrapper`](https://github.com/modelscope/agentscope/blob/main/src/agentscope/models/dashscope_model.py) | [guidance](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#dashscope-api) <br> [template](https://github.com/modelscope/agentscope/blob/main/examples/model_configs_template/dashscope_text_embedding_template.json) | text-embedding-v1, text-embedding-v2, ... |
| | Multimodal | [`DashScopeMultiModalWrapper`](https://github.com/modelscope/agentscope/blob/main/src/agentscope/models/dashscope_model.py) | [guidance](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#dashscope-api) <br> [template](https://github.com/modelscope/agentscope/blob/main/examples/model_configs_template/dashscope_multimodal_template.json) | qwen-vl-max, qwen-vl-chat-v1, qwen-audio-chat |
| Gemini API | Chat | [`GeminiChatWrapper`](https://github.com/modelscope/agentscope/blob/main/src/agentscope/models/gemini_model.py) | [guidance](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#gemini-api) <br> [template](https://github.com/modelscope/agentscope/blob/main/examples/model_configs_template/gemini_chat_template.json) | gemini-pro, ... |
| | Embedding | [`GeminiEmbeddingWrapper`](https://github.com/modelscope/agentscope/blob/main/src/agentscope/models/gemini_model.py) | [guidance](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#gemini-api) <br> [template](https://github.com/modelscope/agentscope/blob/main/examples/model_configs_template/gemini_embedding_template.json) | models/embedding-001, ... |
| ZhipuAI API | Chat | [`ZhipuAIChatWrapper`](https://github.com/modelscope/agentscope/blob/main/src/agentscope/models/zhipu_model.py) | [guidance](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#zhipu-api) <br> [template](https://github.com/modelscope/agentscope/blob/main/examples/model_configs_template/zhipu_chat_template.json) | glm-4, ... |
| | Embedding | [`ZhipuAIEmbeddingWrapper`](https://github.com/modelscope/agentscope/blob/main/src/agentscope/models/zhipu_model.py) | [guidance](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#zhipu-api) <br> [template](https://github.com/modelscope/agentscope/blob/main/examples/model_configs_template/zhipu_embedding_template.json) | embedding-2, ... |
| ollama | Chat | [`OllamaChatWrapper`](https://github.com/modelscope/agentscope/blob/main/src/agentscope/models/ollama_model.py) | [guidance](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#ollama-api) <br> [template](https://github.com/modelscope/agentscope/blob/main/examples/model_configs_template/ollama_chat_template.json) | llama3, llama2, Mistral, ... |
| | Embedding | [`OllamaEmbeddingWrapper`](https://github.com/modelscope/agentscope/blob/main/src/agentscope/models/ollama_model.py) | [guidance](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#ollama-api) <br> [template](https://github.com/modelscope/agentscope/blob/main/examples/model_configs_template/ollama_embedding_template.json) | llama2, Mistral, ... |
| | Generation | [`OllamaGenerationWrapper`](https://github.com/modelscope/agentscope/blob/main/src/agentscope/models/ollama_model.py) | [guidance](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#ollama-api) <br> [template](https://github.com/modelscope/agentscope/blob/main/examples/model_configs_template/ollama_generate_template.json) | llama2, Mistral, ... |
| LiteLLM API | Chat | [`LiteLLMChatWrapper`](https://github.com/modelscope/agentscope/blob/main/src/agentscope/models/litellm_model.py) | [guidance](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#litellm-api) <br> [template](https://github.com/modelscope/agentscope/blob/main/examples/model_configs_template/litellm_chat_template.json) | [models supported by litellm](https://docs.litellm.ai/docs/)... |
| Post Request based API | - | [`PostAPIModelWrapper`](https://github.com/modelscope/agentscope/blob/main/src/agentscope/models/post_model.py) | [guidance](https://modelscope.github.io/agentscope/en/tutorial/203-model.html#post-request-api) <br> [template](https://github.com/modelscope/agentscope/blob/main/examples/model_configs_template/postapi_model_config_template.json) | - |
**Supported Local Model Deployment**
AgentScope enables developers to rapidly deploy local model services using
the following libraries.
- [ollama (CPU inference)](https://github.com/modelscope/agentscope/blob/main/scripts/README.md#ollama)
- [Flask + Transformers](https://github.com/modelscope/agentscope/blob/main/scripts/README.md#with-transformers-library)
- [Flask + ModelScope](https://github.com/modelscope/agentscope/blob/main/scripts/README.md#with-modelscope-library)
- [FastChat](https://github.com/modelscope/agentscope/blob/main/scripts/README.md#fastchat)
- [vllm](https://github.com/modelscope/agentscope/blob/main/scripts/README.md#vllm)
**Supported Services**
- Web Search
- Data Query
- Retrieval
- Code Execution
- File Operation
- Text Processing
- Multi Modality
**Example Applications**
- Model
- <img src="https://img.alicdn.com/imgextra/i3/O1CN01SFL0Gu26nrQBFKXFR_!!6000000007707-2-tps-500-500.png" alt="new" width="30" height="30"/>[Using Llama3 in AgentScope](https://github.com/modelscope/agentscope/blob/main/examples/model_llama3)
- Conversation
- [Basic Conversation](https://github.com/modelscope/agentscope/blob/main/examples/conversation_basic)
- [Autonomous Conversation with Mentions](https://github.com/modelscope/agentscope/blob/main/examples/conversation_with_mentions)
- [Self-Organizing Conversation](https://github.com/modelscope/agentscope/blob/main/examples/conversation_self_organizing)
- [Basic Conversation with LangChain library](https://github.com/modelscope/agentscope/blob/main/examples/conversation_with_langchain)
- [Conversation with ReAct Agent](https://github.com/modelscope/agentscope/blob/main/examples/conversation_with_react_agent)
- [Conversation in Natural Language to Query SQL](https://github.com/modelscope/agentscope/blob/main/examples/conversation_nl2sql/)
- [Conversation with RAG Agent](https://github.com/modelscope/agentscope/blob/main/examples/conversation_with_RAG_agents)
- <img src="https://img.alicdn.com/imgextra/i3/O1CN01SFL0Gu26nrQBFKXFR_!!6000000007707-2-tps-500-500.png" alt="new" width="30" height="30"/>[Conversation with gpt-4o](https://github.com/modelscope/agentscope/blob/main/examples/conversation_with_gpt-4o)
- <img src="https://img.alicdn.com/imgextra/i3/O1CN01SFL0Gu26nrQBFKXFR_!!6000000007707-2-tps-500-500.png" alt="new" width="30" height="30"/>[Conversation with Software Engineering Agent](https://github.com/modelscope/agentscope/blob/main/examples/conversation_with_swe-agent/)
- <img src="https://img.alicdn.com/imgextra/i3/O1CN01SFL0Gu26nrQBFKXFR_!!6000000007707-2-tps-500-500.png" alt="new" width="30" height="30"/>[Conversation with Customized Services](https://github.com/modelscope/agentscope/blob/main/examples/conversation_with_customized_services/)
- Game
- [Gomoku](https://github.com/modelscope/agentscope/blob/main/examples/game_gomoku)
- [Werewolf](https://github.com/modelscope/agentscope/blob/main/examples/game_werewolf)
- Distribution
- [Distributed Conversation](https://github.com/modelscope/agentscope/blob/main/examples/distributed_conversation)
- [Distributed Debate](https://github.com/modelscope/agentscope/blob/main/examples/distributed_debate)
- [Distributed Parallel Optimization](https://github.com/modelscope/agentscope/blob/main/examples/distributed_parallel_optimization)
- [Distributed Large Scale Simulation](https://github.com/modelscope/agentscope/blob/main/examples/distributed_simulation)
More models, services and examples are coming soon!
## Installation
AgentScope requires **Python 3.9** or higher.
***Note: This project is currently in active development, it's recommended to
install AgentScope from source.***
### From source
- Install AgentScope in editable mode:
```bash
# Pull the source code from GitHub
git clone https://github.com/modelscope/agentscope.git
# Install the package in editable mode
cd agentscope
pip install -e .
```
- To build distributed multi-agent applications:
```bash
# On windows
pip install -e .[distribute]
# On mac
pip install -e .\[distribute\]
```
### Using pip
- Install AgentScope from pip:
```bash
pip install agentscope --pre
```
## Quick Start
### Configuration
In AgentScope, the model deployment and invocation are decoupled by
`ModelWrapper`.
To use these model wrappers, you need to prepare a model config file as
follows.
```python
model_config = {
# The identifies of your config and used model wrapper
"config_name": "{your_config_name}", # The name to identify the config
"model_type": "{model_type}", # The type to identify the model wrapper
# Detailed parameters into initialize the model wrapper
# ...
}
```
Taking OpenAI Chat API as an example, the model configuration is as follows:
```python
openai_model_config = {
"config_name": "my_openai_config", # The name to identify the config
"model_type": "openai_chat", # The type to identify the model wrapper
# Detailed parameters into initialize the model wrapper
"model_name": "gpt-4", # The used model in openai API, e.g. gpt-4, gpt-3.5-turbo, etc.
"api_key": "xxx", # The API key for OpenAI API. If not set, env
# variable OPENAI_API_KEY will be used.
"organization": "xxx", # The organization for OpenAI API. If not set, env
# variable OPENAI_ORGANIZATION will be used.
}
```
More details about how to set up local model services and prepare model
configurations is in our
[tutorial](https://modelscope.github.io/agentscope/index.html#welcome-to-agentscope-tutorial-hub).
### Create Agents
Create built-in user and assistant agents as follows.
```python
from agentscope.agents import DialogAgent, UserAgent
import agentscope
# Load model configs
agentscope.init(model_configs="./model_configs.json")
# Create a dialog agent and a user agent
dialog_agent = DialogAgent(name="assistant",
model_config_name="my_openai_config")
user_agent = UserAgent()
```
### Construct Conversation
In AgentScope, **message** is the bridge among agents, which is a
**dict** that contains two necessary fields `name` and `content` and an
optional field `url` to local files (image, video or audio) or website.
```python
from agentscope.message import Msg
x = Msg(name="Alice", content="Hi!")
x = Msg("Bob", "What about this picture I took?", url="/path/to/picture.jpg")
```
Start a conversation between two agents (e.g. dialog_agent and user_agent)
with the following code:
```python
x = None
while True:
x = dialog_agent(x)
x = user_agent(x)
if x.content == "exit": # user input "exit" to exit the conversation_basic
break
```
### AgentScope Studio
AgentScope provides an easy-to-use runtime user interface capable of
displaying multimodal output on the front end, including text, images,
audio and video.
Refer to our [tutorial](https://modelscope.github.io/agentscope/en/tutorial/209-gui.html) for more details.
<h5 align="center">
<img src="https://img.alicdn.com/imgextra/i4/O1CN015kjnkd1xdwJoNxqLZ_!!6000000006467-0-tps-3452-1984.jpg" width="600" alt="agentscope-logo">
</h5>
## Tutorial
- [About AgentScope](https://modelscope.github.io/agentscope/zh_CN/tutorial/101-agentscope.html)
- [Installation](https://modelscope.github.io/agentscope/zh_CN/tutorial/102-installation.html)
- [Quick Start](https://modelscope.github.io/agentscope/zh_CN/tutorial/103-example.html)
- [Model](https://modelscope.github.io/agentscope/zh_CN/tutorial/203-model.html)
- [Prompt Engineering](https://modelscope.github.io/agentscope/zh_CN/tutorial/206-prompt.html)
- [Agent](https://modelscope.github.io/agentscope/zh_CN/tutorial/201-agent.html)
- [Memory](https://modelscope.github.io/agentscope/zh_CN/tutorial/205-memory.html)
- [Response Parser](https://modelscope.github.io/agentscope/zh_CN/tutorial/203-parser.html)
- [Tool](https://modelscope.github.io/agentscope/zh_CN/tutorial/204-service.html)
- [Pipeline and MsgHub](https://modelscope.github.io/agentscope/zh_CN/tutorial/202-pipeline.html)
- [Distribution](https://modelscope.github.io/agentscope/zh_CN/tutorial/208-distribute.html)
- [AgentScope Studio](https://modelscope.github.io/agentscope/zh_CN/tutorial/209-gui.html)
- [Logging](https://modelscope.github.io/agentscope/zh_CN/tutorial/105-logging.html)
- [Monitor](https://modelscope.github.io/agentscope/zh_CN/tutorial/207-monitor.html)
- [Example: Werewolf Game](https://modelscope.github.io/agentscope/zh_CN/tutorial/104-usecase.html)
## License
AgentScope is released under Apache License 2.0.
## Contributing
Contributions are always welcomed!
We provide a developer version with additional pre-commit hooks to perform
checks compared to the official version:
```bash
# For windows
pip install -e .[dev]
# For mac
pip install -e .\[dev\]
# Install pre-commit hooks
pre-commit install
```
Please refer to our [Contribution Guide](https://modelscope.github.io/agentscope/en/tutorial/302-contribute.html) for more details.
## References
If you find our work helpful for your research or application, please
cite [our paper](https://arxiv.org/abs/2402.14034):
```
@article{agentscope,
author = {Dawei Gao and
Zitao Li and
Xuchen Pan and
Weirui Kuang and
Zhijian Ma and
Bingchen Qian and
Fei Wei and
Wenhao Zhang and
Yuexiang Xie and
Daoyuan Chen and
Liuyi Yao and
Hongyi Peng and
Zeyu Zhang and
Lin Zhu and
Chen Cheng and
Hongzhu Shi and
Yaliang Li and
Bolin Ding and
Jingren Zhou},
title = {AgentScope: A Flexible yet Robust Multi-Agent Platform},
journal = {CoRR},
volume = {abs/2402.14034},
year = {2024},
}
```

View File

@@ -0,0 +1,204 @@
LICENSE
README.md
pyproject.toml
setup.py
src/agentscope/__init__.py
src/agentscope/_init.py
src/agentscope/_runtime.py
src/agentscope/_version.py
src/agentscope/constants.py
src/agentscope/exception.py
src/agentscope/file_manager.py
src/agentscope/logging.py
src/agentscope/message.py
src/agentscope/msghub.py
src/agentscope.egg-info/PKG-INFO
src/agentscope.egg-info/SOURCES.txt
src/agentscope.egg-info/dependency_links.txt
src/agentscope.egg-info/entry_points.txt
src/agentscope.egg-info/requires.txt
src/agentscope.egg-info/top_level.txt
src/agentscope/agents/__init__.py
src/agentscope/agents/agent.py
src/agentscope/agents/dialog_agent.py
src/agentscope/agents/dict_dialog_agent.py
src/agentscope/agents/operator.py
src/agentscope/agents/rag_agent.py
src/agentscope/agents/react_agent.py
src/agentscope/agents/rpc_agent.py
src/agentscope/agents/text_to_image_agent.py
src/agentscope/agents/user_agent.py
src/agentscope/memory/__init__.py
src/agentscope/memory/memory.py
src/agentscope/memory/temporary_memory.py
src/agentscope/models/__init__.py
src/agentscope/models/config.py
src/agentscope/models/dashscope_model.py
src/agentscope/models/gemini_model.py
src/agentscope/models/litellm_model.py
src/agentscope/models/model.py
src/agentscope/models/ollama_model.py
src/agentscope/models/openai_model.py
src/agentscope/models/post_model.py
src/agentscope/models/response.py
src/agentscope/models/zhipu_model.py
src/agentscope/parsers/__init__.py
src/agentscope/parsers/code_block_parser.py
src/agentscope/parsers/json_object_parser.py
src/agentscope/parsers/parser_base.py
src/agentscope/parsers/tagged_content_parser.py
src/agentscope/pipelines/__init__.py
src/agentscope/pipelines/functional.py
src/agentscope/pipelines/pipeline.py
src/agentscope/prompt/__init__.py
src/agentscope/prompt/_prompt_comparer.py
src/agentscope/prompt/_prompt_engine.py
src/agentscope/prompt/_prompt_examples.json
src/agentscope/prompt/_prompt_generator_base.py
src/agentscope/prompt/_prompt_generator_en.py
src/agentscope/prompt/_prompt_generator_zh.py
src/agentscope/prompt/_prompt_optimizer.py
src/agentscope/prompt/_prompt_utils.py
src/agentscope/rag/__init__.py
src/agentscope/rag/knowledge.py
src/agentscope/rag/knowledge_bank.py
src/agentscope/rag/llama_index_knowledge.py
src/agentscope/rpc/__init__.py
src/agentscope/rpc/rpc_agent_client.py
src/agentscope/rpc/rpc_agent_pb2.py
src/agentscope/rpc/rpc_agent_pb2_grpc.py
src/agentscope/server/__init__.py
src/agentscope/server/launcher.py
src/agentscope/server/servicer.py
src/agentscope/service/__init__.py
src/agentscope/service/service_response.py
src/agentscope/service/service_status.py
src/agentscope/service/service_toolkit.py
src/agentscope/service/execute_code/__init__.py
src/agentscope/service/execute_code/exec_python.py
src/agentscope/service/execute_code/exec_shell.py
src/agentscope/service/file/__init__.py
src/agentscope/service/file/common.py
src/agentscope/service/file/json.py
src/agentscope/service/file/text.py
src/agentscope/service/multi_modality/__init__.py
src/agentscope/service/multi_modality/dashscope_services.py
src/agentscope/service/multi_modality/openai_services.py
src/agentscope/service/retrieval/__init__.py
src/agentscope/service/retrieval/retrieval_from_list.py
src/agentscope/service/retrieval/similarity.py
src/agentscope/service/sql_query/__init__.py
src/agentscope/service/sql_query/mongodb.py
src/agentscope/service/sql_query/mysql.py
src/agentscope/service/sql_query/sqlite.py
src/agentscope/service/text_processing/__init__.py
src/agentscope/service/text_processing/summarization.py
src/agentscope/service/web/__init__.py
src/agentscope/service/web/arxiv.py
src/agentscope/service/web/dblp.py
src/agentscope/service/web/download.py
src/agentscope/service/web/search.py
src/agentscope/service/web/web_digest.py
src/agentscope/studio/__init__.py
src/agentscope/studio/_app.py
src/agentscope/studio/_client.py
src/agentscope/studio/static/css/base.css
src/agentscope/studio/static/css/dashboard-detail-code.css
src/agentscope/studio/static/css/dashboard-detail-dialogue.css
src/agentscope/studio/static/css/dashboard-detail-invocation.css
src/agentscope/studio/static/css/dashboard-detail.css
src/agentscope/studio/static/css/dashboard-runs.css
src/agentscope/studio/static/css/dashboard.css
src/agentscope/studio/static/css/font.css
src/agentscope/studio/static/css/index.css
src/agentscope/studio/static/css/workstation-drag-components.css
src/agentscope/studio/static/css/workstation.css
src/agentscope/studio/static/css_third_party/clusterize.css
src/agentscope/studio/static/css_third_party/drawflow.min.css
src/agentscope/studio/static/css_third_party/highlight.min.css
src/agentscope/studio/static/css_third_party/katex.min.css
src/agentscope/studio/static/css_third_party/prism-line-numbers.min.css
src/agentscope/studio/static/css_third_party/prism.min.css
src/agentscope/studio/static/css_third_party/tabulator.min.css
src/agentscope/studio/static/fonts/KRYPTON.ttf
src/agentscope/studio/static/fonts/SourceSans3-Regular.ttf
src/agentscope/studio/static/html/dashboard-detail-code.html
src/agentscope/studio/static/html/dashboard-detail-dialogue.html
src/agentscope/studio/static/html/dashboard-detail-invocation.html
src/agentscope/studio/static/html/dashboard-detail.html
src/agentscope/studio/static/html/dashboard-runs.html
src/agentscope/studio/static/html/dashboard.html
src/agentscope/studio/static/html/index-guide.html
src/agentscope/studio/static/html/market.html
src/agentscope/studio/static/html/server.html
src/agentscope/studio/static/html/template.html
src/agentscope/studio/static/html/workstation_iframe.html
src/agentscope/studio/static/html-drag-components/agent-dialogagent.html
src/agentscope/studio/static/html-drag-components/agent-dictdialogagent.html
src/agentscope/studio/static/html-drag-components/agent-reactagent.html
src/agentscope/studio/static/html-drag-components/agent-texttoimageagent.html
src/agentscope/studio/static/html-drag-components/agent-useragent.html
src/agentscope/studio/static/html-drag-components/message-msg.html
src/agentscope/studio/static/html-drag-components/model-dashscope-chat.html
src/agentscope/studio/static/html-drag-components/model-openai-chat.html
src/agentscope/studio/static/html-drag-components/model-post-api-chat.html
src/agentscope/studio/static/html-drag-components/model-post-api-dall-e.html
src/agentscope/studio/static/html-drag-components/pipeline-forlooppipeline.html
src/agentscope/studio/static/html-drag-components/pipeline-ifelsepipeline.html
src/agentscope/studio/static/html-drag-components/pipeline-msghub.html
src/agentscope/studio/static/html-drag-components/pipeline-placeholder.html
src/agentscope/studio/static/html-drag-components/pipeline-sequentialpipeline.html
src/agentscope/studio/static/html-drag-components/pipeline-switchpipeline.html
src/agentscope/studio/static/html-drag-components/pipeline-whilelooppipeline.html
src/agentscope/studio/static/html-drag-components/service-bing-search.html
src/agentscope/studio/static/html-drag-components/service-execute-python.html
src/agentscope/studio/static/html-drag-components/service-google-search.html
src/agentscope/studio/static/html-drag-components/service-read-text.html
src/agentscope/studio/static/html-drag-components/service-write-text.html
src/agentscope/studio/static/html-drag-components/welcome.html
src/agentscope/studio/static/js/dashboard-detail-code.js
src/agentscope/studio/static/js/dashboard-detail-dialogue.js
src/agentscope/studio/static/js/dashboard-detail-invocation.js
src/agentscope/studio/static/js/dashboard-detail.js
src/agentscope/studio/static/js/dashboard-runs.js
src/agentscope/studio/static/js/dashboard.js
src/agentscope/studio/static/js/index.js
src/agentscope/studio/static/js/workstation.js
src/agentscope/studio/static/js/workstation_iframe.js
src/agentscope/studio/static/js_third_party/clusterize.min.js
src/agentscope/studio/static/js_third_party/drawflow.min.js
src/agentscope/studio/static/js_third_party/highlight.min.js
src/agentscope/studio/static/js_third_party/jquery.min.js
src/agentscope/studio/static/js_third_party/katex.min.js
src/agentscope/studio/static/js_third_party/marked-katex-extension.umd.js
src/agentscope/studio/static/js_third_party/marked.min.js
src/agentscope/studio/static/js_third_party/micromodal.min.js
src/agentscope/studio/static/js_third_party/monaco-editor.loader.js
src/agentscope/studio/static/js_third_party/prism-line-numbers.min.js
src/agentscope/studio/static/js_third_party/prism-python.min.js
src/agentscope/studio/static/js_third_party/prism.min.js
src/agentscope/studio/static/js_third_party/socket.io.js
src/agentscope/studio/static/js_third_party/sweetalert2@9
src/agentscope/studio/static/js_third_party/tabulator.min.js
src/agentscope/studio/static/workstation_templates/en1.json
src/agentscope/studio/static/workstation_templates/en2.json
src/agentscope/studio/static/workstation_templates/en3.json
src/agentscope/studio/static/workstation_templates/en4.json
src/agentscope/studio/static/workstation_templates/zh1.json
src/agentscope/studio/templates/index.html
src/agentscope/studio/templates/workstation.html
src/agentscope/utils/__init__.py
src/agentscope/utils/common.py
src/agentscope/utils/monitor.py
src/agentscope/utils/token_utils.py
src/agentscope/utils/tools.py
src/agentscope/web/__init__.py
src/agentscope/web/gradio/__init__.py
src/agentscope/web/gradio/constants.py
src/agentscope/web/gradio/studio.py
src/agentscope/web/gradio/utils.py
src/agentscope/web/workstation/__init__.py
src/agentscope/web/workstation/workflow.py
src/agentscope/web/workstation/workflow_dag.py
src/agentscope/web/workstation/workflow_node.py
src/agentscope/web/workstation/workflow_utils.py

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,5 @@
[console_scripts]
as_gradio = agentscope.web.gradio.studio:run_app
as_server = agentscope.server.launcher:as_server
as_studio = agentscope.studio:init
as_workflow = agentscope.web.workstation.workflow:main

View File

@@ -0,0 +1,133 @@
networkx
black
docstring_parser
pydantic
loguru==0.6.0
tiktoken
Pillow
requests
chardet
inputimeout
openai>=1.3.0
numpy
Flask==3.0.0
Flask-Cors==4.0.0
Flask-SocketIO==5.3.6
flask_sqlalchemy
flake8
dashscope==1.14.1
openai>=1.3.0
ollama>=0.1.7
google-generativeai>=0.4.0
zhipuai
litellm
psutil
scipy
[dev]
networkx
black
docstring_parser
pydantic
loguru==0.6.0
tiktoken
Pillow
requests
chardet
inputimeout
openai>=1.3.0
numpy
Flask==3.0.0
Flask-Cors==4.0.0
Flask-SocketIO==5.3.6
flask_sqlalchemy
flake8
dashscope==1.14.1
ollama>=0.1.7
google-generativeai>=0.4.0
zhipuai
litellm
psutil
scipy
pytest
pytest-cov
pre-commit
[distribute]
networkx
black
docstring_parser
pydantic
loguru==0.6.0
tiktoken
Pillow
requests
chardet
inputimeout
openai>=1.3.0
numpy
Flask==3.0.0
Flask-Cors==4.0.0
Flask-SocketIO==5.3.6
flask_sqlalchemy
flake8
dashscope==1.14.1
ollama>=0.1.7
google-generativeai>=0.4.0
zhipuai
litellm
psutil
scipy
grpcio==1.60.0
grpcio-tools==1.60.0
protobuf==4.25.0
expiringdict
dill
[full]
networkx
black
docstring_parser
pydantic
loguru==0.6.0
tiktoken
Pillow
requests
chardet
inputimeout
openai>=1.3.0
numpy
Flask==3.0.0
Flask-Cors==4.0.0
Flask-SocketIO==5.3.6
flask_sqlalchemy
flake8
dashscope==1.14.1
ollama>=0.1.7
google-generativeai>=0.4.0
zhipuai
litellm
psutil
scipy
grpcio==1.60.0
grpcio-tools==1.60.0
protobuf==4.25.0
expiringdict
dill
docker
pymongo
pymysql
bs4
beautifulsoup4
feedparser
sphinx
sphinx-autobuild
sphinx_rtd_theme
myst-parser
sphinxcontrib-mermaid
pytest
pytest-cov
pre-commit
gradio==4.19.1
modelscope_studio==0.0.5
llama-index==0.10.30

View File

@@ -0,0 +1 @@
agentscope

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
""" Import all modules in the package. """
# modules
from . import agents
from . import memory
from . import models
from . import pipelines
from . import service
from . import message
from . import prompt
from . import web
from . import exception
from . import parsers
from . import rag
# objects or function
from .msghub import msghub
from ._version import __version__
from ._init import init
__all__ = [
"init",
"msghub",
]

View File

@@ -0,0 +1,204 @@
# -*- coding: utf-8 -*-
"""The init function for the package."""
import json
import os
import shutil
from typing import Optional, Union, Sequence
from agentscope import agents
from .agents import AgentBase
from ._runtime import _runtime
from .file_manager import file_manager
from .logging import LOG_LEVEL, setup_logger
from .utils.monitor import MonitorFactory
from .models import read_model_configs
from .constants import _DEFAULT_DIR
from .constants import _DEFAULT_LOG_LEVEL
from .studio._client import _studio_client
# init setting
_INIT_SETTINGS = {}
def init(
model_configs: Optional[Union[dict, str, list]] = None,
project: Optional[str] = None,
name: Optional[str] = None,
save_dir: str = _DEFAULT_DIR,
save_log: bool = True,
save_code: bool = True,
save_api_invoke: bool = False,
use_monitor: bool = True,
logger_level: LOG_LEVEL = _DEFAULT_LOG_LEVEL,
runtime_id: Optional[str] = None,
agent_configs: Optional[Union[str, list, dict]] = None,
studio_url: Optional[str] = None,
) -> Sequence[AgentBase]:
"""A unified entry to initialize the package, including model configs,
runtime names, saving directories and logging settings.
Args:
model_configs (`Optional[Union[dict, str, list]]`, defaults to `None`):
A dict, a list of dicts, or a path to a json file containing
model configs.
project (`Optional[str]`, defaults to `None`):
The project name, which is used to identify the project.
name (`Optional[str]`, defaults to `None`):
The name for runtime, which is used to identify this runtime.
runtime_id (`Optional[str]`, defaults to `None`):
The id for runtime, which is used to identify this runtime. Use
`None` will generate a random id.
save_dir (`str`, defaults to `./runs`):
The directory to save logs, files, codes, and api invocations.
If `dir` is `None`, when saving logs, files, codes, and api
invocations, the default directory `./runs` will be created.
save_log (`bool`, defaults to `False`):
Whether to save logs locally.
save_code (`bool`, defaults to `False`):
Whether to save codes locally.
save_api_invoke (`bool`, defaults to `False`):
Whether to save api invocations locally, including model and web
search invocation.
use_monitor (`bool`, defaults to `True`):
Whether to activate the monitor.
logger_level (`LOG_LEVEL`, defaults to `"INFO"`):
The logging level of logger.
agent_configs (`Optional[Union[str, list, dict]]`, defaults to `None`):
The config dict(s) of agents or the path to the config file,
which can be loaded by json.loads(). One agent config should
cover the required arguments to initialize a specific agent
object, otherwise the default values will be used.
studio_url (`Optional[str]`, defaults to `None`):
The url of the agentscope studio.
"""
init_process(
model_configs=model_configs,
project=project,
name=name,
runtime_id=runtime_id,
save_dir=save_dir,
save_api_invoke=save_api_invoke,
save_log=save_log,
use_monitor=use_monitor,
logger_level=logger_level,
studio_url=studio_url,
)
# save init settings for subprocess
_INIT_SETTINGS["model_configs"] = model_configs
_INIT_SETTINGS["project"] = _runtime.project
_INIT_SETTINGS["name"] = _runtime.name
_INIT_SETTINGS["runtime_id"] = _runtime.runtime_id
_INIT_SETTINGS["save_dir"] = save_dir
_INIT_SETTINGS["save_api_invoke"] = save_api_invoke
_INIT_SETTINGS["save_log"] = save_log
_INIT_SETTINGS["logger_level"] = logger_level
_INIT_SETTINGS["use_monitor"] = use_monitor
# Save code if needed
if save_code:
# Copy python file in os.path.curdir into runtime directory
cur_dir = os.path.abspath(os.path.curdir)
for filename in os.listdir(cur_dir):
if filename.endswith(".py"):
file_abs = os.path.join(cur_dir, filename)
shutil.copy(file_abs, str(file_manager.dir_code))
# Load config and init agent by configs
if agent_configs is not None:
if isinstance(agent_configs, str):
with open(agent_configs, "r", encoding="utf-8") as file:
configs = json.load(file)
elif isinstance(agent_configs, dict):
configs = [agent_configs]
else:
configs = agent_configs
# setup agents
agent_objs = []
for config in configs:
agent_cls = getattr(agents, config["class"])
agent_args = config["args"]
agent = agent_cls(**agent_args)
agent_objs.append(agent)
return agent_objs
return []
def init_process(
model_configs: Optional[Union[dict, str, list]] = None,
project: Optional[str] = None,
name: Optional[str] = None,
runtime_id: Optional[str] = None,
save_dir: str = _DEFAULT_DIR,
save_api_invoke: bool = False,
save_log: bool = False,
use_monitor: bool = True,
logger_level: LOG_LEVEL = _DEFAULT_LOG_LEVEL,
studio_url: Optional[str] = None,
) -> None:
"""An entry to initialize the package in a process.
Args:
project (`Optional[str]`, defaults to `None`):
The project name, which is used to identify the project.
name (`Optional[str]`, defaults to `None`):
The name for runtime, which is used to identify this runtime.
runtime_id (`Optional[str]`, defaults to `None`):
The id for runtime, which is used to identify this runtime.
save_dir (`str`, defaults to `./runs`):
The directory to save logs, files, codes, and api invocations.
If `dir` is `None`, when saving logs, files, codes, and api
invocations, the default directory `./runs` will be created.
save_api_invoke (`bool`, defaults to `False`):
Whether to save api invocations locally, including model and web
search invocation.
model_configs (`Optional[Sequence]`, defaults to `None`):
A sequence of pre-init model configs.
save_log (`bool`, defaults to `False`):
Whether to save logs locally.
use_monitor (`bool`, defaults to `True`):
Whether to activate the monitor.
logger_level (`LOG_LEVEL`, defaults to `"INFO"`):
The logging level of logger.
studio_url (`Optional[str]`, defaults to `None`):
The url of the agentscope studio.
"""
# Init the runtime
if project is not None:
_runtime.project = project
if name is not None:
_runtime.name = name
if runtime_id is not None:
_runtime.runtime_id = runtime_id
# Init file manager and save configs by default
file_manager.init(save_dir, save_api_invoke)
# Init logger
dir_log = str(file_manager.dir_log) if save_log else None
setup_logger(dir_log, logger_level)
# Load model configs if needed
if model_configs is not None:
read_model_configs(model_configs)
# Init monitor
_ = MonitorFactory.get_monitor(
db_path=file_manager.path_db,
impl_type="sqlite" if use_monitor else "dummy",
)
# Init studio client, which will push messages to web ui and fetch user
# inputs from web ui
if studio_url is not None:
_studio_client.initialize(_runtime.runtime_id, studio_url)
# Register in AgentScope Studio
_studio_client.register_running_instance(
project=_runtime.project,
name=_runtime.name,
timestamp=_runtime.timestamp,
run_dir=file_manager.dir_root,
pid=os.getpid(),
)

View File

@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
"""Manage the id for each runtime"""
import os
from datetime import datetime
from typing import Any
from agentscope.utils.tools import _get_timestamp
from agentscope.utils.tools import _get_process_creation_time
from agentscope.utils.tools import _generate_random_code
_RUNTIME_ID_FORMAT = "run_%Y%m%d-%H%M%S_{}"
_RUNTIME_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
class _Runtime:
"""A singleton class used to record the runtime information, which will
be initialized when the package is imported."""
project: str
"""The project name, which is used to identify the project."""
name: str
"""The name for runtime, which is used to identify this runtime."""
runtime_id: str
"""The id for runtime, which is used to identify the this runtime and
name the saving directory."""
pid: int
"""The process id of the runtime."""
_timestamp: datetime
"""The timestamp of when the runtime is initialized."""
_instance = None
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
"""Create a singleton instance."""
if not cls._instance:
cls._instance = super(_Runtime, cls).__new__(
cls,
*args,
**kwargs,
)
return cls._instance
def __init__(self) -> None:
"""Generate random project name, runtime name and default
runtime_id when the package is initialized. After that, user can set
them by calling `agentscope.init(project="xxx", name="xxx",
runtime_id="xxx")`."""
self.project = _generate_random_code()
self.name = _generate_random_code(uppercase=False)
self.pid = os.getpid()
self._timestamp = _get_process_creation_time()
# Obtain time from timestamp in string format, and then turn it into
# runtime ID format
self.runtime_id = _get_timestamp(
_RUNTIME_ID_FORMAT,
self._timestamp,
).format(self.name)
def generate_new_runtime_id(self) -> str:
"""Generate a new random runtime id."""
return _get_timestamp(_RUNTIME_ID_FORMAT).format(
_generate_random_code(uppercase=False),
)
@property
def timestamp(self) -> str:
"""Get the current timestamp in specific format."""
return self._timestamp.strftime(_RUNTIME_TIMESTAMP_FORMAT)
@staticmethod
def _flush() -> None:
"""
Only for unittest usage. Don't use this function in your code.
Flush the runtime singleton.
"""
global _runtime
_runtime = _Runtime()
_runtime = _Runtime()

View File

@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
""" Version of AgentScope."""
__version__ = "0.0.6alpha3"

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
""" Import all agent related modules in the package. """
from .agent import AgentBase, DistConf
from .operator import Operator
from .dialog_agent import DialogAgent
from .dict_dialog_agent import DictDialogAgent
from .user_agent import UserAgent
from .text_to_image_agent import TextToImageAgent
from .rpc_agent import RpcAgent
from .react_agent import ReActAgent
from .rag_agent import LlamaIndexAgent
__all__ = [
"AgentBase",
"Operator",
"DialogAgent",
"DictDialogAgent",
"TextToImageAgent",
"UserAgent",
"ReActAgent",
"DistConf",
"RpcAgent",
"LlamaIndexAgent",
]

View File

@@ -0,0 +1,458 @@
# -*- coding: utf-8 -*-
""" Base class for Agent """
from __future__ import annotations
from abc import ABCMeta
from typing import Optional
from typing import Sequence
from typing import Union
from typing import Any
from typing import Type
import uuid
from loguru import logger
from agentscope.agents.operator import Operator
from agentscope.message import Msg
from agentscope.models import load_model_by_config_name
from agentscope.memory import TemporaryMemory
class _AgentMeta(ABCMeta):
"""The meta-class for agent.
1. record the init args into `_init_settings` field.
2. register class name into `registry` field.
"""
def __init__(cls, name: Any, bases: Any, attrs: Any) -> None:
if not hasattr(cls, "_registry"):
cls._registry = {}
else:
if name in cls._registry:
logger.warning(
f"Agent class with name [{name}] already exists.",
)
else:
cls._registry[name] = cls
super().__init__(name, bases, attrs)
def __call__(cls, *args: tuple, **kwargs: dict) -> Any:
to_dist = kwargs.pop("to_dist", False)
if to_dist is True:
to_dist = DistConf()
if to_dist is not False and to_dist is not None:
from .rpc_agent import RpcAgent
if cls is not RpcAgent and not issubclass(cls, RpcAgent):
return RpcAgent(
name=(
args[0]
if len(args) > 0
else kwargs["name"] # type: ignore[arg-type]
),
host=to_dist.pop( # type: ignore[arg-type]
"host",
"localhost",
),
port=to_dist.pop("port", None), # type: ignore[arg-type]
max_pool_size=kwargs.pop( # type: ignore[arg-type]
"max_pool_size",
8192,
),
max_timeout_seconds=to_dist.pop( # type: ignore[arg-type]
"max_timeout_seconds",
1800,
),
local_mode=to_dist.pop( # type: ignore[arg-type]
"local_mode",
True,
),
lazy_launch=to_dist.pop( # type: ignore[arg-type]
"lazy_launch",
True,
),
agent_id=cls.generate_agent_id(),
connect_existing=False,
agent_class=cls,
agent_configs={
"args": args,
"kwargs": kwargs,
"class_name": cls.__name__,
},
)
instance = super().__call__(*args, **kwargs)
instance._init_settings = {
"args": args,
"kwargs": kwargs,
"class_name": cls.__name__,
}
return instance
class DistConf(dict):
"""Distribution configuration for agents."""
def __init__(
self,
host: str = "localhost",
port: int = None,
max_pool_size: int = 8192,
max_timeout_seconds: int = 1800,
local_mode: bool = True,
lazy_launch: bool = True,
):
"""Init the distributed configuration.
Args:
host (`str`, defaults to `"localhost"`):
Hostname of the rpc agent server.
port (`int`, defaults to `None`):
Port of the rpc agent server.
max_pool_size (`int`, defaults to `8192`):
Max number of task results that the server can accommodate.
max_timeout_seconds (`int`, defaults to `1800`):
Timeout for task results.
local_mode (`bool`, defaults to `True`):
Whether the started rpc server only listens to local
requests.
lazy_launch (`bool`, defaults to `True`):
Only launch the server when the agent is called.
"""
self["host"] = host
self["port"] = port
self["max_pool_size"] = max_pool_size
self["max_timeout_seconds"] = max_timeout_seconds
self["local_mode"] = local_mode
self["lazy_launch"] = lazy_launch
class AgentBase(Operator, metaclass=_AgentMeta):
"""Base class for all agents.
All agents should inherit from this class and implement the `reply`
function.
"""
_version: int = 1
def __init__(
self,
name: str,
sys_prompt: Optional[str] = None,
model_config_name: str = None,
use_memory: bool = True,
memory_config: Optional[dict] = None,
to_dist: Optional[Union[DistConf, bool]] = False,
) -> None:
r"""Initialize an agent from the given arguments.
Args:
name (`str`):
The name of the agent.
sys_prompt (`Optional[str]`):
The system prompt of the agent, which can be passed by args
or hard-coded in the agent.
model_config_name (`str`, defaults to None):
The name of the model config, which is used to load model from
configuration.
use_memory (`bool`, defaults to `True`):
Whether the agent has memory.
memory_config (`Optional[dict]`):
The config of memory.
to_dist (`Optional[Union[DistConf, bool]]`, default to `False`):
The configurations passed to :py:meth:`to_dist` method. Used in
:py:class:`_AgentMeta`, when this parameter is provided,
the agent will automatically be converted into its distributed
version. Below are some examples:
.. code-block:: python
# run as a sub process
agent = XXXAgent(
# ... other parameters
to_dist=True,
)
# connect to an existing agent server
agent = XXXAgent(
# ... other parameters
to_dist=DistConf(
host="<ip of your server>",
port=<port of your server>,
# other parameters
),
)
See :doc:`Tutorial<tutorial/208-distribute>` for detail.
"""
self.name = name
self.memory_config = memory_config
if sys_prompt is not None:
self.sys_prompt = sys_prompt
# TODO: support to receive a ModelWrapper instance
if model_config_name is not None:
self.model = load_model_by_config_name(model_config_name)
if use_memory:
self.memory = TemporaryMemory(memory_config)
else:
self.memory = None
# The global unique id of this agent
self._agent_id = self.__class__.generate_agent_id()
# The audience of this agent, which means if this agent generates a
# response, it will be passed to all agents in the audience.
self._audience = None
# convert to distributed agent, conversion is in `_AgentMeta`
if to_dist is not False and to_dist is not None:
logger.info(
f"Convert {self.__class__.__name__}[{self.name}] into"
" a distributed agent.",
)
@classmethod
def generate_agent_id(cls) -> str:
"""Generate the agent_id of this agent instance"""
# TODO: change cls.__name__ into a global unique agent_type
return f"{cls.__name__}_{uuid.uuid4().hex}"
# todo: add a unique agent_type field to distinguish different agent class
@classmethod
def get_agent_class(cls, agent_class_name: str) -> Type[AgentBase]:
"""Get the agent class based on the specific agent class name.
Args:
agent_class_name (`str`): the name of the agent class.
Raises:
ValueError: Agent class name not exits.
Returns:
Type[AgentBase]: the AgentBase sub-class.
"""
if agent_class_name not in cls._registry:
raise ValueError(f"Agent [{agent_class_name}] not found.")
return cls._registry[agent_class_name] # type: ignore[return-value]
@classmethod
def register_agent_class(cls, agent_class: Type[AgentBase]) -> None:
"""Register the agent class into the registry.
Args:
agent_class (Type[AgentBase]): the agent class to be registered.
"""
agent_class_name = agent_class.__name__
if agent_class_name in cls._registry:
logger.info(
f"Agent class with name [{agent_class_name}] already exists.",
)
else:
cls._registry[agent_class_name] = agent_class
def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None, user_backboard:str = "") -> Msg:
"""Define the actions taken by this agent.
Args:
x (`Optional[Union[Msg, Sequence[Msg]]]`, defaults to `None`):
The input message(s) to the agent, which also can be omitted if
the agent doesn't need any input.
user_backboard (`str`): The user backboard, including user's IDE files in brief.
Returns:
`Msg`: The output message generated by the agent.
Note:
Given that some agents are in an adversarial environment,
their input doesn't include the thoughts of other agents.
"""
raise NotImplementedError(
f"Agent [{type(self).__name__}] is missing the required "
f'"reply" function.',
)
def load_from_config(self, config: dict) -> None:
"""Load configuration for this agent.
Args:
config (`dict`): model configuration
"""
def export_config(self) -> dict:
"""Return configuration of this agent.
Returns:
The configuration of current agent.
"""
return {}
def load_memory(self, memory: Sequence[dict]) -> None:
r"""Load input memory."""
def __call__(self, *args: Any, **kwargs: Any) -> dict:
"""Calling the reply function, and broadcast the generated
response to all audiences if needed."""
res = self.reply(*args, **kwargs)
# broadcast to audiences if needed
if self._audience is not None:
self._broadcast_to_audience(res)
return res
def speak(
self,
content: Union[str, Msg],
) -> None:
"""
Speak out the message generated by the agent. If a string is given,
a Msg object will be created with the string as the content.
Args:
content (`Union[str, Msg]`):
The content of the message to be spoken out. If a string is
given, a Msg object will be created with the agent's name, role
as "assistant", and the given string as the content.
"""
if isinstance(content, str):
msg = Msg(
name=self.name,
content=content,
role="assistant",
)
elif isinstance(content, Msg):
msg = content
else:
raise TypeError(
"From version 0.0.5, the speak method only accepts str or Msg "
f"object, got {type(content)} instead.",
)
logger.chat(msg)
def observe(self, x: Union[dict, Sequence[dict]]) -> None:
"""Observe the input, store it in memory without response to it.
Args:
x (`Union[dict, Sequence[dict]]`):
The input message to be recorded in memory.
"""
if self.memory:
self.memory.add(x)
def reset_audience(self, audience: Sequence[AgentBase]) -> None:
"""Set the audience of this agent, which means if this agent
generates a response, it will be passed to all audiences.
Args:
audience (`Sequence[AgentBase]`):
The audience of this agent, which will be notified when this
agent generates a response message.
"""
# TODO: we leave the consideration of nested msghub for future.
# for now we suppose one agent can only be in one msghub
self._audience = [_ for _ in audience if _ != self]
def clear_audience(self) -> None:
"""Remove the audience of this agent."""
# TODO: we leave the consideration of nested msghub for future.
# for now we suppose one agent can only be in one msghub
self._audience = None
def rm_audience(
self,
audience: Union[Sequence[AgentBase], AgentBase],
) -> None:
"""Remove the given audience from the Sequence"""
if not isinstance(audience, Sequence):
audience = [audience]
for agent in audience:
if self._audience is not None and agent in self._audience:
self._audience.pop(self._audience.index(agent))
else:
logger.warning(
f"Skip removing agent [{agent.name}] from the "
f"audience for its inexistence.",
)
def _broadcast_to_audience(self, x: dict) -> None:
"""Broadcast the input to all audiences."""
for agent in self._audience:
agent.observe(x)
@property
def agent_id(self) -> str:
"""The unique id of this agent.
Returns:
str: agent_id
"""
return self._agent_id
def to_dist(
self,
host: str = "localhost",
port: int = None,
max_pool_size: int = 8192,
max_timeout_seconds: int = 1800,
local_mode: bool = True,
lazy_launch: bool = True,
launch_server: bool = None,
) -> AgentBase:
"""Convert current agent instance into a distributed version.
Args:
host (`str`, defaults to `"localhost"`):
Hostname of the rpc agent server.
port (`int`, defaults to `None`):
Port of the rpc agent server.
max_pool_size (`int`, defaults to `8192`):
Only takes effect when `host` and `port` are not filled in.
The max number of agent reply messages that the started agent
server can accommodate. Note that the oldest message will be
deleted after exceeding the pool size.
max_timeout_seconds (`int`, defaults to `1800`):
Only takes effect when `host` and `port` are not filled in.
Maximum time for reply messages to be cached in the launched
agent server. Note that expired messages will be deleted.
local_mode (`bool`, defaults to `True`):
Only takes effect when `host` and `port` are not filled in.
Whether the started agent server only listens to local
requests.
lazy_launch (`bool`, defaults to `True`):
Only takes effect when `host` and `port` are not filled in.
If `True`, launch the agent server when the agent is called,
otherwise, launch the agent server immediately.
launch_server(`bool`, defaults to `None`):
This field has been deprecated and will be removed in
future releases.
Returns:
`AgentBase`: the wrapped agent instance with distributed
functionality
"""
from .rpc_agent import RpcAgent
if issubclass(self.__class__, RpcAgent):
return self
if launch_server is not None:
logger.warning(
"`launch_server` has been deprecated and will be removed in "
"future releases. When `host` and `port` is not provided, the "
"agent server will be launched automatically.",
)
return RpcAgent(
name=self.name,
agent_class=self.__class__,
agent_configs=self._init_settings,
host=host,
port=port,
max_pool_size=max_pool_size,
max_timeout_seconds=max_timeout_seconds,
local_mode=local_mode,
lazy_launch=lazy_launch,
agent_id=self.agent_id,
)

View File

@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
"""A general dialog agent."""
from typing import Optional, Union, Sequence
from ..message import Msg
from .agent import AgentBase
class DialogAgent(AgentBase):
"""A simple agent used to perform a dialogue. Your can set its role by
`sys_prompt`."""
def __init__(
self,
name: str,
sys_prompt: str,
model_config_name: str,
use_memory: bool = True,
memory_config: Optional[dict] = None,
) -> None:
"""Initialize the dialog agent.
Arguments:
name (`str`):
The name of the agent.
sys_prompt (`Optional[str]`):
The system prompt of the agent, which can be passed by args
or hard-coded in the agent.
model_config_name (`str`):
The name of the model config, which is used to load model from
configuration.
use_memory (`bool`, defaults to `True`):
Whether the agent has memory.
memory_config (`Optional[dict]`):
The config of memory.
"""
super().__init__(
name=name,
sys_prompt=sys_prompt,
model_config_name=model_config_name,
use_memory=use_memory,
memory_config=memory_config,
)
def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None) -> Msg:
"""Reply function of the agent. Processes the input data,
generates a prompt using the current dialogue memory and system
prompt, and invokes the language model to produce a response. The
response is then formatted and added to the dialogue memory.
Args:
x (`Optional[Union[Msg, Sequence[Msg]]]`, defaults to `None`):
The input message(s) to the agent, which also can be omitted if
the agent doesn't need any input.
Returns:
`Msg`: The output message generated by the agent.
"""
# record the input if needed
if self.memory:
self.memory.add(x)
# prepare prompt
prompt = self.model.format(
Msg("system", self.sys_prompt, role="system"),
self.memory
and self.memory.get_memory()
or x, # type: ignore[arg-type]
)
# call llm and generate response
response = self.model(prompt).text
msg = Msg(self.name, response, role="assistant")
# Print/speak the message in this agent's voice
self.speak(msg)
# Record the message in memory
if self.memory:
self.memory.add(msg)
return msg

View File

@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
"""An agent that replies in a dictionary format."""
from typing import Optional, Union, Sequence
from ..message import Msg
from .agent import AgentBase
from ..parsers import ParserBase
class DictDialogAgent(AgentBase):
"""An agent that generates response in a dict format, where user can
specify the required fields in the response via specifying the parser
About parser, please refer to our
[tutorial](https://modelscope.github.io/agentscope/en/tutorial/203-parser.html)
For usage example, please refer to the example of werewolf in
`examples/game_werewolf`"""
def __init__(
self,
name: str,
sys_prompt: str,
model_config_name: str,
use_memory: bool = True,
memory_config: Optional[dict] = None,
max_retries: Optional[int] = 3,
) -> None:
"""Initialize the dict dialog agent.
Arguments:
name (`str`):
The name of the agent.
sys_prompt (`Optional[str]`, defaults to `None`):
The system prompt of the agent, which can be passed by args
or hard-coded in the agent.
model_config_name (`str`, defaults to None):
The name of the model config, which is used to load model from
configuration.
use_memory (`bool`, defaults to `True`):
Whether the agent has memory.
memory_config (`Optional[dict]`, defaults to `None`):
The config of memory.
max_retries (`Optional[int]`, defaults to `None`):
The maximum number of retries when failed to parse the model
output.
""" # noqa
super().__init__(
name=name,
sys_prompt=sys_prompt,
model_config_name=model_config_name,
use_memory=use_memory,
memory_config=memory_config,
)
self.parser = None
self.max_retries = max_retries
def set_parser(self, parser: ParserBase) -> None:
"""Set response parser, which will provide 1) format instruction; 2)
response parsing; 3) filtering fields when returning message, storing
message in memory. So developers only need to change the
parser, and the agent will work as expected.
"""
self.parser = parser
def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None) -> Msg:
"""Reply function of the agent.
Processes the input data, generates a prompt using the current
dialogue memory and system prompt, and invokes the language
model to produce a response. The response is then formatted
and added to the dialogue memory.
Args:
x (`Optional[Union[Msg, Sequence[Msg]]]`, defaults to `None`):
The input message(s) to the agent, which also can be omitted if
the agent doesn't need any input.
Returns:
`Msg`: The output message generated by the agent.
Raises:
`json.decoder.JSONDecodeError`:
If the response from the language model is not valid JSON,
it defaults to treating the response as plain text.
"""
# record the input if needed
if self.memory:
self.memory.add(x)
# prepare prompt
prompt = self.model.format(
Msg("system", self.sys_prompt, role="system"),
self.memory
and self.memory.get_memory()
or x, # type: ignore[arg-type]
Msg("system", self.parser.format_instruction, "system"),
)
# call llm
res = self.model(
prompt,
parse_func=self.parser.parse,
max_retries=self.max_retries,
)
# Filter the parsed response by keys for storing in memory, returning
# in the reply function, and feeding into the metadata field in the
# returned message object.
self.memory.add(
Msg(self.name, self.parser.to_memory(res.parsed), "assistant"),
)
msg = Msg(
self.name,
content=self.parser.to_content(res.parsed),
role="assistant",
metadata=self.parser.to_metadata(res.parsed),
)
self.speak(msg)
return msg

View File

@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
"""A common base class for AgentBase and PipelineBase"""
from abc import ABC
from abc import abstractmethod
from typing import Any
class Operator(ABC):
"""
Abstract base class `Operator` defines a protocol for classes that
implement callable behavior.
The class is designed to be subclassed with an overridden `__call__`
method that specifies the execution logic for the operator.
"""
@abstractmethod
def __call__(self, *args: Any, **kwargs: Any) -> dict:
"""Calling function"""

View File

@@ -0,0 +1,194 @@
# -*- coding: utf-8 -*-
"""
This example shows how to build an agent with RAG
with LlamaIndex.
Notice, this is a Beta version of RAG agent.
"""
from typing import Any, Optional, Union, Sequence
from loguru import logger
from agentscope.agents.agent import AgentBase
from agentscope.message import Msg
from agentscope.rag import Knowledge
CHECKING_PROMPT = """
Is the retrieved content relevant to the query?
Retrieved content: {}
Query: {}
Only answer YES or NO.
"""
class LlamaIndexAgent(AgentBase):
"""
A LlamaIndex agent build on LlamaIndex.
"""
def __init__(
self,
name: str,
sys_prompt: str,
model_config_name: str,
knowledge_list: list[Knowledge] = None,
knowledge_id_list: list[str] = None,
similarity_top_k: int = None,
log_retrieval: bool = True,
recent_n_mem_for_retrieve: int = 1,
**kwargs: Any,
) -> None:
"""
Initialize the RAG LlamaIndexAgent
Args:
name (str):
the name for the agent
sys_prompt (str):
system prompt for the RAG agent
model_config_name (str):
language model for the agent
knowledge_list (list[Knowledge]):
a list of knowledge.
User can choose to pass a list knowledge object
directly when initializing the RAG agent. Another
choice can be passing a list of knowledge ids and
obtain the knowledge with the `equip` function of a
knowledge bank.
knowledge_id_list (list[Knowledge]):
a list of id of the knowledge.
This is designed for easy setting up multiple RAG
agents with a config file. To obtain the knowledge
objects, users can pass this agent to the `equip`
function in a knowledge bank to add corresponding
knowledge to agent's self.knowledge_list.
similarity_top_k (int):
the number of most similar data blocks retrieved
from each of the knowledge
log_retrieval (bool):
whether to print the retrieved content
recent_n_mem_for_retrieve (int):
the number of pieces of memory used as part of
retrival query
"""
super().__init__(
name=name,
sys_prompt=sys_prompt,
model_config_name=model_config_name,
)
self.knowledge_list = knowledge_list or []
self.knowledge_id_list = knowledge_id_list or []
self.similarity_top_k = similarity_top_k
self.log_retrieval = log_retrieval
self.recent_n_mem_for_retrieve = recent_n_mem_for_retrieve
self.description = kwargs.get("description", "")
def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None) -> Msg:
"""
Reply function of the RAG agent.
Processes the input data,
1) use the input data to retrieve with RAG function;
2) generates a prompt using the current memory and system
prompt;
3) invokes the language model to produce a response. The
response is then formatted and added to the dialogue memory.
Args:
x (`Optional[Union[Msg, Sequence[Msg]]]`, defaults to `None`):
The input message(s) to the agent, which also can be omitted if
the agent doesn't need any input.
Returns:
`Msg`: The output message generated by the agent.
"""
retrieved_docs_to_string = ""
# record the input if needed
if self.memory:
self.memory.add(x)
# in case no input is provided (e.g., in msghub),
# use the memory as query
history = self.memory.get_memory(
recent_n=self.recent_n_mem_for_retrieve,
)
query = (
"/n".join(
[msg["content"] for msg in history],
)
if isinstance(history, list)
else str(history)
)
elif x is not None:
query = x.content
else:
query = ""
if len(query) > 0:
# when content has information, do retrieval
scores = []
for knowledge in self.knowledge_list:
retrieved_nodes = knowledge.retrieve(
str(query),
self.similarity_top_k,
)
for node in retrieved_nodes:
scores.append(node.score)
retrieved_docs_to_string += (
"\n>>>> score:"
+ str(node.score)
+ "\n>>>> source:"
+ str(node.node.get_metadata_str())
+ "\n>>>> content:"
+ node.get_content()
)
if self.log_retrieval:
self.speak("[retrieved]:" + retrieved_docs_to_string)
if max(scores) < 0.4:
# if the max score is lower than 0.4, then we let LLM
# decide whether the retrieved content is relevant
# to the user input.
msg = Msg(
name="user",
role="user",
content=CHECKING_PROMPT.format(
retrieved_docs_to_string,
query,
),
)
msg = self.model.format(msg)
checking = self.model(msg)
logger.info(checking)
checking = checking.text.lower()
if "no" in checking:
retrieved_docs_to_string = "EMPTY"
# prepare prompt
prompt = self.model.format(
Msg(
name="system",
role="system",
content=self.sys_prompt,
),
# {"role": "system", "content": retrieved_docs_to_string},
self.memory.get_memory(
recent_n=self.recent_n_mem_for_retrieve,
),
Msg(
name="user",
role="user",
content="Context: " + retrieved_docs_to_string,
),
)
# call llm and generate response
response = self.model(prompt).text
msg = Msg(self.name, response)
# Print/speak the message in this agent's voice
self.speak(msg)
if self.memory:
# Record the message in memory
self.memory.add(msg)
return msg

View File

@@ -0,0 +1,316 @@
# -*- coding: utf-8 -*-
"""An agent class that implements the ReAct algorithm. The agent will reason
and act iteratively to solve problems. More details can be found in the paper
https://arxiv.org/abs/2210.03629.
"""
from typing import Any, Optional, Union, Sequence
from loguru import logger
from agentscope.exception import ResponseParsingError, FunctionCallError
from agentscope.agents import AgentBase
from agentscope.message import Msg
from agentscope.parsers import MarkdownJsonDictParser
from agentscope.service import ServiceToolkit
from agentscope.service.service_toolkit import ServiceFunction
INSTRUCTION_PROMPT = """## Your Role:
You are a powerful AI agent and specifically aimed to use C# and Unity to create new Game Contents, which in the game project are named as 'Gurouce'.
Gurouce is refer to anything that can be USED to make some influence in game, and your main job is follow user's instruction to create new game contents,
This game is a 2D game in Unity with C# scripts.
## What You Should Do:
0. Check user's instruction, make sure you know where the game project, and user's instruction is rational and can be realize. Otherwize, just finish.
1. To realize a new Gurouce, you should first analyze the user's task, to answer three main questions:
A. How the Gurouce find the target to influence?
B. At what situation the Gurouce will start to influence?
C. How the Gurouce influence the target?
2. Then, open and read the source file, then inherit from them and override their functions to realize the three main questions:
A. BaseFindTarget.cs
B. BaseStartCondition.cs
C. BaseInfluence.cs
You should open and read the example Gurouce to learn how to use. The example Gurouce is RockGurouce, when using it, it will create a "Rock" Gurouce and fly from the game character towards where the mouse pointer is, and cause damage to the target it hits, then destory itself.
3. Since you have to write some source files, make sure each file implement their job functionality, and all three questions above are anwsered well.
3. Next, Call "compile" function, it will compile the source files and update game contents.
4. If failed to compile, follow the error message and edit again, otherwise finish.
## Note:
1. Fully understand the tool functions and their arguments before using them.
2. Check the folder structure and relevant scripts before write any code.
3. Your EVERY response MUST have at least one function call, and the function call must be in the "function" field.
4. Make sure the types and values of the arguments you provided to the tool functions are correct.
5. Don't take things for granted. For example, where you are, what's the time now, etc. You can try to use the tool functions to get information.
6. If the function execution fails, you should analyze the error and try to solve it.
7. You don't have to finish the whole task quickly, we will ask your several times, so follow the "What You Should Do" and where we have done, to respond once a step.
## Resources:
1. The tool functions you can use.
2. A Specific Unity game project.
## Constraint:
1. You should not use any other tool functions.
2. There is a folder path 'Assets/HotUpdate/Gurouce/', you must create a subfolder in it, and you can only edit within the subfolder. But you can read any other file if need. BTW, the base scripts are in 'BaseScripts' subfolder and RockGurouce is in 'RockGurouce' subfolder.
3. You have to decide a Gurouce name, and you have to change "Base" to the Gurouce name you decided when you write the source files' names.
""" # noqa
class ReActAgent(AgentBase):
"""An agent class that implements the ReAct algorithm. More details refer
to https://arxiv.org/abs/2210.03629.
Note this is an example implementation of ReAct algorithm in AgentScope.
We follow the idea within the paper, but the detailed prompt engineering
maybe different. Developers are encouraged to modify the prompt to fit
their own needs.
"""
def __init__(
self,
name: str,
model_config_name: str,
service_toolkit: ServiceToolkit = None,
sys_prompt: str = "You're a helpful assistant. Your name is {name}.",
max_iters: int = 10,
verbose: bool = True,
**kwargs: Any,
) -> None:
"""Initialize the ReAct agent with the given name, model config name
and tools.
Args:
name (`str`):
The name of the agent.
sys_prompt (`str`):
The system prompt of the agent.
model_config_name (`str`):
The name of the model config, which is used to load model from
configuration.
service_toolkit (`ServiceToolkit`):
A `ServiceToolkit` object that contains the tool functions.
max_iters (`int`, defaults to `10`):
The maximum number of iterations of the reasoning-acting loops.
verbose (`bool`, defaults to `True`):
Whether to print the detailed information during reasoning and
acting steps. If `False`, only the content in speak field will
be print out.
"""
super().__init__(
name=name,
sys_prompt=sys_prompt,
model_config_name=model_config_name,
)
# TODO: To compatible with the old version, which will be deprecated
# soon
if "tools" in kwargs:
logger.warning(
"The argument `tools` will be deprecated soon. "
"Please use `service_toolkit` instead. Example refers to "
"https://github.com/modelscope/agentscope/blob/main/"
"examples/conversation_with_react_agent/code/"
"conversation_with_react_agent.py",
)
service_funcs = {}
for func, json_schema in kwargs["tools"]:
name = json_schema["function"]["name"]
service_funcs[name] = ServiceFunction(
name=name,
original_func=func,
processed_func=func,
json_schema=json_schema,
)
if service_toolkit is None:
service_toolkit = ServiceToolkit()
service_toolkit.service_funcs = service_funcs
else:
service_toolkit.service_funcs.update(service_funcs)
elif service_toolkit is None:
raise ValueError(
"The argument `service_toolkit` is required to initialize "
"the ReActAgent.",
)
self.service_toolkit = service_toolkit
self.verbose = verbose
self.max_iters = max_iters
if not sys_prompt.endswith("\n"):
sys_prompt = sys_prompt + "\n"
self.sys_prompt = "\n".join(
[
# The brief intro of the role and target
sys_prompt.format(name=self.name),
# The instruction prompt for tools
self.service_toolkit.tools_instruction,
# The detailed instruction prompt for the agent
INSTRUCTION_PROMPT,
],
)
# Put sys prompt into memory
self.memory.add(Msg("system", self.sys_prompt, role="system"))
# Initialize a parser object to formulate the response from the model
self.parser = MarkdownJsonDictParser(
content_hint={
"thought": "what you thought",
"speak": "what you speak",
"function": service_toolkit.tools_calling_format,
},
required_keys=["thought", "speak", "function"],
# Only print the speak field when verbose is False
keys_to_content=True if self.verbose else "speak",
)
def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None) -> Msg:
"""The reply function that achieves the ReAct algorithm.
The more details please refer to https://arxiv.org/abs/2210.03629"""
self.memory.add(x)
for _ in range(self.max_iters):
# Step 1: Thought
if self.verbose:
self.speak(f" ITER {_+1}, STEP 1: REASONING ".center(70, "#"))
# Prepare hint to remind model what the response format is
# Won't be recorded in memory to save tokens
hint_msg = Msg(
"system",
self.parser.format_instruction,
role="system",
echo=self.verbose,
)
# Prepare prompt for the model
prompt = self.model.format(self.memory.get_memory(), hint_msg)
print('====================================Prompt==========================================')
print(prompt)
# Generate and parse the response
try:
res = self.model(
prompt,
parse_func=self.parser.parse,
max_retries=1,
)
print('====================================Result Return==========================================')
print(res)
print('====================================Result Parsed==========================================')
print(res.parsed)
# Record the response in memory
self.memory.add(
Msg(
self.name,
self.parser.to_memory(res.parsed),
"assistant",
),
)
# Print out the response
msg_returned = Msg(
self.name,
self.parser.to_content(res.parsed),
"assistant",
)
print('====================================Speak==========================================')
print(msg_returned)
self.speak(msg_returned)
# Skip the next steps if no need to call tools
# The parsed field is a dictionary
print('====================================Function==========================================')
print(res.parsed["function"])
arg_function = res.parsed["function"]
if (
isinstance(arg_function, str)
and arg_function in ["[]", ""]
or isinstance(arg_function, list)
and len(arg_function) == 0
):
# Only the speak field is exposed to users or other agents
return msg_returned
# Only catch the response parsing error and expose runtime
# errors to developers for debugging
except ResponseParsingError as e:
# Print out raw response from models for developers to debug
response_msg = Msg(self.name, e.raw_response, "assistant")
self.speak(response_msg)
# Re-correct by model itself
error_msg = Msg("system", str(e), "system")
self.speak(error_msg)
self.memory.add([response_msg, error_msg])
# Skip acting step to re-correct the response
continue
# Step 2: Acting
if self.verbose:
self.speak(f" ITER {_+1}, STEP 2: ACTING ".center(70, "#"))
# Parse, check and execute the tool functions in service toolkit
try:
if res.parsed["function"] == "finish":
hint_msg = Msg(
"system",
"You have Finished the task."
"Now generate a reply by summarizing the current "
"situation.",
role="system",
echo=self.verbose,
)
# Generate a reply by summarizing the current situation
prompt = self.model.format(self.memory.get_memory(), hint_msg)
res = self.model(prompt)
res_msg = Msg(self.name, res.text, "assistant")
self.speak(res_msg)
return res_msg
execute_results = self.service_toolkit.parse_and_call_func(
res.parsed["function"],
)
# Note: Observing the execution results and generate response
# are finished in the next reasoning step. We just put the
# execution results into memory, and wait for the next loop
# to generate response.
# Record execution results into memory as system message
msg_res = Msg("system", execute_results, "system")
self.speak(msg_res)
self.memory.add(msg_res)
except FunctionCallError as e:
# Catch the function calling error that can be handled by
# the model
error_msg = Msg("system", str(e), "system")
self.speak(error_msg)
self.memory.add(error_msg)
# Exceed the maximum iterations
hint_msg = Msg(
"system",
"You have failed to generate a response in the maximum "
"iterations. Now generate a reply by summarizing the current "
"situation.",
role="system",
echo=self.verbose,
)
# Generate a reply by summarizing the current situation
prompt = self.model.format(self.memory.get_memory(), hint_msg)
res = self.model(prompt)
res_msg = Msg(self.name, res.text, "assistant")
self.speak(res_msg)
return res_msg

View File

@@ -0,0 +1,178 @@
# -*- coding: utf-8 -*-
""" Base class for Rpc Agent """
from typing import Type, Optional, Union, Sequence
from agentscope.agents.agent import AgentBase
from agentscope.message import (
PlaceholderMessage,
serialize,
Msg,
)
from agentscope.rpc import RpcAgentClient
from agentscope.server.launcher import RpcAgentServerLauncher
from agentscope.studio._client import _studio_client
class RpcAgent(AgentBase):
"""A wrapper to extend an AgentBase into a gRPC Client."""
def __init__(
self,
name: str,
host: str = "localhost",
port: int = None,
agent_class: Type[AgentBase] = None,
agent_configs: Optional[dict] = None,
max_pool_size: int = 8192,
max_timeout_seconds: int = 1800,
local_mode: bool = True,
lazy_launch: bool = True,
agent_id: str = None,
connect_existing: bool = False,
) -> None:
"""Initialize a RpcAgent instance.
Args:
name (`str`): the name of the agent.
host (`str`, defaults to `localhost`):
Hostname of the rpc agent server.
port (`int`, defaults to `None`):
Port of the rpc agent server.
agent_class (`Type[AgentBase]`):
the AgentBase subclass of the source agent.
agent_configs (`dict`): The args used to
initialize the agent, generated by `_AgentMeta`.
max_pool_size (`int`, defaults to `8192`):
Max number of task results that the server can accommodate.
max_timeout_seconds (`int`, defaults to `1800`):
Timeout for task results.
local_mode (`bool`, defaults to `True`):
Whether the started gRPC server only listens to local
requests.
lazy_launch (`bool`, defaults to `True`):
Only launch the server when the agent is called.
agent_id (`str`, defaults to `None`):
The agent id of this instance. If `None`, it will
be generated randomly.
connect_existing (`bool`, defaults to `False`):
Set to `True`, if the agent is already running on the agent
server.
"""
super().__init__(name=name)
self.agent_class = agent_class
self.agent_configs = agent_configs
self.host = host
self.port = port
self.server_launcher = None
self.client = None
self.connect_existing = connect_existing
if agent_id is not None:
self._agent_id = agent_id
# if host and port are not provided, launch server locally
launch_server = port is None
if launch_server:
self.host = "localhost"
studio_url = None
if _studio_client.active:
studio_url = _studio_client.studio_url
self.server_launcher = RpcAgentServerLauncher(
host=self.host,
port=port,
max_pool_size=max_pool_size,
max_timeout_seconds=max_timeout_seconds,
local_mode=local_mode,
custom_agents=[agent_class],
studio_url=studio_url,
)
if not lazy_launch:
self._launch_server()
else:
self.client = RpcAgentClient(
host=self.host,
port=self.port,
agent_id=self.agent_id,
)
if not self.connect_existing:
self.client.create_agent(agent_configs)
def _launch_server(self) -> None:
"""Launch a rpc server and update the port and the client"""
self.server_launcher.launch()
self.port = self.server_launcher.port
self.client = RpcAgentClient(
host=self.host,
port=self.port,
agent_id=self.agent_id,
)
self.client.create_agent(self.agent_configs)
def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None) -> Msg:
if self.client is None:
self._launch_server()
return PlaceholderMessage(
name=self.name,
content=None,
client=self.client,
x=x,
)
def observe(self, x: Union[dict, Sequence[dict]]) -> None:
if self.client is None:
self._launch_server()
self.client.call_func(
func_name="_observe",
value=serialize(x), # type: ignore[arg-type]
)
def clone_instances(
self,
num_instances: int,
including_self: bool = True,
) -> Sequence[AgentBase]:
"""
Clone a series of this instance with different agent_id and
return them as a list.
Args:
num_instances (`int`): The number of instances in the returned
list.
including_self (`bool`): Whether to include the instance calling
this method in the returned list.
Returns:
`Sequence[AgentBase]`: A list of agent instances.
"""
generated_instance_number = (
num_instances - 1 if including_self else num_instances
)
generated_instances = []
# launch the server before clone instances
if self.client is None:
self._launch_server()
# put itself as the first element of the returned list
if including_self:
generated_instances.append(self)
# clone instances without agent server
for _ in range(generated_instance_number):
new_agent_id = self.client.call_func("_clone_agent")
generated_instances.append(
RpcAgent(
name=self.name,
host=self.host,
port=self.port,
agent_id=new_agent_id,
connect_existing=True,
),
)
return generated_instances
def stop(self) -> None:
"""Stop the RpcAgent and the rpc server."""
if self.server_launcher is not None:
self.server_launcher.shutdown()
def __del__(self) -> None:
self.stop()

View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
"""An agent that convert text to image."""
from typing import Optional, Union, Sequence
from loguru import logger
from .agent import AgentBase
from ..message import Msg
class TextToImageAgent(AgentBase):
"""
A agent used to perform text to image tasks.
TODO: change the agent into a service.
"""
def __init__(
self,
name: str,
model_config_name: str,
use_memory: bool = True,
memory_config: Optional[dict] = None,
) -> None:
"""Initialize the text to image agent.
Arguments:
name (`str`):
The name of the agent.
model_config_name (`str`, defaults to None):
The name of the model config, which is used to load model from
configuration.
use_memory (`bool`, defaults to `True`):
Whether the agent has memory.
memory_config (`Optional[dict]`):
The config of memory.
"""
super().__init__(
name=name,
sys_prompt="",
model_config_name=model_config_name,
use_memory=use_memory,
memory_config=memory_config,
)
logger.warning(
"The `TextToImageAgent` will be deprecated in v0.0.6, "
"please use `text_to_image` service and `ReActAgent` instead.",
)
def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None) -> Msg:
if self.memory:
self.memory.add(x)
if x is None:
# get the last message from memory
if self.memory and self.memory.size() > 0:
x = self.memory.get_memory()[-1]
else:
return Msg(
self.name,
content="Please provide a text prompt to generate image.",
role="assistant",
)
image_urls = self.model(x.content).image_urls
# TODO: optimize the construction of content
msg = Msg(
self.name,
content="This is the generated image",
role="assistant",
url=image_urls,
)
self.speak(msg)
if self.memory:
self.memory.add(msg)
return msg

View File

@@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
"""User Agent class"""
import time
from typing import Union, Sequence
from typing import Optional
from loguru import logger
from agentscope.agents import AgentBase
from agentscope.studio._client import _studio_client
from agentscope.message import Msg
from agentscope.web.gradio.utils import user_input
class UserAgent(AgentBase):
"""User agent class"""
def __init__(self, name: str = "User", require_url: bool = False) -> None:
"""Initialize a UserAgent object.
Arguments:
name (`str`, defaults to `"User"`):
The name of the agent. Defaults to "User".
require_url (`bool`, defaults to `False`):
Whether the agent requires user to input a URL. Defaults to
False. The URL can lead to a website, a file,
or a directory. It will be added into the generated message
in field `url`.
"""
super().__init__(name=name)
self.name = name
self.require_url = require_url
def reply(
self,
x: Optional[Union[Msg, Sequence[Msg]]] = None,
required_keys: Optional[Union[list[str], str]] = None,
timeout: Optional[int] = None,
) -> Msg:
"""
Processes the input provided by the user and stores it in memory,
potentially formatting it with additional provided details.
The method prompts the user for input, then optionally prompts for
additional specifics based on the provided format keys. All
information is encapsulated in a message object, which is then
added to the object's memory.
Arguments:
x (`Optional[Union[Msg, Sequence[Msg]]]`, defaults to `None`):
The input message(s) to the agent, which also can be omitted if
the agent doesn't need any input.
required_keys \
(`Optional[Union[list[str], str]]`, defaults to `None`):
Strings that requires user to input, which will be used as
the key of the returned dict. Defaults to None.
timeout (`Optional[int]`, defaults to `None`):
Raise `TimeoutError` if user exceed input time, set to None
for no limit.
Returns:
`Msg`: The output message generated by the agent.
"""
if self.memory:
self.memory.add(x)
if _studio_client.active:
logger.info(
f"Waiting for input from:\n\n"
f" * {_studio_client.get_run_detail_page_url()}\n",
)
raw_input = _studio_client.get_user_input(
agent_id=self.agent_id,
name=self.name,
require_url=self.require_url,
required_keys=required_keys,
)
print("Python: receive ", raw_input)
content = raw_input["content"]
url = raw_input["url"]
kwargs = {}
else:
# TODO: To avoid order confusion, because `input` print much
# quicker than logger.chat
time.sleep(0.5)
content = user_input(timeout=timeout)
kwargs = {}
if required_keys is not None:
if isinstance(required_keys, str):
required_keys = [required_keys]
for key in required_keys:
kwargs[key] = input(f"{key}: ")
# Input url of file, image, video, audio or website
url = None
if self.require_url:
url = input("URL (or Enter to skip): ")
if url == "":
url = None
# Add additional keys
msg = Msg(
name=self.name,
role="user",
content=content,
url=url,
**kwargs, # type: ignore[arg-type]
)
self.speak(msg)
# Add to memory
if self.memory:
self.memory.add(msg)
return msg
def speak(
self,
content: Union[str, Msg],
) -> None:
"""
Speak out the message generated by the agent. If a string is given,
a Msg object will be created with the string as the content.
Args:
content (`Union[str, Msg]`):
The content of the message to be spoken out. If a string is
given, a Msg object will be created with the agent's name, role
as "user", and the given string as the content.
"""
if isinstance(content, str):
msg = Msg(
name=self.name,
content=content,
role="assistant",
)
_studio_client.push_message(msg)
elif isinstance(content, Msg):
msg = content
else:
raise TypeError(
"From version 0.0.5, the speak method only accepts str or Msg "
f"object, got {type(content)} instead.",
)
logger.chat(msg, disable_gradio=True)

View File

@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
""" Some constants used in the project"""
from numbers import Number
from enum import IntEnum
PACKAGE_NAME = "agentscope"
MSG_TOKEN = f"[{PACKAGE_NAME}_msg]"
# default values
# for file manager
_DEFAULT_DIR = "./runs"
_DEFAULT_LOG_LEVEL = "INFO"
_DEFAULT_SUBDIR_CODE = "code"
_DEFAULT_SUBDIR_FILE = "file"
_DEFAULT_SUBDIR_INVOKE = "invoke"
_DEFAULT_CFG_NAME = ".config"
_DEFAULT_IMAGE_NAME = "image_{}_{}.png"
_DEFAULT_SQLITE_DB_PATH = "agentscope.db"
# for model wrapper
_DEFAULT_MAX_RETRIES = 3
_DEFAULT_MESSAGES_KEY = "inputs"
_DEFAULT_RETRY_INTERVAL = 1
_DEFAULT_API_BUDGET = None
# for execute python
_DEFAULT_PYPI_MIRROR = "http://mirrors.aliyun.com/pypi/simple/"
_DEFAULT_TRUSTED_HOST = "mirrors.aliyun.com"
# for monitor
_DEFAULT_MONITOR_TABLE_NAME = "monitor_metrics"
# for summarization
_DEFAULT_SUMMARIZATION_PROMPT = """
TEXT: {}
"""
_DEFAULT_SYSTEM_PROMPT = """
You are a helpful agent to summarize the text.
You need to keep all the key information of the text in the summary.
"""
_DEFAULT_TOKEN_LIMIT_PROMPT = """
Summarize the text after TEXT in less than {} tokens:
"""
# typing
Embedding = list[Number]
# enums
class ResponseFormat(IntEnum):
"""Enum for model response format."""
NONE = 0
JSON = 1
class ShrinkPolicy(IntEnum):
"""Enum for shrink strategies when the prompt is too long."""
TRUNCATE = 0
SUMMARIZE = 1
# rag related
DEFAULT_CHUNK_SIZE = 1024
DEFAULT_CHUNK_OVERLAP = 20
DEFAULT_TOP_K = 5

View File

@@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
"""AgentScope exception classes."""
# - Model Response Parsing Exceptions
class ResponseParsingError(Exception):
"""The exception class for response parsing error with uncertain
reasons."""
raw_response: str
"""Record the raw response."""
def __init__(self, message: str, raw_response: str = None) -> None:
"""Initialize the exception with the message."""
self.message = message
self.raw_response = raw_response
def __str__(self) -> str:
return f"{self.__class__.__name__}: {self.message}"
class JsonParsingError(ResponseParsingError):
"""The exception class for JSON parsing error."""
class JsonDictValidationError(ResponseParsingError):
"""The exception class for JSON dict validation error."""
class JsonTypeError(ResponseParsingError):
"""The exception class for JSON type error."""
class RequiredFieldNotFoundError(ResponseParsingError):
"""The exception class for missing required field in model response, when
the response is required to be a JSON dict object with required fields."""
class TagNotFoundError(ResponseParsingError):
"""The exception class for missing tagged content in model response."""
missing_begin_tag: bool
"""If the response misses the begin tag."""
missing_end_tag: bool
"""If the response misses the end tag."""
def __init__(
self,
message: str,
raw_response: str = None,
missing_begin_tag: bool = True,
missing_end_tag: bool = True,
):
"""Initialize the exception with the message.
Args:
raw_response (`str`):
Record the raw response from the model.
missing_begin_tag (`bool`, defaults to `True`):
If the response misses the beginning tag, default to `True`.
missing_end_tag (`bool`, defaults to `True`):
If the response misses the end tag, default to `True`.
"""
super().__init__(message, raw_response)
self.missing_begin_tag = missing_begin_tag
self.missing_end_tag = missing_end_tag
# - Function Calling Exceptions
class FunctionCallError(Exception):
"""The base class for exception raising during calling functions."""
def __init__(self, message: str) -> None:
self.message = message
def __str__(self) -> str:
return f"{self.__class__.__name__}: {self.message}"
class FunctionCallFormatError(FunctionCallError):
"""The exception class for function calling format error."""
class FunctionNotFoundError(FunctionCallError):
"""The exception class for function not found error."""
class ArgumentNotFoundError(FunctionCallError):
"""The exception class for missing argument error."""
class ArgumentTypeError(FunctionCallError):
"""The exception class for argument type error."""
class StudioError(Exception):
"""The base class for exception raising during interaction with agentscope
studio."""
def __init__(self, message: str) -> None:
self.message = message
def __str__(self) -> str:
return f"{self.__class__.__name__}: {self.message}"
class StudioRegisterError(StudioError):
"""The exception class for error when registering to agentscope studio."""

View File

@@ -0,0 +1,260 @@
# -*- coding: utf-8 -*-
"""Manage the file system for saving files, code and logs."""
import json
import os
from typing import Any, Union, Optional, List, Literal
from pathlib import Path
import numpy as np
from agentscope._runtime import _runtime
from agentscope.utils.tools import _download_file, _get_timestamp, _hash_string
from agentscope.utils.tools import _generate_random_code
from agentscope.constants import (
_DEFAULT_DIR,
_DEFAULT_SUBDIR_CODE,
_DEFAULT_SUBDIR_FILE,
_DEFAULT_SUBDIR_INVOKE,
_DEFAULT_SQLITE_DB_PATH,
_DEFAULT_IMAGE_NAME,
_DEFAULT_CFG_NAME,
)
def _get_text_embedding_record_hash(
text: str,
embedding_model: Optional[Union[str, dict]],
hash_method: Literal["sha256", "md5", "sha1"] = "sha256",
) -> str:
"""Get the hash of the text embedding record."""
original_data_hash = _hash_string(text, hash_method)
if isinstance(embedding_model, dict):
# Format the dict to avoid duplicate keys
embedding_model = json.dumps(embedding_model, sort_keys=True)
embedding_model_hash = _hash_string(embedding_model, hash_method)
# Calculate the embedding id by hashing the hash codes of the
# original data and the embedding model
record_hash = _hash_string(
original_data_hash + embedding_model_hash,
hash_method,
)
return record_hash
class _FileManager:
"""A singleton class for managing the file system for saving files,
code and logs."""
_instance = None
cache_dir: str = str(Path.home() / ".cache" / "agentscope")
hash_method: Literal["sha256", "md5", "sha1"] = "sha256"
dir: str = os.path.abspath(_DEFAULT_DIR)
"""The directory for saving files, code and logs."""
save_api_invoke: bool = False
"""Whether to save api invocation locally."""
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
"""Create a singleton instance."""
if not cls._instance:
cls._instance = super(_FileManager, cls).__new__(
cls,
*args,
**kwargs,
)
return cls._instance
def _get_and_create_subdir(self, subdir: str) -> str:
"""Get the path of the subdir and create the subdir if it does not
exist."""
path = os.path.join(self.dir, _runtime.runtime_id, subdir)
os.makedirs(path, exist_ok=True)
return path
def _get_file_path(self, file_name: str) -> str:
"""Get the path of the file."""
return os.path.join(self.dir, _runtime.runtime_id, file_name)
@property
def dir_cache(self) -> str:
"""The directory for saving cache files."""
return self.cache_dir
@property
def dir_cache_embedding(self) -> str:
"""Obtain the embedding cache directory."""
dir_cache_embedding = os.path.join(self.cache_dir, "embedding")
if not os.path.exists(dir_cache_embedding):
os.makedirs(dir_cache_embedding)
return dir_cache_embedding
@property
def dir_root(self) -> str:
"""The root directory to save code, information and logs."""
return os.path.join(self.dir, _runtime.runtime_id)
@property
def dir_log(self) -> str:
"""The directory for saving logs."""
return os.path.join(self.dir, _runtime.runtime_id)
@property
def dir_file(self) -> str:
"""The directory for saving files, including images, audios and
videos."""
return self._get_and_create_subdir(_DEFAULT_SUBDIR_FILE)
@property
def dir_code(self) -> str:
"""The directory for saving codes."""
return self._get_and_create_subdir(_DEFAULT_SUBDIR_CODE)
@property
def dir_invoke(self) -> str:
"""The directory for saving api invocations."""
return self._get_and_create_subdir(_DEFAULT_SUBDIR_INVOKE)
@property
def path_db(self) -> str:
"""The path to the sqlite db file."""
return self._get_file_path(_DEFAULT_SQLITE_DB_PATH)
def init(self, save_dir: str, save_api_invoke: bool = False) -> None:
"""Set the directory for saving files."""
self.dir = os.path.abspath(save_dir)
runtime_dir = os.path.join(save_dir, _runtime.runtime_id)
os.makedirs(runtime_dir, exist_ok=True)
self.save_api_invoke = save_api_invoke
# Save the project and name to the runtime directory
self._save_config()
def _save_config(self) -> None:
"""Save the configuration of the runtime in its root directory."""
cfg = {
"project": _runtime.project,
"name": _runtime.name,
"run_id": _runtime.runtime_id,
"timestamp": _runtime.timestamp,
"pid": os.getpid(),
}
with open(
os.path.join(self.dir_root, _DEFAULT_CFG_NAME),
"w",
encoding="utf-8",
) as file:
json.dump(cfg, file, indent=4)
def save_api_invocation(
self,
prefix: str,
record: dict,
) -> Union[None, str]:
"""Save api invocation locally."""
if self.save_api_invoke:
filename = f"{prefix}_{_generate_random_code()}.json"
path_save = os.path.join(str(self.dir_invoke), filename)
with open(path_save, "w", encoding="utf-8") as file:
json.dump(record, file, indent=4)
return filename
else:
return None
def save_image(
self,
image: Union[str, np.ndarray],
filename: Optional[str] = None,
) -> str:
"""Save image file locally, and return the local image path.
Args:
image (`Union[str, np.ndarray]`):
The image url, or the image array.
filename (`Optional[str]`):
The filename of the image. If not specified, a random filename
will be used.
"""
if filename is None:
filename = _DEFAULT_IMAGE_NAME.format(
_get_timestamp(
"%Y%m%d-%H%M%S",
),
_generate_random_code(),
)
path_file = os.path.join(self.dir_file, filename)
if isinstance(image, str):
# download the image from url
_download_file(image, path_file)
else:
from PIL import Image
# save image via PIL
Image.fromarray(image).save(path_file)
return path_file
def cache_text_embedding(
self,
text: str,
embedding: List[float],
embedding_model: Union[str, dict],
) -> None:
"""Cache the text embedding locally."""
record_hash = _get_text_embedding_record_hash(
text,
embedding_model,
self.hash_method,
)
# Save the embedding to the cache directory
np.save(
os.path.join(
self.dir_cache_embedding,
f"{record_hash}.npy",
),
embedding,
)
def fetch_cached_text_embedding(
self,
text: str,
embedding_model: Union[str, dict],
) -> Union[None, List[float]]:
"""Fetch the text embedding from the cache."""
record_hash = _get_text_embedding_record_hash(
text,
embedding_model,
self.hash_method,
)
try:
return np.load(
os.path.join(
self.dir_cache_embedding,
f"{record_hash}.npy",
),
)
except FileNotFoundError:
return None
@staticmethod
def _flush() -> None:
"""
Only for unittest usage. Don't use this function in your code.
Flush the file_manager singleton.
"""
global file_manager
file_manager = _FileManager()
file_manager = _FileManager()

View File

@@ -0,0 +1,282 @@
# -*- coding: utf-8 -*-
"""Logging utilities."""
import json
import os
import sys
from typing import Optional, Literal, Any
from loguru import logger
from agentscope.studio._client import _studio_client
from agentscope.web.gradio.utils import (
generate_image_from_name,
send_msg,
get_reset_msg,
thread_local_data,
)
LOG_LEVEL = Literal[
"TRACE",
"DEBUG",
"INFO",
"SUCCESS",
"WARNING",
"ERROR",
"CRITICAL",
]
LEVEL_SAVE_LOG = "SAVE_LOG"
LEVEL_SAVE_MSG = "SAVE_MSG"
LEVEL_DISPLAY_MSG = "DISPLAY_MSG"
_SPEAKER_COLORS = [
("\033[90m", "\033[0m"),
("\033[91m", "\033[0m"),
("\033[92m", "\033[0m"),
("\033[93m", "\033[0m"),
("\033[94m", "\033[0m"),
("\033[95m", "\033[0m"),
("\033[96m", "\033[0m"),
("\033[97m", "\033[0m"),
]
_SPEAKER_TO_COLORS = {}
def _get_speaker_color(speaker: str) -> tuple[str, str]:
"""Get the color markers for a speaker. If the speaker is new, assign a
new color. Otherwise, return the color that was assigned to the speaker.
Args:
speaker (`str`):
The speaker to be assigned a color.
Returns:
`tuple[str, str]`: A color marker tuple, e.g. ("<blue>", "</blue>").
"""
global _SPEAKER_COLORS, _SPEAKER_TO_COLORS
if speaker in _SPEAKER_TO_COLORS:
return _SPEAKER_TO_COLORS[speaker]
else:
markers = _SPEAKER_COLORS[
(len(_SPEAKER_TO_COLORS) + 1)
% len(
_SPEAKER_COLORS,
)
]
# Record the color for this speaker
_SPEAKER_TO_COLORS[speaker] = markers
return markers
# add chat function for logger
def _chat(
message: dict,
*args: Any,
disable_gradio: bool = False,
**kwargs: Any,
) -> None:
"""
Log a chat message with the format of"<speaker>: <content>". If the
running instance is registered in the studio, the message will be sent
and display in the studio.
Args:
message (`dict`):
The message to be logged as "<name/role>: <content>", which must
be an object of Msg class.
"""
# Push message to studio if it is active
if _studio_client.active:
_studio_client.push_message(message)
# Save message into chat file, add default to ignore not serializable
# objects
logger.log(
LEVEL_SAVE_MSG,
json.dumps(message, ensure_ascii=False, default=lambda _: None),
*args,
**kwargs,
)
# Print message in terminal with specific format
if isinstance(message, dict):
contain_name_or_role = "name" in message or "role" in message
contain_content = "content" in message
contain_url = message.get("url", None) is not None
# print content if contain name or role and contain content
if contain_name_or_role:
speaker = message.get("name", None) or message.get("role", None)
(m1, m2) = _get_speaker_color(speaker)
print_str = []
print_str_without_markers = []
if contain_content:
print_str.append(
f"{m1}\033[1m{speaker}\033[0m{m2}: {message['content']}",
)
print_str_without_markers.append(
f"{speaker}: {message['content']}",
)
if contain_url:
print_str.append(
f"{m1}\033[1m{speaker}\033[0m{m2}: {message['url']}",
)
print_str_without_markers.append(
f"{speaker}: {message['url']}",
)
if len(print_str) > 0:
print_str = "\n".join(print_str)
print_str_without_markers = "\n".join(
print_str_without_markers,
)
logger.log(LEVEL_DISPLAY_MSG, print_str, *args, **kwargs)
logger.log(
LEVEL_SAVE_LOG,
print_str_without_markers,
*args,
**kwargs,
)
if hasattr(thread_local_data, "uid") and not disable_gradio:
log_gradio(message, thread_local_data.uid, **kwargs)
return
logger.log(LEVEL_DISPLAY_MSG, message, *args, **kwargs)
logger.log(LEVEL_SAVE_LOG, message, *args, **kwargs)
def log_gradio(message: dict, uid: str, **kwargs: Any) -> None:
"""Send chat message to studio.
Args:
message (`dict`):
The message to be logged. It should have "name"(or "role") and
"content" keys, and the message will be logged as "<name/role>:
<content>".
uid (`str`):
The local value 'uid' of the thread.
"""
if uid:
get_reset_msg(uid=uid)
name = message.get("name", "default") or message.get("role", "default")
avatar = kwargs.get("avatar", None) or generate_image_from_name(
message["name"],
)
msg = message["content"]
flushing = True
if "url" in message and message["url"]:
flushing = False
if isinstance(message["url"], str):
message["url"] = [message["url"]]
for i in range(len(message["url"])):
msg += "\n" + f"""<img src="{message['url'][i]}"/>"""
if "audio_path" in message and message["audio_path"]:
flushing = False
if isinstance(message["audio_path"], str):
message["audio_path"] = [message["audio_path"]]
for i in range(len(message["audio_path"])):
msg += (
"\n"
+ f"""<audio src="{message['audio_path'][i]}"
controls/></audio>"""
)
if "video_path" in message and message["video_path"]:
flushing = False
if isinstance(message["video_path"], str):
message["video_path"] = [message["video_path"]]
for i in range(len(message["video_path"])):
msg += (
"\n"
+ f"""<video src="{message['video_path'][i]}"
controls/></video>"""
)
send_msg(
msg,
role=name,
uid=uid,
flushing=flushing,
avatar=avatar,
)
def _level_format(record: dict) -> str:
"""Format the log record."""
# Display the chat message
if record["level"].name in [LEVEL_DISPLAY_MSG, LEVEL_SAVE_LOG]:
return "{message}\n"
else:
return (
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{"
"level: <8}</level> | <cyan>{name}</cyan>:<cyan>{"
"function}</cyan>:<cyan>{line}</cyan> - <level>{"
"message}</level>\n"
)
def setup_logger(
path_log: Optional[str] = None,
level: LOG_LEVEL = "INFO",
) -> None:
r"""Setup `loguru.logger` and redirect stderr to logging.
Args:
path_log (`str`, defaults to `""`):
The directory of log files.
level (`str`, defaults to `"INFO"`):
The logging level, which is one of the following: `"TRACE"`,
`"DEBUG"`, `"INFO"`, `"SUCCESS"`, `"WARNING"`, `"ERROR"`,
`"CRITICAL"`.
"""
# avoid reinit in subprocess
if not hasattr(logger, "chat"):
# add chat function for logger
logger.level(LEVEL_SAVE_LOG, no=51)
logger.level(LEVEL_DISPLAY_MSG, no=52)
# save chat message into file
logger.level(LEVEL_SAVE_MSG, no=53)
logger.chat = _chat
# set logging level
logger.remove()
# standard output for all logging except chat
logger.add(
sys.stdout,
filter=lambda record: record["level"].name
not in [LEVEL_SAVE_LOG, LEVEL_SAVE_MSG],
format=_level_format,
enqueue=True,
level=level,
)
if path_log is not None:
os.makedirs(path_log, exist_ok=True)
path_log_file = os.path.join(path_log, "logging.log")
path_chat_file = os.path.join(
path_log,
"logging.chat",
)
# save all logging into file
logger.add(
path_log_file,
filter=lambda record: record["level"].name
not in [LEVEL_SAVE_MSG, LEVEL_DISPLAY_MSG],
format=_level_format,
enqueue=True,
level=level,
)
logger.add(
path_chat_file,
format="{message}",
enqueue=True,
level=LEVEL_SAVE_MSG,
)

View File

@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
"""
import al memory related modules
"""
from .memory import MemoryBase
from .temporary_memory import TemporaryMemory
__all__ = [
"MemoryBase",
"TemporaryMemory",
]

View File

@@ -0,0 +1,129 @@
# -*- coding: utf-8 -*-
"""
Base class for memory
TODO: a abstract class for a piece of memory
TODO: data structure to organize multiple memory pieces in memory class
"""
from abc import ABC, abstractmethod
from typing import Iterable, Sequence
from typing import Optional
from typing import Union
from typing import Callable
from ..message import Msg
class MemoryBase(ABC):
"""Base class for memory."""
_version: int = 1
def __init__(
self,
config: Optional[dict] = None,
) -> None:
"""MemoryBase is a base class for memory of agents.
Args:
config (`Optional[dict]`, defaults to `None`):
Configuration of this memory.
"""
self.config = {} if config is None else config
def update_config(self, config: dict) -> None:
"""
Configure memory as specified in config
Args:
config (`dict`): Configuration of resetting this memory
"""
self.config = config
@abstractmethod
def get_memory(
self,
recent_n: Optional[int] = None,
filter_func: Optional[Callable[[int, dict], bool]] = None,
) -> list:
"""
Return a certain range (`recent_n` or all) of memory,
filtered by `filter_func`
Args:
recent_n (int, optional):
indicate the most recent N memory pieces to be returned.
filter_func (Optional[Callable[[int, dict], bool]]):
filter function to decide which pieces of memory should
be returned, taking the index and a piece of memory as
input and return True (return this memory) or False
(does not return)
"""
@abstractmethod
def add(
self,
memories: Union[Sequence[Msg], Msg, None],
) -> None:
"""
Adding new memory fragment, depending on how the memory are stored
Args:
memories (Union[Sequence[Msg], Msg, None]):
Memories to be added.
"""
@abstractmethod
def delete(self, index: Union[Iterable, int]) -> None:
"""
Delete memory fragment, depending on how the memory are stored
and matched
Args:
index (Union[Iterable, int]):
indices of the memory fragments to delete
"""
@abstractmethod
def load(
self,
memories: Union[str, list[Msg], Msg],
overwrite: bool = False,
) -> None:
"""
Load memory, depending on how the memory are passed, design to load
from both file or dict
Args:
memories (Union[str, list[Msg], Msg]):
memories to be loaded.
If it is in str type, it will be first checked if it is a
file; otherwise it will be deserialized as messages.
Otherwise, memories must be either in message type or list
of messages.
overwrite (bool):
if True, clear the current memory before loading the new ones;
if False, memories will be appended to the old one at the end.
"""
@abstractmethod
def export(
self,
file_path: Optional[str] = None,
to_mem: bool = False,
) -> Optional[list]:
"""
Export memory, depending on how the memory are stored
Args:
file_path (Optional[str]):
file path to save the memory to.
to_mem (Optional[str]):
if True, just return the list of messages in memory
Notice: this method prevents file_path is None when to_mem
is False.
"""
@abstractmethod
def clear(self) -> None:
"""Clean memory, depending on how the memory are stored"""
@abstractmethod
def size(self) -> int:
"""Returns the number of memory segments in memory."""
raise NotImplementedError

View File

@@ -0,0 +1,350 @@
# -*- coding: utf-8 -*-
"""
Memory module for conversation
"""
import json
import os
from typing import Iterable, Sequence
from typing import Optional
from typing import Union
from typing import Callable
from loguru import logger
from .memory import MemoryBase
from ..models import load_model_by_config_name
from ..service.retrieval.retrieval_from_list import retrieve_from_list
from ..service.retrieval.similarity import Embedding
from ..message import (
deserialize,
serialize,
MessageBase,
Msg,
PlaceholderMessage,
)
class TemporaryMemory(MemoryBase):
"""
In-memory memory module, not writing to hard disk
"""
def __init__(
self,
config: Optional[dict] = None,
embedding_model: Union[str, Callable] = None,
) -> None:
"""
Temporary memory module for conversation.
Args:
config (dict):
configuration of the memory
embedding_model (Union[str, Callable])
if the temporary memory needs to be embedded,
then either pass the name of embedding model or
the embedding model itself.
"""
super().__init__(config)
self._content = []
# prepare embedding model if needed
if isinstance(embedding_model, str):
self.embedding_model = load_model_by_config_name(embedding_model)
else:
self.embedding_model = embedding_model
def add(
self,
memories: Union[Sequence[Msg], Msg, None],
embed: bool = False,
) -> None:
# pylint: disable=too-many-branches
"""
Adding new memory fragment, depending on how the memory are stored
Args:
memories (`Union[Sequence[Msg], Msg, None]`):
Memories to be added.
embed (`bool`):
Whether to generate embedding for the new added memories
"""
if memories is None:
return
if not isinstance(memories, Sequence):
record_memories = [memories]
else:
record_memories = memories
# if memory doesn't have id attribute, we skip the checking
memories_idx = set(_.id for _ in self._content if hasattr(_, "id"))
for memory_unit in record_memories:
if not issubclass(type(memory_unit), MessageBase):
try:
memory_unit = Msg(**memory_unit)
except Exception as exc:
raise ValueError(
f"Cannot add {memory_unit} to memory, "
f"must be with subclass of MessageBase",
) from exc
# in case this is a PlaceholderMessage, try to update
# the values first
if isinstance(memory_unit, PlaceholderMessage):
memory_unit.update_value()
memory_unit = Msg(**memory_unit)
# add to memory if it's new
if (
not hasattr(memory_unit, "id")
or memory_unit.id not in memories_idx
):
if embed:
if self.embedding_model:
# TODO: embed only content or its string representation
memory_unit.embedding = self.embedding_model(
[memory_unit],
return_embedding_only=True,
)
else:
raise RuntimeError("Embedding model is not provided.")
self._content.append(memory_unit)
def delete(self, index: Union[Iterable, int]) -> None:
"""
Delete memory fragment, depending on how the memory are stored
and matched
Args:
index (Union[Iterable, int]):
indices of the memory fragments to delete
"""
if self.size() == 0:
logger.warning(
"The memory is empty, and the delete operation is "
"skipping.",
)
return
if isinstance(index, int):
index = [index]
if isinstance(index, list):
index = set(index)
invalid_index = [_ for _ in index if _ >= self.size() or _ < 0]
if len(invalid_index) > 0:
logger.warning(
f"Skip delete operation for the invalid "
f"index {invalid_index}",
)
self._content = [
_ for i, _ in enumerate(self._content) if i not in index
]
else:
raise NotImplementedError(
"index type only supports {None, int, list}",
)
def export(
self,
file_path: Optional[str] = None,
to_mem: bool = False,
) -> Optional[list]:
"""
Export memory, depending on how the memory are stored
Args:
file_path (Optional[str]):
file path to save the memory to. The messages will
be serialized and written to the file.
to_mem (Optional[str]):
if True, just return the list of messages in memory
Notice: this method prevents file_path is None when to_mem
is False.
"""
if to_mem:
return self._content
if to_mem is False and file_path is not None:
with open(file_path, "w", encoding="utf-8") as f:
f.write(serialize(self._content))
else:
raise NotImplementedError(
"file type only supports "
"{json, yaml, pkl}, default is json",
)
return None
def load(
self,
memories: Union[str, list[Msg], Msg],
overwrite: bool = False,
) -> None:
"""
Load memory, depending on how the memory are passed, design to load
from both file or dict
Args:
memories (Union[str, list[Msg], Msg]):
memories to be loaded.
If it is in str type, it will be first checked if it is a
file; otherwise it will be deserialized as messages.
Otherwise, memories must be either in message type or list
of messages.
overwrite (bool):
if True, clear the current memory before loading the new ones;
if False, memories will be appended to the old one at the end.
"""
if isinstance(memories, str):
if os.path.isfile(memories):
with open(memories, "r", encoding="utf-8") as f:
load_memories = deserialize(f.read())
else:
try:
load_memories = deserialize(memories)
if not isinstance(load_memories, dict) and not isinstance(
load_memories,
list,
):
logger.warning(
"The memory loaded by json.loads is "
"neither a dict nor a list, which may "
"cause unpredictable errors.",
)
except json.JSONDecodeError as e:
raise json.JSONDecodeError(
f"Cannot load [{memories}] via " f"json.loads.",
e.doc,
e.pos,
)
else:
load_memories = memories
# overwrite the original memories after loading the new ones
if overwrite:
self.clear()
self.add(load_memories)
def clear(self) -> None:
"""Clean memory, depending on how the memory are stored"""
self._content = []
def size(self) -> int:
"""Returns the number of memory segments in memory."""
return len(self._content)
def retrieve_by_embedding(
self,
query: Union[str, Embedding],
metric: Callable[[Embedding, Embedding], float],
top_k: int = 1,
preserve_order: bool = True,
embedding_model: Callable[[Union[str, dict]], Embedding] = None,
) -> list[dict]:
"""Retrieve memory by their embeddings.
Args:
query (`Union[str, Embedding]`):
Query string or embedding.
metric (`Callable[[Embedding, Embedding], float]`):
A metric to compute the relevance between embeddings of query
and memory. In default, higher relevance means better match,
and you can set `reverse` to `True` to reverse the order.
top_k (`int`, defaults to `1`):
The number of memory units to retrieve.
preserve_order (`bool`, defaults to `True`):
Whether to preserve the original order of the retrieved memory
units.
embedding_model (`Callable[[Union[str, dict]], Embedding]`, \
defaults to `None`):
A callable object to embed the memory unit. If not provided, it
will use the default embedding model.
Returns:
`list[dict]`: a list of retrieved memory units in
specific order.
"""
retrieved_items = retrieve_from_list(
query,
self.get_embeddings(embedding_model or self.embedding_model),
metric,
top_k,
self.embedding_model,
preserve_order,
).content
# obtain the corresponding memory item
response = []
for score, index, _ in retrieved_items:
response.append(
{
"score": score,
"index": index,
"memory": self._content[index],
},
)
return response
def get_embeddings(
self,
embedding_model: Callable[[Union[str, dict]], Embedding] = None,
) -> list:
"""Get embeddings of all memory units. If `embedding_model` is
provided, the memory units that doesn't have `embedding` attribute
will be embedded. Otherwise, its embedding will be `None`.
Args:
embedding_model
(`Callable[[Union[str, dict]], Embedding]`, defaults to
`None`):
Embedding model or embedding vector.
Returns:
`list[Union[Embedding, None]]`: List of embeddings or None.
"""
embeddings = []
for memory_unit in self._content:
if memory_unit.embedding is None and embedding_model is not None:
# embedding
# TODO: embed only content or its string representation
memory_unit.embedding = embedding_model(memory_unit)
embeddings.append(memory_unit.embedding)
return embeddings
def get_memory(
self,
recent_n: Optional[int] = None,
filter_func: Optional[Callable[[int, dict], bool]] = None,
) -> list:
"""Retrieve memory.
Args:
recent_n (`Optional[int]`, default `None`):
The last number of memories to return.
filter_func
(`Callable[[int, dict], bool]`, default to `None`):
The function to filter memories, which take the index and
memory unit as input, and return a boolean value.
"""
# extract the recent `recent_n` entries in memories
if recent_n is None:
memories = self._content
else:
if recent_n > self.size():
logger.warning(
"The retrieved number of memories {} is "
"greater than the total number of memories {"
"}",
recent_n,
self.size(),
)
memories = self._content[-recent_n:]
# filter the memories
if filter_func is not None:
memories = [_ for i, _ in enumerate(memories) if filter_func(i, _)]
return memories

View File

@@ -0,0 +1,377 @@
# -*- coding: utf-8 -*-
"""The base class for message unit"""
from typing import Any, Optional, Union, Sequence, Literal
from uuid import uuid4
import json
from loguru import logger
from .rpc import RpcAgentClient, ResponseStub, call_in_thread
from .utils.tools import _get_timestamp
class MessageBase(dict):
"""Base Message class, which is used to maintain information for dialog,
memory and used to construct prompt.
"""
def __init__(
self,
name: str,
content: Any,
role: Literal["user", "system", "assistant"] = "assistant",
url: Optional[Union[Sequence[str], str]] = None,
timestamp: Optional[str] = None,
**kwargs: Any,
) -> None:
"""Initialize the message object
Args:
name (`str`):
The name of who send the message. It's often used in
role-playing scenario to tell the name of the sender.
content (`Any`):
The content of the message.
role (`Literal["system", "user", "assistant"]`, defaults to "assistant"):
The role of who send the message. It can be one of the
`"system"`, `"user"`, or `"assistant"`. Default to
`"assistant"`.
url (`Optional[Union[list[str], str]]`, defaults to None):
A url to file, image, video, audio or website.
timestamp (`Optional[str]`, defaults to None):
The timestamp of the message, if None, it will be set to
current time.
**kwargs (`Any`):
Other attributes of the message.
""" # noqa
# id and timestamp will be added to the object as its attributes
# rather than items in dict
self.id = uuid4().hex
if timestamp is None:
self.timestamp = _get_timestamp()
else:
self.timestamp = timestamp
self.name = name
self.content = content
self.role = role
self.url = url
self.update(kwargs)
def __getattr__(self, key: Any) -> Any:
try:
return self[key]
except KeyError as e:
raise AttributeError(f"no attribute '{key}'") from e
def __setattr__(self, key: Any, value: Any) -> None:
self[key] = value
def __delattr__(self, key: Any) -> None:
try:
del self[key]
except KeyError as e:
raise AttributeError(f"no attribute '{key}'") from e
def to_str(self) -> str:
"""Return the string representation of the message"""
raise NotImplementedError
def serialize(self) -> str:
"""Return the serialized message."""
raise NotImplementedError
class Msg(MessageBase):
"""The Message class."""
id: str
"""The id of the message."""
name: str
"""The name of who send the message."""
content: Any
"""The content of the message."""
role: Literal["system", "user", "assistant"]
"""The role of the message sender."""
metadata: Optional[dict]
"""Save the information for application's control flow, or other
purposes."""
url: Optional[Union[Sequence[str], str]]
"""A url to file, image, video, audio or website."""
timestamp: str
"""The timestamp of the message."""
def __init__(
self,
name: str,
content: Any,
role: Literal["system", "user", "assistant"] = None,
url: Optional[Union[Sequence[str], str]] = None,
timestamp: Optional[str] = None,
echo: bool = False,
metadata: Optional[Union[dict, str]] = None,
**kwargs: Any,
) -> None:
"""Initialize the message object
Args:
name (`str`):
The name of who send the message.
content (`Any`):
The content of the message.
role (`Literal["system", "user", "assistant"]`):
Used to identify the source of the message, e.g. the system
information, the user input, or the model response. This
argument is used to accommodate most Chat API formats.
url (`Optional[Union[list[str], str]]`, defaults to `None`):
A url to file, image, video, audio or website.
timestamp (`Optional[str]`, defaults to `None`):
The timestamp of the message, if None, it will be set to
current time.
echo (`bool`, defaults to `False`):
Whether to print the message to the console.
metadata (`Optional[Union[dict, str]]`, defaults to `None`):
Save the information for application's control flow, or other
purposes.
**kwargs (`Any`):
Other attributes of the message.
"""
if role is None:
logger.warning(
"A new field `role` is newly added to the message. "
"Please specify the role of the message. Currently we use "
'a default "assistant" value.',
)
super().__init__(
name=name,
content=content,
role=role or "assistant",
url=url,
timestamp=timestamp,
metadata=metadata,
**kwargs,
)
if echo:
logger.chat(self)
def to_str(self) -> str:
"""Return the string representation of the message"""
return f"{self.name}: {self.content}"
def serialize(self) -> str:
return json.dumps({"__type": "Msg", **self})
#return json.dumps({"role" : self.role, "content": str(self.content)})
class PlaceholderMessage(Msg):
"""A placeholder for the return message of RpcAgent."""
PLACEHOLDER_ATTRS = {
"_host",
"_port",
"_client",
"_task_id",
"_stub",
"_is_placeholder",
}
LOCAL_ATTRS = {
"name",
"timestamp",
*PLACEHOLDER_ATTRS,
}
def __init__(
self,
name: str,
content: Any,
url: Optional[Union[Sequence[str], str]] = None,
timestamp: Optional[str] = None,
host: str = None,
port: int = None,
task_id: int = None,
client: Optional[RpcAgentClient] = None,
x: dict = None,
**kwargs: Any,
) -> None:
"""A placeholder message, records the address of the real message.
Args:
name (`str`):
The name of who send the message. It's often used in
role-playing scenario to tell the name of the sender.
However, you can also only use `role` when calling openai api.
The usage of `name` refers to
https://cookbook.openai.com/examples/how_to_format_inputs_to_chatgpt_models.
content (`Any`):
The content of the message.
role (`Literal["system", "user", "assistant"]`, defaults to "assistant"):
The role of the message, which can be one of the `"system"`,
`"user"`, or `"assistant"`.
url (`Optional[Union[list[str], str]]`, defaults to None):
A url to file, image, video, audio or website.
timestamp (`Optional[str]`, defaults to None):
The timestamp of the message, if None, it will be set to
current time.
host (`str`, defaults to `None`):
The hostname of the rpc server where the real message is
located.
port (`int`, defaults to `None`):
The port of the rpc server where the real message is located.
task_id (`int`, defaults to `None`):
The task id of the real message in the rpc server.
client (`RpcAgentClient`, defaults to `None`):
An RpcAgentClient instance used to connect to the generator of
this placeholder.
x (`dict`, defaults to `None`):
Input parameters used to call rpc methods on the client.
""" # noqa
super().__init__(
name=name,
content=content,
url=url,
timestamp=timestamp,
**kwargs,
)
# placeholder indicates whether the real message is still in rpc server
self._is_placeholder = True
if client is None:
self._stub: ResponseStub = None
self._host: str = host
self._port: int = port
self._task_id: int = task_id
else:
self._stub = call_in_thread(
client,
x.serialize() if x is not None else "",
"_reply",
)
self._host = client.host
self._port = client.port
self._task_id = None
def __is_local(self, key: Any) -> bool:
return (
key in PlaceholderMessage.LOCAL_ATTRS or not self._is_placeholder
)
def __getattr__(self, __name: str) -> Any:
"""Get attribute value from PlaceholderMessage. Get value from rpc
agent server if necessary.
Args:
__name (`str`):
Attribute name.
"""
if not self.__is_local(__name):
self.update_value()
return MessageBase.__getattr__(self, __name)
def __getitem__(self, __key: Any) -> Any:
"""Get item value from PlaceholderMessage. Get value from rpc
agent server if necessary.
Args:
__key (`Any`):
Item name.
"""
if not self.__is_local(__key):
self.update_value()
return MessageBase.__getitem__(self, __key)
def to_str(self) -> str:
return f"{self.name}: {self.content}"
def update_value(self) -> MessageBase:
"""Get attribute values from rpc agent server immediately"""
if self._is_placeholder:
# retrieve real message from rpc agent server
self.__update_task_id()
client = RpcAgentClient(self._host, self._port)
result = client.call_func(
func_name="_get",
value=json.dumps({"task_id": self._task_id}),
)
msg = deserialize(result)
status = msg.pop("__status", "OK")
if status == "ERROR":
raise RuntimeError(msg.content)
self.update(msg)
# the actual value has been updated, not a placeholder anymore
self._is_placeholder = False
return self
def __update_task_id(self) -> None:
if self._stub is not None:
try:
resp = deserialize(self._stub.get_response())
except Exception as e:
logger.error(
f"Failed to get task_id: {self._stub.get_response()}",
)
raise ValueError(
f"Failed to get task_id: {self._stub.get_response()}",
) from e
self._task_id = resp["task_id"] # type: ignore[call-overload]
self._stub = None
def serialize(self) -> str:
if self._is_placeholder:
self.__update_task_id()
return json.dumps(
{
"__type": "PlaceholderMessage",
"name": self.name,
"content": None,
"timestamp": self.timestamp,
"host": self._host,
"port": self._port,
"task_id": self._task_id,
},
)
else:
states = {
k: v
for k, v in self.items()
if k not in PlaceholderMessage.PLACEHOLDER_ATTRS
}
states["__type"] = "Msg"
return json.dumps(states)
_MSGS = {
"Msg": Msg,
"PlaceholderMessage": PlaceholderMessage,
}
def deserialize(s: Union[str, bytes]) -> Union[Msg, Sequence]:
"""Deserialize json string into MessageBase"""
js_msg = json.loads(s)
msg_type = js_msg.pop("__type")
if msg_type == "List":
return [deserialize(s) for s in js_msg["__value"]]
elif msg_type not in _MSGS:
raise NotImplementedError(
f"Deserialization of {msg_type} is not supported.",
)
return _MSGS[msg_type](**js_msg)
def serialize(messages: Union[Sequence[MessageBase], MessageBase]) -> str:
"""Serialize multiple MessageBase instance"""
if isinstance(messages, MessageBase):
return messages.serialize()
seq = [msg.serialize() for msg in messages]
return json.dumps({"__type": "List", "__value": seq})

View File

@@ -0,0 +1,189 @@
# -*- coding: utf-8 -*-
""" Import modules in models package."""
import json
from typing import Union, Type
from loguru import logger
from .config import _ModelConfig
from .model import ModelWrapperBase
from .response import ModelResponse
from .post_model import (
PostAPIModelWrapperBase,
PostAPIChatWrapper,
)
from .openai_model import (
OpenAIWrapperBase,
OpenAIChatWrapper,
OpenAIDALLEWrapper,
OpenAIEmbeddingWrapper,
)
from .dashscope_model import (
DashScopeChatWrapper,
DashScopeImageSynthesisWrapper,
DashScopeTextEmbeddingWrapper,
DashScopeMultiModalWrapper,
)
from .ollama_model import (
OllamaChatWrapper,
OllamaEmbeddingWrapper,
OllamaGenerationWrapper,
)
from .gemini_model import (
GeminiChatWrapper,
GeminiEmbeddingWrapper,
)
from .zhipu_model import (
ZhipuAIChatWrapper,
ZhipuAIEmbeddingWrapper,
)
from .litellm_model import (
LiteLLMChatWrapper,
)
__all__ = [
"ModelWrapperBase",
"ModelResponse",
"PostAPIModelWrapperBase",
"PostAPIChatWrapper",
"OpenAIWrapperBase",
"OpenAIChatWrapper",
"OpenAIDALLEWrapper",
"OpenAIEmbeddingWrapper",
"DashScopeChatWrapper",
"DashScopeImageSynthesisWrapper",
"DashScopeTextEmbeddingWrapper",
"DashScopeMultiModalWrapper",
"OllamaChatWrapper",
"OllamaEmbeddingWrapper",
"OllamaGenerationWrapper",
"GeminiChatWrapper",
"GeminiEmbeddingWrapper",
"ZhipuAIChatWrapper",
"ZhipuAIEmbeddingWrapper",
"LiteLLMChatWrapper",
"load_model_by_config_name",
"load_config_by_name",
"read_model_configs",
"clear_model_configs",
]
_MODEL_CONFIGS: dict[str, dict] = {}
def _get_model_wrapper(model_type: str) -> Type[ModelWrapperBase]:
"""Get the specific type of model wrapper
Args:
model_type (`str`): The model type name.
Returns:
`Type[ModelWrapperBase]`: The corresponding model wrapper class.
"""
wrapper = ModelWrapperBase.get_wrapper(model_type=model_type)
if wrapper is None:
logger.warning(
f"Unsupported model_type [{model_type}],"
"use PostApiModelWrapper instead.",
)
return PostAPIModelWrapperBase
return wrapper
def load_config_by_name(config_name: str) -> Union[dict, None]:
"""Load the model config by name, and return the config dict."""
return _MODEL_CONFIGS.get(config_name, None)
def load_model_by_config_name(config_name: str) -> ModelWrapperBase:
"""Load the model by config name, and return the model wrapper."""
if len(_MODEL_CONFIGS) == 0:
raise ValueError(
"No model configs loaded, please call "
"`read_model_configs` first.",
)
# Find model config by name
if config_name not in _MODEL_CONFIGS:
raise ValueError(
f"Cannot find [{config_name}] in loaded configurations.",
)
config = _MODEL_CONFIGS.get(config_name, None)
if config is None:
raise ValueError(
f"Cannot find [{config_name}] in loaded configurations.",
)
model_type = config.model_type
kwargs = {k: v for k, v in config.items() if k != "model_type"}
return _get_model_wrapper(model_type=model_type)(**kwargs)
def clear_model_configs() -> None:
"""Clear the loaded model configs."""
_MODEL_CONFIGS.clear()
def read_model_configs(
configs: Union[dict, str, list],
clear_existing: bool = False,
) -> None:
"""read model configs from a path or a list of dicts.
Args:
configs (`Union[str, list, dict]`):
The path of the model configs | a config dict | a list of model
configs.
clear_existing (`bool`, defaults to `False`):
Whether to clear the loaded model configs before reading.
Returns:
`dict`:
The model configs.
"""
if clear_existing:
clear_model_configs()
cfgs = None
if isinstance(configs, str):
with open(configs, "r", encoding="utf-8") as f:
cfgs = json.load(f)
if isinstance(configs, dict):
cfgs = [configs]
if isinstance(configs, list):
if not all(isinstance(_, dict) for _ in configs):
raise ValueError(
"The model config unit should be a dict.",
)
cfgs = configs
if cfgs is None:
raise TypeError(
f"Invalid type of model_configs, it could be a dict, a list of "
f"dicts, or a path to a json file (containing a dict or a list "
f"of dicts), but got {type(configs)}",
)
format_configs = _ModelConfig.format_configs(configs=cfgs)
# check if name is unique
for cfg in format_configs:
if cfg.config_name in _MODEL_CONFIGS:
logger.warning(
f"config_name [{cfg.config_name}] already exists.",
)
continue
_MODEL_CONFIGS[cfg.config_name] = cfg
# print the loaded model configs
logger.info(
"Load configs for model wrapper: {}",
", ".join(_MODEL_CONFIGS.keys()),
)

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
"""The model config."""
from typing import Union, Sequence, Any
from loguru import logger
class _ModelConfig(dict):
"""Base class for model config."""
__getattr__ = dict.__getitem__
__setattr__ = dict.__setitem__
def __init__(
self,
config_name: str,
model_type: str = None,
**kwargs: Any,
):
"""Initialize the config with the given arguments, and checking the
type of the arguments.
Args:
config_name (`str`): A unique name of the model config.
model_type (`str`, optional): The class name (or its model type) of
the generated model wrapper. Defaults to None.
Raises:
`ValueError`: If `config_name` is not provided.
"""
if config_name is None:
raise ValueError("The `config_name` field is required for Cfg")
if model_type is None:
logger.warning(
f"`model_type` is not provided in config [{config_name}],"
" use `PostAPIModelWrapperBase` by default.",
)
super().__init__(
config_name=config_name,
model_type=model_type,
**kwargs,
)
@classmethod
def format_configs(
cls,
configs: Union[Sequence[dict], dict],
) -> Sequence:
"""Covert config dicts into a list of _ModelConfig.
Args:
configs (Union[Sequence[dict], dict]): configs in dict format.
Returns:
Sequence[_ModelConfig]: converted ModelConfig list.
"""
if isinstance(configs, dict):
return [_ModelConfig(**configs)]
return [_ModelConfig(**cfg) for cfg in configs]

View File

@@ -0,0 +1,847 @@
# -*- coding: utf-8 -*-
"""Model wrapper for DashScope models"""
import os
from abc import ABC
from http import HTTPStatus
from typing import Any, Union, List, Sequence
from loguru import logger
from ..message import Msg
from ..utils.tools import _convert_to_str, _guess_type_by_extension
try:
import dashscope
except ImportError:
dashscope = None
from .model import ModelWrapperBase, ModelResponse
from ..file_manager import file_manager
class DashScopeWrapperBase(ModelWrapperBase, ABC):
"""The model wrapper for DashScope API."""
def __init__(
self,
config_name: str,
model_name: str = None,
api_key: str = None,
generate_args: dict = None,
**kwargs: Any,
) -> None:
"""Initialize the DashScope wrapper.
Args:
config_name (`str`):
The name of the model config.
model_name (`str`, default `None`):
The name of the model to use in DashScope API.
api_key (`str`, default `None`):
The API key for DashScope API.
generate_args (`dict`, default `None`):
The extra keyword arguments used in DashScope api generation,
e.g. `temperature`, `seed`.
"""
if model_name is None:
model_name = config_name
logger.warning("model_name is not set, use config_name instead.")
super().__init__(config_name=config_name)
if dashscope is None:
raise ImportError(
"Cannot find dashscope package in current python environment.",
)
self.model_name = model_name
self.generate_args = generate_args or {}
self.api_key = api_key
dashscope.api_key = self.api_key
self.max_length = None
# Set monitor accordingly
self._register_default_metrics()
def format(
self,
*args: Union[Msg, Sequence[Msg]],
) -> Union[List[dict], str]:
raise RuntimeError(
f"Model Wrapper [{type(self).__name__}] doesn't "
f"need to format the input. Please try to use the "
f"model wrapper directly.",
)
class DashScopeChatWrapper(DashScopeWrapperBase):
"""The model wrapper for DashScope's chat API, refer to
https://help.aliyun.com/zh/dashscope/developer-reference/api-details
"""
model_type: str = "dashscope_chat"
deprecated_model_type: str = "tongyi_chat"
def _register_default_metrics(self) -> None:
# Set monitor accordingly
# TODO: set quota to the following metrics
self.monitor.register(
self._metric("call_counter"),
metric_unit="times",
)
self.monitor.register(
self._metric("prompt_tokens"),
metric_unit="token",
)
self.monitor.register(
self._metric("completion_tokens"),
metric_unit="token",
)
self.monitor.register(
self._metric("total_tokens"),
metric_unit="token",
)
def __call__(
self,
messages: list,
**kwargs: Any,
) -> ModelResponse:
"""Processes a list of messages to construct a payload for the
DashScope API call. It then makes a request to the DashScope API
and returns the response. This method also updates monitoring
metrics based on the API response.
Each message in the 'messages' list can contain text content and
optionally an 'image_urls' key. If 'image_urls' is provided,
it is expected to be a list of strings representing URLs to images.
These URLs will be transformed to a suitable format for the DashScope
API, which might involve converting local file paths to data URIs.
Args:
messages (`list`):
A list of messages to process.
**kwargs (`Any`):
The keyword arguments to DashScope chat completions API,
e.g. `temperature`, `max_tokens`, `top_p`, etc. Please
refer to
https://help.aliyun.com/zh/dashscope/developer-reference/api-details
for more detailed arguments.
Returns:
`ModelResponse`:
The response text in text field, and the raw response in
raw field.
Note:
`parse_func`, `fault_handler` and `max_retries` are reserved for
`_response_parse_decorator` to parse and check the response
generated by model wrapper. Their usages are listed as follows:
- `parse_func` is a callable function used to parse and check
the response generated by the model, which takes the response
as input.
- `max_retries` is the maximum number of retries when the
`parse_func` raise an exception.
- `fault_handler` is a callable function which is called
when the response generated by the model is invalid after
`max_retries` retries.
The rule of roles in messages for DashScope is very rigid,
for more details, please refer to
https://help.aliyun.com/zh/dashscope/developer-reference/api-details
"""
# step1: prepare keyword arguments
kwargs = {**self.generate_args, **kwargs}
# step2: checking messages
if not isinstance(messages, list):
raise ValueError(
"Dashscope `messages` field expected type `list`, "
f"got `{type(messages)}` instead.",
)
if not all("role" in msg and "content" in msg for msg in messages):
raise ValueError(
"Each message in the 'messages' list must contain a 'role' "
"and 'content' key for DashScope API.",
)
# step3: forward to generate response
response = dashscope.Generation.call(
model=self.model_name,
messages=messages,
result_format="message", # set the result to be "message" format.
**kwargs,
)
if response.status_code != HTTPStatus.OK:
error_msg = (
f" Request id: {response.request_id},"
f" Status code: {response.status_code},"
f" error code: {response.code},"
f" error message: {response.message}."
)
raise RuntimeError(error_msg)
# step4: record the api invocation if needed
self._save_model_invocation(
arguments={
"model": self.model_name,
"messages": messages,
**kwargs,
},
response=response,
)
# step5: update monitor accordingly
# The metric names are unified for comparison
self.update_monitor(
call_counter=1,
prompt_tokens=response.usage.get("input_tokens", 0),
completion_tokens=response.usage.get("output_tokens", 0),
total_tokens=response.usage.get("input_tokens", 0)
+ response.usage.get("output_tokens", 0),
)
# step6: return response
return ModelResponse(
text=response.output["choices"][0]["message"]["content"],
raw=response,
)
def format(
self,
*args: Union[Msg, Sequence[Msg]],
) -> List:
"""Format the messages for DashScope Chat API.
In this format function, the input messages are formatted into a
single system messages with format "{name}: {content}" for each
message. Note this strategy maybe not suitable for all scenarios,
and developers are encouraged to implement their own prompt
engineering strategies.
The following is an example:
.. code-block:: python
prompt = model.format(
Msg("system", "You're a helpful assistant", role="system"),
Msg("Bob", "Hi, how can I help you?", role="assistant"),
Msg("user", "What's the date today?", role="user")
)
The prompt will be as follows:
.. code-block:: python
[
{
"role": "system",
"content": "You're a helpful assistant",
}
{
"role": "user",
"content": (
"## Dialogue History\\n"
"Bob: Hi, how can I help you?\\n"
"user: What's the date today?"
)
}
]
Args:
args (`Union[Msg, Sequence[Msg]]`):
The input arguments to be formatted, where each argument
should be a `Msg` object, or a list of `Msg` objects.
In distribution, placeholder is also allowed.
Returns:
`List[dict]`:
The formatted messages.
"""
# Parse all information into a list of messages
input_msgs = []
for _ in args:
if _ is None:
continue
if isinstance(_, Msg):
input_msgs.append(_)
elif isinstance(_, list) and all(isinstance(__, Msg) for __ in _):
input_msgs.extend(_)
else:
raise TypeError(
f"The input should be a Msg object or a list "
f"of Msg objects, got {type(_)}.",
)
messages = []
# record dialog history as a list of strings
dialogue = []
for i, unit in enumerate(input_msgs):
if i == 0 and unit.role == "system":
# system prompt
messages.append(
{
"role": unit.role,
"content": _convert_to_str(unit.content),
},
)
else:
# Merge all messages into a dialogue history prompt
dialogue.append(
f"{unit.name}: {_convert_to_str(unit.content)}",
)
dialogue_history = "\n".join(dialogue)
user_content_template = "## Dialogue History\n{dialogue_history}"
messages.append(
{
"role": "user",
"content": user_content_template.format(
dialogue_history=dialogue_history,
),
},
)
return messages
class DashScopeImageSynthesisWrapper(DashScopeWrapperBase):
"""The model wrapper for DashScope Image Synthesis API, refer to
https://help.aliyun.com/zh/dashscope/developer-reference/quick-start-1
"""
model_type: str = "dashscope_image_synthesis"
def _register_default_metrics(self) -> None:
# Set monitor accordingly
# TODO: set quota to the following metrics
self.monitor.register(
self._metric("call_counter"),
metric_unit="times",
)
self.monitor.register(
self._metric("image_count"),
metric_unit="image",
)
def __call__(
self,
prompt: str,
save_local: bool = False,
**kwargs: Any,
) -> ModelResponse:
"""
Args:
prompt (`str`):
The prompt string to generate images from.
save_local: (`bool`, default `False`):
Whether to save the generated images locally, and replace
the returned image url with the local path.
**kwargs (`Any`):
The keyword arguments to DashScope Image Synthesis API,
e.g. `n`, `size`, etc. Please refer to
https://help.aliyun.com/zh/dashscope/developer-reference/api-details-9
for more detailed arguments.
Returns:
`ModelResponse`:
A list of image urls in image_urls field and the
raw response in raw field.
Note:
`parse_func`, `fault_handler` and `max_retries` are reserved
for `_response_parse_decorator` to parse and check the
response generated by model wrapper. Their usages are listed
as follows:
- `parse_func` is a callable function used to parse and
check the response generated by the model, which takes
the response as input.
- `max_retries` is the maximum number of retries when the
`parse_func` raise an exception.
- `fault_handler` is a callable function which is called
when the response generated by the model is invalid after
`max_retries` retries.
"""
# step1: prepare keyword arguments
kwargs = {**self.generate_args, **kwargs}
# step2: forward to generate response
response = dashscope.ImageSynthesis.call(
model=self.model_name,
prompt=prompt,
**kwargs,
)
if response.status_code != HTTPStatus.OK:
error_msg = (
f" Request id: {response.request_id},"
f" Status code: {response.status_code},"
f" error code: {response.code},"
f" error message: {response.message}."
)
raise RuntimeError(error_msg)
# step3: record the model api invocation if needed
self._save_model_invocation(
arguments={
"model": self.model_name,
"prompt": prompt,
**kwargs,
},
response=response,
)
# step4: update monitor accordingly
self.update_monitor(
call_counter=1,
**response.usage,
)
# step5: return response
images = response.output["results"]
# Get image urls as a list
urls = [_["url"] for _ in images]
if save_local:
# Return local url if save_local is True
urls = [file_manager.save_image(_) for _ in urls]
return ModelResponse(image_urls=urls, raw=response)
class DashScopeTextEmbeddingWrapper(DashScopeWrapperBase):
"""The model wrapper for DashScope Text Embedding API."""
model_type: str = "dashscope_text_embedding"
def _register_default_metrics(self) -> None:
# Set monitor accordingly
# TODO: set quota to the following metrics
self.monitor.register(
self._metric("call_counter"),
metric_unit="times",
)
self.monitor.register(
self._metric("total_tokens"),
metric_unit="token",
)
def __call__(
self,
texts: Union[list[str], str],
**kwargs: Any,
) -> ModelResponse:
"""Embed the messages with DashScope Text Embedding API.
Args:
texts (`list[str]` or `str`):
The messages used to embed.
**kwargs (`Any`):
The keyword arguments to DashScope Text Embedding API,
e.g. `text_type`. Please refer to
https://help.aliyun.com/zh/dashscope/developer-reference/api-details-15
for more detailed arguments.
Returns:
`ModelResponse`:
A list of embeddings in embedding field and the raw
response in raw field.
Note:
`parse_func`, `fault_handler` and `max_retries` are reserved
for `_response_parse_decorator` to parse and check the response
generated by model wrapper. Their usages are listed as follows:
- `parse_func` is a callable function used to parse and
check the response generated by the model, which takes the
response as input.
- `max_retries` is the maximum number of retries when the
`parse_func` raise an exception.
- `fault_handler` is a callable function which is called
when the response generated by the model is invalid after
`max_retries` retries.
"""
# step1: prepare keyword arguments
kwargs = {**self.generate_args, **kwargs}
# step2: forward to generate response
response = dashscope.TextEmbedding.call(
input=texts,
model=self.model_name,
**kwargs,
)
if response.status_code != HTTPStatus.OK:
error_msg = (
f" Request id: {response.request_id},"
f" Status code: {response.status_code},"
f" error code: {response.code},"
f" error message: {response.message}."
)
raise RuntimeError(error_msg)
# step3: record the model api invocation if needed
self._save_model_invocation(
arguments={
"model": self.model_name,
"input": texts,
**kwargs,
},
response=response,
)
# step4: update monitor accordingly
self.update_monitor(
call_counter=1,
**response.usage,
)
# step5: return response
return ModelResponse(
embedding=[_["embedding"] for _ in response.output["embeddings"]],
raw=response,
)
class DashScopeMultiModalWrapper(DashScopeWrapperBase):
"""The model wrapper for DashScope Multimodal API, refer to
https://help.aliyun.com/zh/dashscope/developer-reference/tongyi-qianwen-vl-api
"""
model_type: str = "dashscope_multimodal"
def _register_default_metrics(self) -> None:
# Set monitor accordingly
# TODO: set quota to the following metrics
self.monitor.register(
self._metric("call_counter"),
metric_unit="times",
)
self.monitor.register(
self._metric("prompt_tokens"),
metric_unit="token",
)
self.monitor.register(
self._metric("completion_tokens"),
metric_unit="token",
)
self.monitor.register(
self._metric("total_tokens"),
metric_unit="token",
)
def __call__(
self,
messages: list,
**kwargs: Any,
) -> ModelResponse:
"""Model call for DashScope MultiModal API.
Args:
messages (`list`):
A list of messages to process.
**kwargs (`Any`):
The keyword arguments to DashScope MultiModal API,
e.g. `stream`. Please refer to
https://help.aliyun.com/zh/dashscope/developer-reference/tongyi-qianwen-vl-plus-api
for more detailed arguments.
Returns:
`ModelResponse`:
The response text in text field, and the raw response in
raw field.
Note:
If involving image links, then the messages should be of the
following form:
.. code-block:: python
messages = [
{
"role": "system",
"content": [
{"text": "You are a helpful assistant."},
],
},
{
"role": "user",
"content": [
{"text": "What does this picture depict?"},
{"image": "http://example.com/image.jpg"},
],
},
]
Therefore, you should input a list matching the content value
above.
If only involving words, just input them.
`parse_func`, `fault_handler` and `max_retries` are reserved
for `_response_parse_decorator` to parse and check the response
generated by model wrapper. Their usages are listed as follows:
- `parse_func` is a callable function used to parse and
check the response generated by the model, which takes the
response as input.
- `max_retries` is the maximum number of retries when the
`parse_func` raise an exception.
- `fault_handler` is a callable function which is called
when the response generated by the model is invalid after
`max_retries` retries.
"""
# step1: prepare keyword arguments
kwargs = {**self.generate_args, **kwargs}
# step2: forward to generate response
response = dashscope.MultiModalConversation.call(
model=self.model_name,
messages=messages,
**kwargs,
)
# Unhandle code path here
# response could be a generator , if stream is yes
# suggest add a check here
if response.status_code != HTTPStatus.OK:
error_msg = (
f" Request id: {response.request_id},"
f" Status code: {response.status_code},"
f" error code: {response.code},"
f" error message: {response.message}."
)
raise RuntimeError(error_msg)
# step3: record the model api invocation if needed
self._save_model_invocation(
arguments={
"model": self.model_name,
"messages": messages,
**kwargs,
},
response=response,
)
# step4: update monitor accordingly
input_tokens = response.usage.get("input_tokens", 0)
image_tokens = response.usage.get("image_tokens", 0)
audio_tokens = response.usage.get("audio_tokens", 0)
output_tokens = response.usage.get("output_tokens", 0)
self.update_monitor(
call_counter=1,
prompt_tokens=input_tokens,
completion_tokens=output_tokens,
total_tokens=input_tokens
+ output_tokens
+ image_tokens
+ audio_tokens,
)
# step5: return response
content = response.output["choices"][0]["message"]["content"]
if isinstance(content, list):
content = content[0]["text"]
return ModelResponse(
text=content,
raw=response,
)
def format(
self,
*args: Union[Msg, Sequence[Msg]],
) -> List:
"""Format the messages for DashScope Multimodal API.
The multimodal API has the following requirements:
- The roles of messages must alternate between "user" and
"assistant".
- The message with the role "system" should be the first message
in the list.
- If the system message exists, then the second message must
have the role "user".
- The last message in the list should have the role "user".
- In each message, more than one figure is allowed.
With the above requirements, we format the messages as follows:
- If the first message is a system message, then we will keep it as
system prompt.
- We merge all messages into a dialogue history prompt in a single
message with the role "user".
- When there are multiple figures in the given messages, we will
attach it to the user message by order. Note if there are
multiple figures, this strategy may cause misunderstanding for
the model. For advanced solutions, developers are encouraged to
implement their own prompt engineering strategies.
The following is an example:
.. code-block:: python
prompt = model.format(
Msg(
"system",
"You're a helpful assistant",
role="system", url="figure1"
),
Msg(
"Bob",
"How about this picture?",
role="assistant", url="figure2"
),
Msg(
"user",
"It's wonderful! How about mine?",
role="user", image="figure3"
)
)
The prompt will be as follows:
.. code-block:: python
[
{
"role": "system",
"content": [
{"text": "You are a helpful assistant"},
{"image": "figure1"}
]
},
{
"role": "user",
"content": [
{"image": "figure2"},
{"image": "figure3"},
{
"text": (
"## Dialogue History\\n"
"Bob: How about this picture?\\n"
"user: It's wonderful! How about mine?"
)
},
]
}
]
Note:
In multimodal API, the url of local files should be prefixed with
"file://", which will be attached in this format function.
Args:
args (`Union[Msg, Sequence[Msg]]`):
The input arguments to be formatted, where each argument
should be a `Msg` object, or a list of `Msg` objects.
In distribution, placeholder is also allowed.
Returns:
`List[dict]`:
The formatted messages.
"""
# Parse all information into a list of messages
input_msgs = []
for _ in args:
if _ is None:
continue
if isinstance(_, Msg):
input_msgs.append(_)
elif isinstance(_, list) and all(isinstance(__, Msg) for __ in _):
input_msgs.extend(_)
else:
raise TypeError(
f"The input should be a Msg object or a list "
f"of Msg objects, got {type(_)}.",
)
messages = []
# record dialog history as a list of strings
dialogue = []
image_or_audio_dicts = []
for i, unit in enumerate(input_msgs):
if i == 0 and unit.role == "system":
# system prompt
content = self.convert_url(unit.url)
content.append({"text": _convert_to_str(unit.content)})
messages.append(
{
"role": unit.role,
"content": content,
},
)
else:
# text message
dialogue.append(
f"{unit.name}: {_convert_to_str(unit.content)}",
)
# image and audio
image_or_audio_dicts.extend(self.convert_url(unit.url))
dialogue_history = "\n".join(dialogue)
user_content_template = "## Dialogue History\n{dialogue_history}"
messages.append(
{
"role": "user",
"content": [
# Place the image or audio before the dialogue history
*image_or_audio_dicts,
{
"text": user_content_template.format(
dialogue_history=dialogue_history,
),
},
],
},
)
return messages
def convert_url(self, url: Union[str, Sequence[str], None]) -> List[dict]:
"""Convert the url to the format of DashScope API. Note for local
files, a prefix "file://" will be added.
Args:
url (`Union[str, Sequence[str], None]`):
A string of url of a list of urls to be converted.
Returns:
`List[dict]`:
A list of dictionaries with key as the type of the url
and value as the url. Only "image" and "audio" are supported.
"""
if url is None:
return []
if isinstance(url, str):
url_type = _guess_type_by_extension(url)
if url_type in ["audio", "image"]:
# Add prefix for local files
if os.path.exists(url):
url = "file://" + url
return [{url_type: url}]
else:
# skip unsupported url
logger.warning(
f"Skip unsupported url ({url_type}), "
f"expect image or audio.",
)
return []
elif isinstance(url, list):
dicts = []
for _ in url:
dicts.extend(self.convert_url(_))
return dicts
else:
raise TypeError(
f"Unsupported url type {type(url)}, " f"str or list expected.",
)

View File

@@ -0,0 +1,417 @@
# -*- coding: utf-8 -*-
"""Google Gemini model wrapper."""
import os
from abc import ABC
from collections.abc import Iterable
from typing import Sequence, Union, Any, List
from loguru import logger
from agentscope.message import Msg
from agentscope.models import ModelWrapperBase, ModelResponse
from agentscope.utils.tools import _convert_to_str
try:
import google.generativeai as genai
# This package will be installed when the google-generativeai is installed
import google.ai.generativelanguage as glm
except ImportError:
genai = None
glm = None
class GeminiWrapperBase(ModelWrapperBase, ABC):
"""The base class for Google Gemini model wrapper."""
_generation_method = None
"""The generation method used in `__call__` function, which is used to
filter models in `list_models` function."""
def __init__(
self,
config_name: str,
model_name: str,
api_key: str = None,
**kwargs: Any,
) -> None:
"""Initialize the wrapper for Google Gemini model.
Args:
model_name (`str`):
The name of the model.
api_key (`str`, defaults to `None`):
The api_key for the model. If it is not provided, it will be
loaded from environment variable.
"""
super().__init__(config_name=config_name)
# Test if the required package is installed
if genai is None:
raise ImportError(
"The google-generativeai package is not installed, "
"please install it first.",
)
# Load the api_key from argument or environment variable
api_key = api_key or os.environ.get("GOOGLE_API_KEY")
if api_key is None:
raise ValueError(
"Google api_key must be provided or set as an "
"environment variable.",
)
genai.configure(api_key=api_key, **kwargs)
self.model_name = model_name
self._register_default_metrics()
def _register_default_metrics(self) -> None:
"""Register the default metrics for the model."""
raise NotImplementedError(
"The method `_register_default_metrics` must be implemented.",
)
def list_models(self) -> Sequence:
"""List all available models for this API calling."""
support_models = list(genai.list_models())
if self.generation_method is None:
return support_models
else:
return [
_
for _ in support_models
if self._generation_method in _.supported_generation_methods
]
def __call__(self, *args: Any, **kwargs: Any) -> ModelResponse:
"""Processing input with the model."""
raise NotImplementedError(
f"Model Wrapper [{type(self).__name__}]"
f" is missing the the required `__call__`"
f" method.",
)
class GeminiChatWrapper(GeminiWrapperBase):
"""The wrapper for Google Gemini chat model, e.g. gemini-pro"""
model_type: str = "gemini_chat"
"""The type of the model, which is used in model configuration."""
generation_method = "generateContent"
"""The generation method used in `__call__` function."""
def __init__(
self,
config_name: str,
model_name: str,
api_key: str = None,
**kwargs: Any,
) -> None:
super().__init__(
config_name=config_name,
model_name=model_name,
api_key=api_key,
**kwargs,
)
# Create the generative model
self.model = genai.GenerativeModel(model_name, **kwargs)
def __call__(
self,
contents: Union[Sequence, str],
stream: bool = False,
**kwargs: Any,
) -> ModelResponse:
"""Generate response for the given contents.
Args:
contents (`Union[Sequence, str]`):
The content to generate response.
stream (`bool`, defaults to `False`):
Whether to use stream mode.
**kwargs:
The additional arguments for generating response.
Returns:
`ModelResponse`:
The response text in text field, and the raw response in raw
field.
"""
# step1: checking messages
if isinstance(contents, Iterable):
pass
elif not isinstance(contents, str):
logger.warning(
"The input content is not a string or a list of "
"messages, which may cause unexpected behavior.",
)
# step2: forward to generate response
# TODO: support response in stream mode
response = self.model.generate_content(
contents,
stream=stream,
**kwargs,
)
# step3: Check for candidates and handle accordingly
if (
not response.candidates[0].content
or not response.candidates[0].content.parts
or not response.candidates[0].content.parts[0].text
):
# If we cannot get the response text from the model
finish_reason = response.candidates[0].finish_reason
reasons = glm.Candidate.FinishReason
if finish_reason == reasons.STOP:
error_info = (
"Natural stop point of the model or provided stop "
"sequence."
)
elif finish_reason == reasons.MAX_TOKENS:
error_info = (
"The maximum number of tokens as specified in the request "
"was reached."
)
elif finish_reason == reasons.SAFETY:
error_info = (
"The candidate content was flagged for safety reasons."
)
elif finish_reason == reasons.RECITATION:
error_info = (
"The candidate content was flagged for recitation reasons."
)
elif finish_reason in [
reasons.FINISH_REASON_UNSPECIFIED,
reasons.OTHER,
]:
error_info = "Unknown error."
else:
error_info = "No information provided from Gemini API."
raise ValueError(
"The Google Gemini API failed to generate text response with "
f"the following finish reason: {error_info}\n"
f"YOUR INPUT: {contents}\n"
f"RAW RESPONSE FROM GEMINI API: {response}\n",
)
# step4: record the api invocation if needed
self._save_model_invocation(
arguments={
"contents": contents,
"stream": stream,
**kwargs,
},
response=str(response),
)
# step5: update monitor accordingly
token_prompt = self.model.count_tokens(contents).total_tokens
token_response = self.model.count_tokens(response.text).total_tokens
self.update_monitor(
call_counter=1,
completion_tokens=token_response,
prompt_tokens=token_prompt,
total_tokens=token_prompt + token_response,
)
# step6: return response
return ModelResponse(
text=response.text,
raw=response,
)
def _register_default_metrics(self) -> None:
"""Register the default metrics for the model."""
self.monitor.register(
self._metric("call_counter"),
metric_unit="times",
)
self.monitor.register(
self._metric("prompt_tokens"),
metric_unit="token",
)
self.monitor.register(
self._metric("completion_tokens"),
metric_unit="token",
)
self.monitor.register(
self._metric("total_tokens"),
metric_unit="token",
)
def format(
self,
*args: Union[Msg, Sequence[Msg]],
) -> List[dict]:
"""This function provide a basic prompting strategy for Gemini Chat
API in multi-party conversation, which combines all input into a
single string, and wrap it into a user message.
We make the above decision based on the following constraints of the
Gemini generate API:
1. In Gemini `generate_content` API, the `role` field must be either
`user` or `model`.
2. If we pass a list of messages to the `generate_content` API,
the `user` role must speak in the beginning and end of the
messages, and `user` and `model` must alternative. This prevents
us to build a multi-party conversations, where `model` may keep
speaking in different names.
The above information is updated to 2024/03/21. More information
about the Gemini `generate_content` API can be found in
https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/gemini
Based on the above considerations, we decide to combine all messages
into a single user message. This is a simple and straightforward
strategy, if you have any better ideas, pull request and
discussion are welcome in our GitHub repository
https://github.com/agentscope/agentscope!
Args:
args (`Union[Msg, Sequence[Msg]]`):
The input arguments to be formatted, where each argument
should be a `Msg` object, or a list of `Msg` objects.
In distribution, placeholder is also allowed.
Returns:
`List[dict]`:
A list with one user message.
"""
input_msgs = []
for _ in args:
if _ is None:
continue
if isinstance(_, Msg):
input_msgs.append(_)
elif isinstance(_, list) and all(isinstance(__, Msg) for __ in _):
input_msgs.extend(_)
else:
raise TypeError(
f"The input should be a Msg object or a list "
f"of Msg objects, got {type(_)}.",
)
# record dialog history as a list of strings
sys_prompt = None
dialogue = []
for i, unit in enumerate(input_msgs):
if i == 0 and unit.role == "system":
# system prompt
sys_prompt = _convert_to_str(unit.content)
else:
# Merge all messages into a dialogue history prompt
dialogue.append(
f"{unit.name}: {_convert_to_str(unit.content)}",
)
dialogue_history = "\n".join(dialogue)
if sys_prompt is None:
user_content_template = "## Dialogue History\n{dialogue_history}"
else:
user_content_template = (
"{sys_prompt}\n"
"\n"
"## Dialogue History\n"
"{dialogue_history}"
)
messages = [
{
"role": "user",
"parts": [
user_content_template.format(
sys_prompt=sys_prompt,
dialogue_history=dialogue_history,
),
],
},
]
return messages
class GeminiEmbeddingWrapper(GeminiWrapperBase):
"""The wrapper for Google Gemini embedding model,
e.g. models/embedding-001"""
model_type: str = "gemini_embedding"
"""The type of the model, which is used in model configuration."""
_generation_method = "embedContent"
"""The generation method used in `__call__` function."""
def __call__(
self,
content: Union[Sequence[Msg], str],
task_type: str = None,
title: str = None,
**kwargs: Any,
) -> ModelResponse:
"""Generate embedding for the given content. More detailed information
please refer to
https://ai.google.dev/tutorials/python_quickstart#use_embeddings
Args:
content (`Union[Sequence[Msg], str]`):
The content to generate embedding.
task_type (`str`, defaults to `None`):
The type of the task.
title (`str`, defaults to `None`):
The title of the content.
**kwargs:
The additional arguments for generating embedding.
Returns:
`ModelResponse`:
The response embedding in embedding field, and the raw response
in raw field.
"""
# step1: forward to generate response
response = genai.embed_content(
model=self.model_name,
content=content,
task_type=task_type,
title=title,
**kwargs,
)
# step2: record the api invocation if needed
self._save_model_invocation(
arguments={
"content": content,
"task_type": task_type,
"title": title,
**kwargs,
},
response=response,
)
# TODO: Up to 2023/03/11, the embedding model doesn't support to
# count tokens.
# step3: update monitor accordingly
self.update_monitor(call_counter=1)
return ModelResponse(
raw=response,
embedding=response["embedding"],
)
def _register_default_metrics(self) -> None:
"""Register the default metrics for the model."""
self.monitor.register(
self._metric("call_counter"),
metric_unit="times",
)

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