From 799dbd396eeff71e9e5a7ab30935685b2794c9a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Fri, 25 Aug 2023 21:10:14 +0800 Subject: [PATCH] feat: archive --- .well-known/skills.yaml | 17 ++++ metagpt/actions/action.py | 2 +- metagpt/actions/action_output.py | 6 +- metagpt/actions/talk_action.py | 32 +++++++ metagpt/learn/skill_loader.py | 38 ++++++++ metagpt/memory/brain_memory.py | 47 ++++++++++ metagpt/provider/openai_api.py | 2 + metagpt/roles/assistant.py | 143 +++++++++++++++++++++++++++++++ metagpt/roles/role.py | 6 ++ metagpt/schema.py | 3 +- 10 files changed, 291 insertions(+), 5 deletions(-) create mode 100644 .well-known/skills.yaml create mode 100644 metagpt/actions/talk_action.py create mode 100644 metagpt/learn/skill_loader.py create mode 100644 metagpt/memory/brain_memory.py create mode 100644 metagpt/roles/assistant.py diff --git a/.well-known/skills.yaml b/.well-known/skills.yaml new file mode 100644 index 000000000..5ccb8094b --- /dev/null +++ b/.well-known/skills.yaml @@ -0,0 +1,17 @@ +entities: + Assistant: + skills: + - name: text_to_speech + description: Text-to-speech + requisite: + - AZURE_TTS_SUBSCRIPTION_KEY + - AZURE_TTS_REGION + - name: text_to_image + description: Create a drawing based on the text. + requisite: + - OPENAI_API_KEY + - METAGPT_TEXT_TO_IMAGE_MODEL + - name: text_to_embedding + description: Convert the text into embeddings. + requisite: + - OPENAI_API_KEY diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index 899c2515c..86a6664ba 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -62,6 +62,6 @@ class Action(ABC): instruct_content = output_class(**parsed_data) return ActionOutput(content, instruct_content) - async def run(self, *args, **kwargs): + async def run(self, *args, **kwargs) -> str | ActionOutput | None: """Run action""" raise NotImplementedError("The run method should be implemented in a subclass.") diff --git a/metagpt/actions/action_output.py b/metagpt/actions/action_output.py index c0b88dcf9..6c812e7fe 100644 --- a/metagpt/actions/action_output.py +++ b/metagpt/actions/action_output.py @@ -6,16 +6,16 @@ @File : action_output """ -from typing import Dict, Type +from typing import Dict, Type, Optional from pydantic import BaseModel, create_model, root_validator, validator class ActionOutput: content: str - instruct_content: BaseModel + instruct_content: Optional[BaseModel] = None - def __init__(self, content: str, instruct_content: BaseModel): + def __init__(self, content: str, instruct_content: BaseModel=None): self.content = content self.instruct_content = instruct_content diff --git a/metagpt/actions/talk_action.py b/metagpt/actions/talk_action.py new file mode 100644 index 000000000..4275a1b9e --- /dev/null +++ b/metagpt/actions/talk_action.py @@ -0,0 +1,32 @@ +from metagpt.actions import Action, ActionOutput +from metagpt.logs import logger + + + +class TalkAction(Action): + def __init__(self, options, name: str = '', talk='', history_summary='', context=None, llm=None): + context = context or {} + context["talk"] = talk + context["history_summery"] = history_summary + super(TalkAction, self).__init__(options=options, name=name, context=context, llm=llm) + self._talk = talk + self._history_summary = history_summary + self._rsp = None + + @property + def prompt(self): + prompt = f"{self._history_summary}\n\n" + if self._history_summary != "": + prompt += "According to the historical conversation above, " + language = self.options.get("language", "Chinese") + prompt += f"Answer in {language}:\n {self._talk}" + return prompt + + async def run(self, *args, **kwargs) -> ActionOutput: + prompt = self.prompt + logger.info(prompt) + rsp = await self.llm.aask(msg=prompt, system_msgs=[]) + logger.info(rsp) + self._rsp = ActionOutput(content=rsp) + return self._rsp + diff --git a/metagpt/learn/skill_loader.py b/metagpt/learn/skill_loader.py new file mode 100644 index 000000000..eeca12871 --- /dev/null +++ b/metagpt/learn/skill_loader.py @@ -0,0 +1,38 @@ +from pathlib import Path +from typing import List, Dict + +import yaml +from pydantic import BaseModel + + +class Skill(BaseModel): + name: str + description: str + requisite: List[str] + + +class EntitySkills(BaseModel): + skills: List[Skill] + + +class SkillsDeclaration(BaseModel): + entities: Dict[str, EntitySkills] + + +class SkillLoader: + def __init__(self): + skill_file_name = Path(__file__).parent.parent.parent / ".well-known/skills.yaml" + with open(str(skill_file_name), 'r') as file: + skills = yaml.safe_load(file) + self._skills = SkillsDeclaration(**skills) + + def get_skill_list(self, entity_name: str = "Assistant"): + if not self._skills or entity_name not in self._skills.entities: + return {} + entity_skills = self._skills.entities.get(entity_name) + + description_to_name_mappings = {} + for s in entity_skills.skills: + description_to_name_mappings[s.description] = s.name + + return description_to_name_mappings diff --git a/metagpt/memory/brain_memory.py b/metagpt/memory/brain_memory.py new file mode 100644 index 000000000..97319859a --- /dev/null +++ b/metagpt/memory/brain_memory.py @@ -0,0 +1,47 @@ +from enum import Enum +from typing import List + +import pydantic + +from metagpt import Message + +class MessageType(Enum): + Talk = "TALK" + Solution = "SOLUTION" + Problem = "PROBLEM" + Skill = "SKILL" + Answer = "ANSWER" + + +class BrainMemory(pydantic.BaseModel): + history: List[Message] = [] + stack: List[Message] = [] + solution: List[Message] = [] + + + def add_talk(self, msg: Message): + msg.add_tag(MessageType.Talk.value) + self.history.append(msg) + + def add_answer(self, msg: Message): + msg.add_tag(MessageType.Answer.value) + self.history.append(msg) + + @property + def history_text(self): + if len(self.history) == 0: + return "" + texts = [m.content for m in self.history[:-1]] + return "\n".join(texts) + + def move_to_solution(self): + while len(self.history) > 1: + msg = self.history.pop() + self.solution.append(msg) + + @property + def last_talk(self): + if len(self.history) == 0 or not self.history[-1].is_contain_tags([MessageType.Talk.value]): + return "" + return self.history[-1].content + diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 48b7991dc..06a3154e8 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -313,6 +313,8 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): async def get_summary(self, text: str, max_words=20): """Generate text summary""" + if len(text) < max_words: + return text language = self._options.get("language", "English") command = f"Translate the above content into a {language} summary of less than {max_words} words." msg = text + "\n\n" + command diff --git a/metagpt/roles/assistant.py b/metagpt/roles/assistant.py new file mode 100644 index 000000000..fde011892 --- /dev/null +++ b/metagpt/roles/assistant.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023/8/7 +@Author : mashenquan +@File : fork_meta_role.py +@Desc : I am attempting to incorporate certain symbol concepts from UML into MetaGPT, enabling it to have the + ability to freely construct flows through symbol concatenation. Simultaneously, I am also striving to + make these symbols configurable and standardized, making the process of building flows more convenient. + For more about `fork` node in activity diagrams, see: `https://www.uml-diagrams.org/activity-diagrams.html` + This file defines a `fork` style meta role capable of generating arbitrary roles at runtime based on a + configuration file. +@Modified By: mashenquan, 2023/8/22. A definition has been provided for the return value of _think: returning false indicates that further reasoning cannot continue. + +""" +import asyncio +import re + +from metagpt.actions import ActionOutput +from metagpt.actions.talk_action import TalkAction +from metagpt.config import Config +from metagpt.learn.skill_loader import SkillLoader +from metagpt.logs import logger +from metagpt.memory.brain_memory import BrainMemory, MessageType +from metagpt.provider.openai_api import CostManager +from metagpt.roles import Role +from metagpt.schema import Message + +DEFAULT_MAX_TOKENS = 1500 +COMMAND_TOKENS = 500 + + +class Assistant(Role): + """解决通用问题的助手""" + + def __init__(self, options, cost_manager, name="Lily", profile="An assistant", goal="Help to solve problem", + constraints="Talk in {language}", desc="", *args, **kwargs): + super(Assistant, self).__init__(options=options, cost_manager=cost_manager, name=name, profile=profile, + goal=goal, constraints=constraints, desc=desc, *args, **kwargs) + self.memory = BrainMemory() + self.skills = SkillLoader() + + async def think(self) -> bool: + """Everything will be done part by part.""" + if self.memory.history_text != "": + self._refine_memory() + + + prompt = "" + history_text = self.memory.history_text + history_summary = "" + if history_text != "": + max_tokens = self.options.get("MAX_TOKENS", DEFAULT_MAX_TOKENS) + history_summary = await self._llm.get_summary(history_text, max_tokens - COMMAND_TOKENS) + prompt += history_summary + "\n\n" + prompt += "Analyze the conversation history above, in conjunction with the current sentence: \n{self.memory.last_talk}\n\n" + else: + prompt += f"Refer to this sentence:\n {self.memory.last_talk}\n" + skills = self.skills.get_skill_list() + for desc, name in skills.items(): + prompt += f"If want you to do {desc}, return `[SKILL]: {name}` brief and clear. For instance: [SKILL]: text_to_image\n" + if history_text != "": + prompt += "If the last sentence is not related to the conversation history above, return `[SOLUTION]: {title of the history conversation}` brief and clear. For instance: [SOLUTION]: Solution for distributing watermelon\n" + prompt += "If the preceding text presents a complete question and solution, rewrite and return `[SOLUTION]: {problem}` brief and clear. For instance: [SOLUTION]: Solution for distributing watermelon\n" + prompt += "If the preceding text presents an unresolved issue and its corresponding discussion, rewrite and return `[PROBLEM]: {problem}` brief and clear. For instance: [PROBLEM]: How to distribute watermelon?\n" + prompt += "Otherwise, rewrite and return `[TALK]: {talk}` brief and clear. For instance: [TALK]: distribute watermelon" + logger.info(prompt) + rsp = await self._llm.aask(prompt, []) + logger.info(rsp) + return await self._plan(rsp, history_summary=history_summary) + + async def act(self) -> ActionOutput: + result = await self._rc.todo.run(**self._options) + if not result: + return None + if isinstance(result, str): + msg = Message(content=result) + output = ActionOutput(content=result) + else: + msg = Message(content=result.content, instruct_content=result.instruct_content, + cause_by=type(self._rc.todo)) + output = result + self.memory.add_answer(msg) + return output + + async def talk(self, text): + self.memory.add_talk(Message(content=text, tags=set([MessageType.Talk.value]))) + + async def _plan(self, rsp, **kwargs) -> bool: + skill, text = Assistant.extract_info(rsp) + handlers = { + MessageType.Talk.value: self.talk_handler, + MessageType.Problem.value: self.problem_handler, + MessageType.Solution.value: self.solution_handler, + MessageType.Skill.value: self.skill_handler, + } + handler = handlers.get(skill, self.talk_handler) + return await handler(text, **kwargs) + + @staticmethod + def extract_info(input_string): + pattern = r'\[([A-Z]+)\]:\s*(.+)' + match = re.match(pattern, input_string) + if match: + return match.group(1), match.group(2) + else: + return None, input_string + + async def problem_handler(self, text, **kwargs) -> bool: + action = TalkAction(options=self.options, talk=text, llm=self._llm, **kwargs) + self.add_to_do(action) + return True + + async def solution_handler(self, text, **kwargs) -> bool: + self.memory.move_to_solution() # 问题解决后及时清空内存 + action = TalkAction(options=self.options, talk=text, history_summary="", **kwargs) + self.add_to_do(action) + + async def skill_handler(self, text, **kwargs) -> bool: + pass + + async def _refine_memory(self): + + +async def main(): + options = Config().runtime_options + cost_manager = CostManager(**options) + topic = "dataiku vs. datarobot" + role = Assistant(options=options, cost_manager=cost_manager, language="Chinese") + await role.talk(topic) + while True: + has_action = await role.think() + if not has_action: + break + msg = await role.act() + print(msg) + # 获取用户终端输入 + talk = input("You: ") + await role.talk(talk) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 493c172ae..1bb73f884 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -320,3 +320,9 @@ class Role: for k, v in merged_opts.items(): value = value.replace("{" + f"{k}" + "}", str(v)) return value + + def add_action(self, act): + self._actions.append(act) + + def add_to_do(self, act): + self._rc.todo = act \ No newline at end of file diff --git a/metagpt/schema.py b/metagpt/schema.py index 4c577fd7b..e1cd011c6 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -10,7 +10,7 @@ from __future__ import annotations from dataclasses import dataclass, field from enum import Enum -from typing import Type, TypedDict, Set, Optional +from typing import Type, TypedDict, Set, Optional, List from pydantic import BaseModel @@ -98,6 +98,7 @@ class AIMessage(Message): super().__init__(content, 'assistant') + if __name__ == '__main__': test_content = 'test_message' msgs = [