赞
踩
3.3 实现一个单动作Agent
下面将演示如何使用MetaGPT实现一个单动作智能体,该智能体可以根据用户的需求写出一个python函数,比如:"write a function that calculates the sum of a list"。
步骤如下:
0. 环境配置:略,请参考第一章教程。
1. 准备demo代码:在metagpt目录下创建single_act_agent.py,并复制以下代码:
- import re
- import asyncio
- from metagpt.actions import Action
- from metagpt.roles import Role
- from metagpt.schema import Message
- from metagpt.logs import logger
-
- class SimpleWriteCode(Action):
-
- PROMPT_TEMPLATE : str = """
- Write a python function that can {instruction} and provide two runnable test cases.
- Return ```python your_code_here ``` with NO other texts,
- your code:
- """
-
- name: str = "SimpleWriteCode"
-
- async def run(self, instruction: str):
- prompt = self.PROMPT_TEMPLATE.format(instruction=instruction)
- rsp = await self._aask(prompt)
- code_text = SimpleWriteCode.parse_code(rsp)
- return code_text
-
- @staticmethod
- def parse_code(rsp):
- pattern = r'```python(.*)```'
- match = re.search(pattern, rsp, re.DOTALL)
- code_text = match.group(1) if match else rsp
- return code_text
-
- class SimpleCoder(Role):
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self._init_actions([SimpleWriteCode])
-
- async def _act(self) -> Message:
- logger.info(f"{self._setting}: ready to {self.rc.todo}")
- todo = self.rc.todo # todo will be SimpleWriteCode()
-
- msg = self.get_memories(k=1)[0] # find the most recent messages
-
- code_text = await todo.run(msg.content)
- msg = Message(content=code_text, role=self.profile,
- cause_by=type(todo))
-
- return msg
-
- async def main():
- msg = "write a function that calculates the sum of a list"
- role = SimpleCoder()
- logger.info(msg)
- result = await role.run(msg)
- logger.info(result)
-
- asyncio.run(main())
2. 运行demo:
python single_act_agent.py
结果如下:下图中Agent为我们生成了一个python函数以及两个测试用例来实现数组求和的需求。
测试一个难度更高的需求:返回数组中的最大的负数,返回-1如果数组中没有负数。
步骤:将single_act_agent.py中的main()函数下的msg改为:
msg = "write a function that returns the largest negative number of a list, return -1 if the list does not contain any negative number"
运行single_act_agent.py,agent可以正确的写出解决上述需求的python代码:
详见开篇项目链接中的3.3节,在这里简单对demo代码进行介绍。
首先我们需要实现一个Action动作类,即SimpleWriteCode类,该类下的run函数包括了以下几个部分:
1. 接收用户的需求并组装成prompt
2. 将prompt送入LLM并得到返回结果
3. 从返回结果中提取出python代码部分
- import re
- import asyncio
- from metagpt.actions import Action
-
- class SimpleWriteCode(Action):
-
- PROMPT_TEMPLATE: str = """
- Write a python function that can {instruction} and provide two runnnable test cases.
- Return ```python your_code_here ``` with NO other texts,
- your code:
- """
-
- name: str = "SimpleWriteCode"
-
- async def run(self, instruction: str):
- prompt = self.PROMPT_TEMPLATE.format(instruction=instruction)
- rsp = await self._aask(prompt)
- code_text = SimpleWriteCode.parse_code(rsp)
- return code_text
-
- @staticmethod
- def parse_code(rsp):
- pattern = r'```python(.*)```'
- match = re.search(pattern, rsp, re.DOTALL)
- code_text = match.group(1) if match else rsp
- return code_text
在完成SimpleWriteCode动作类的构建之后,我们还需构建一个Role角色,即SimpleCoder类。该类中我们需要定义init和act两个函数。
init函数将上一步创建的SimpleWriteCode动作类进行装载。
act函数先是通过get_memories函数获取到用户的需求,进行调用todo.run函数执行SimpleWriteCode这个动作,并拿取返回的python代码文本,最后将文本打包成Message格式进行返回。
- from metagpt.roles import Role
- from metagpt.schema import Message
- from metagpt.logs import logger
-
- class SimpleCoder(Role):
-
- name: str = "Alice"
- profile: str = "SimpleCoder"
-
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self._init_actions([SimpleWriteCode])
-
- async def _act(self) -> Message:
- logger.info(f"{self._setting}: ready to {self.rc.todo}")
- todo = self.rc.todo # todo will be SimpleWriteCode()
-
- msg = self.get_memories(k=1)[0] # find the most recent messages
-
- code_text = await todo.run(msg.content)
- msg = Message(content=code_text, role=self.profile, cause_by=type(todo))
-
- return msg
3.3章我们用一个动作(Action)和一个角色(Role)实现了一个单动做Agent。
本章我们用两个动作(Action)和一个角色(Role)来实现一个多动作Agent。该智能体可以实现两个动作,分别是:
(1)根据用户需求生成python代码(同3.3)
(2)执行生成的代码
demo操作如下:
1. 在metagpt目录下新建multi_act_agent.py文件,并复制以下代码:
- import os
- import re
- import subprocess
- import asyncio
-
- import fire
- import sys
- from metagpt.llm import LLM
- from metagpt.actions import Action
- from metagpt.roles import Role
- from metagpt.schema import Message
- from metagpt.logs import logger
-
- class SimpleWriteCode(Action):
-
- PROMPT_TEMPLATE :str = """
- Write a python function that can {instruction} and provide two runnnable test cases.
- Return ```python your_code_here ``` with NO other texts,
- your code:
- """
-
- name: str = "SimpleWriteCode"
-
- async def run(self, instruction: str):
- prompt = self.PROMPT_TEMPLATE.format(instruction=instruction)
- rsp = await self._aask(prompt)
- code_text = SimpleWriteCode.parse_code(rsp)
- return code_text
-
- @staticmethod
- def parse_code(rsp):
- pattern = r'```python(.*)```'
- match = re.search(pattern, rsp, re.DOTALL)
- code_text = match.group(1) if match else rsp
- return code_text
-
- class SimpleRunCode(Action):
-
- name: str = "SimpleRunCode"
-
- async def run(self, code_text: str):
- result = subprocess.run([sys.executable, "-c", code_text], capture_output=True, text=True)
- code_result = result.stdout
- logger.info(f"{code_result=}")
- return code_result
-
- class RunnableCoder(Role):
-
- name: str = "Alice"
- profile: str = "RunnableCoder"
-
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self._init_actions([SimpleWriteCode, SimpleRunCode])
- self._set_react_mode(react_mode="by_order")
-
- async def _act(self) -> Message:
- logger.info(f"{self._setting}: ready to {self.rc.todo}")
- # By choosing the Action by order under the hood
- # todo will be first SimpleWriteCode() then SimpleRunCode()
- todo = self.rc.todo
-
- msg = self.get_memories(k=1)[0] # find the most k recent messagesA
- result = await todo.run(msg.content)
-
- msg = Message(content=result, role=self.profile, cause_by=type(todo))
- self.rc.memory.add(msg)
- return msg
-
- async def main():
- msg = "write a function that calculates the sum of a list"
- role = RunnableCoder()
- logger.info(msg)
- result = await role.run(msg)
- logger.info(result)
-
- asyncio.run(main())
2. 运行该文件,demo中智能体可以根据需求生成代码,并在两个测试用例中执行。
详细解读请参考文章顶部的项目链接中的3.4节,我们这里只做摘要解读。
代码分为两个Action类,一个Role类和一个main函数。
1. Action类SimpleWriteCode同3.3章,这里省略。
2. Action类SimpleRunCode作用是执行SimpleWriteCode生成的代码
- class SimpleRunCode(Action):
-
- name: str = "SimpleRunCode"
-
- async def run(self, code_text: str):
- result = subprocess.run([sys.executable, "-c", code_text], capture_output=True, text=True)
- code_result = result.stdout
- logger.info(f"{code_result=}")
- return code_result
3. Role类RunnableCoder类似3.3章,区别是3.3章单动作Agent的init函数中只初始化了一个Action,而本章节在Role的init函数中加入了前面定义的两个Action,并通过set_react_mode函数来指定Action的执行顺序
- class RunnableCoder(Role):
-
- name: str = "Alice"
- profile: str = "RunnableCoder"
-
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self._init_actions([SimpleWriteCode, SimpleRunCode])
- self._set_react_mode(react_mode="by_order")
-
- async def _act(self) -> Message:
- logger.info(f"{self._setting}: ready to {self.rc.todo}")
- # By choosing the Action by order under the hood
- # todo will be first SimpleWriteCode() then SimpleRunCode()
- todo = self.rc.todo
-
- msg = self.get_memories(k=1)[0] # find the most k recent messagesA
- result = await todo.run(msg.content)
-
- msg = Message(content=result, role=self.profile, cause_by=type(todo))
- self.rc.memory.add(msg)
- return msg
技术文档助手(Tutorial Assistant)可以根据用户输入的主题(例如:摄影教程)生成一份教学文档。文档不是一次性由LLM生成,而是由Agent分步骤执行。Agent首先根据用户输入的主题生成一份大纲,然后再在大纲内的各个主题下生成具体教学内容。
demo演示:
在metagpt目录下创建tutorial_assistant.py文件,并复制以下代码:
(注意:本文在原教程代码上进行了修改,将三处language从Chinese改为English,即生成结果为英文)
- from datetime import datetime
- from typing import Dict
- import asyncio
- from metagpt.actions.write_tutorial import WriteDirectory, WriteContent
- from metagpt.const import TUTORIAL_PATH
- from metagpt.logs import logger
- from metagpt.roles.role import Role, RoleReactMode
- from metagpt.schema import Message
- from metagpt.utils.file import File
-
- from typing import Dict
-
- from metagpt.actions import Action
- from metagpt.prompts.tutorial_assistant import DIRECTORY_PROMPT, CONTENT_PROMPT
- from metagpt.utils.common import OutputParser
-
- class WriteDirectory(Action):
- """Action class for writing tutorial directories.
- Args:
- name: The name of the action.
- language: The language to output, default is "Chinese".
- """
-
- name: str = "WriteDirectory"
- language: str = "English"
-
- async def run(self, topic: str, *args, **kwargs) -> Dict:
- """Execute the action to generate a tutorial directory according to the topic.
- Args:
- topic: The tutorial topic.
- Returns:
- the tutorial directory information, including {"title": "xxx", "directory": [{"dir 1": ["sub dir 1", "sub dir 2"]}]}.
- """
- COMMON_PROMPT = """
- You are now a seasoned technical professional in the field of the internet.
- We need you to write a technical tutorial with the topic "{topic}".
- """
-
- DIRECTORY_PROMPT = COMMON_PROMPT + """
- Please provide the specific table of contents for this tutorial, strictly following the following requirements:
- 1. The output must be strictly in the specified language, {language}.
- 2. Answer strictly in the dictionary format like {{"title": "xxx", "directory": [{{"dir 1": ["sub dir 1", "sub dir 2"]}}, {{"dir 2": ["sub dir 3", "sub dir 4"]}}]}}.
- 3. The directory should be as specific and sufficient as possible, with a primary and secondary directory.The secondary directory is in the array.
- 4. Do not have extra spaces or line breaks.
- 5. Each directory title has practical significance.
- """
- prompt = DIRECTORY_PROMPT.format(topic=topic, language=self.language)
- resp = await self._aask(prompt=prompt)
- return OutputParser.extract_struct(resp, dict)
-
- class WriteContent(Action):
- """Action class for writing tutorial content.
- Args:
- name: The name of the action.
- directory: The content to write.
- language: The language to output, default is "Chinese".
- """
-
- name: str = "WriteContent"
- directory: dict = dict()
- language: str = "English"
-
- async def run(self, topic: str, *args, **kwargs) -> str:
- """Execute the action to write document content according to the directory and topic.
- Args:
- topic: The tutorial topic.
- Returns:
- The written tutorial content.
- """
- COMMON_PROMPT = """
- You are now a seasoned technical professional in the field of the internet.
- We need you to write a technical tutorial with the topic "{topic}".
- """
- CONTENT_PROMPT = COMMON_PROMPT + """
- Now I will give you the module directory titles for the topic.
- Please output the detailed principle content of this title in detail.
- If there are code examples, please provide them according to standard code specifications.
- Without a code example, it is not necessary.
- The module directory titles for the topic is as follows:
- {directory}
- Strictly limit output according to the following requirements:
- 1. Follow the Markdown syntax format for layout.
- 2. If there are code examples, they must follow standard syntax specifications, have document annotations, and be displayed in code blocks.
- 3. The output must be strictly in the specified language, {language}.
- 4. Do not have redundant output, including concluding remarks.
- 5. Strict requirement not to output the topic "{topic}".
- """
- prompt = CONTENT_PROMPT.format(
- topic=topic, language=self.language, directory=self.directory)
- return await self._aask(prompt=prompt)
-
- class TutorialAssistant(Role):
- """Tutorial assistant, input one sentence to generate a tutorial document in markup format.
- Args:
- name: The name of the role.
- profile: The role profile description.
- goal: The goal of the role.
- constraints: Constraints or requirements for the role.
- language: The language in which the tutorial documents will be generated.
- """
-
- name: str = "Stitch"
- profile: str = "Tutorial Assistant"
- goal: str = "Generate tutorial documents"
- constraints: str = "Strictly follow Markdown's syntax, with neat and standardized layout"
- language: str = "English"
-
- topic: str = ""
- main_title: str = ""
- total_content: str = ""
-
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self._init_actions([WriteDirectory(language=self.language)])
- self._set_react_mode(react_mode=RoleReactMode.BY_ORDER.value)
-
-
- async def _think(self) -> None:
- """Determine the next action to be taken by the role."""
- logger.info(self.rc.state)
- logger.info(self,)
- if self.rc.todo is None:
- self._set_state(0)
- return
-
- if self.rc.state + 1 < len(self.states):
- self._set_state(self.rc.state + 1)
- else:
- self.rc.todo = None
-
- async def _handle_directory(self, titles: Dict) -> Message:
- """Handle the directories for the tutorial document.
- Args:
- titles: A dictionary containing the titles and directory structure,
- such as {"title": "xxx", "directory": [{"dir 1": ["sub dir 1", "sub dir 2"]}]}
- Returns:
- A message containing information about the directory.
- """
- self.main_title = titles.get("title")
- directory = f"{self.main_title}\n"
- self.total_content += f"# {self.main_title}"
- actions = list()
- for first_dir in titles.get("directory"):
- actions.append(WriteContent(
- language=self.language, directory=first_dir))
- key = list(first_dir.keys())[0]
- directory += f"- {key}\n"
- for second_dir in first_dir[key]:
- directory += f" - {second_dir}\n"
- self._init_actions(actions)
- self.rc.todo = None
- return Message(content=directory)
-
- async def _act(self) -> Message:
- """Perform an action as determined by the role.
- Returns:
- A message containing the result of the action.
- """
- todo = self.rc.todo
- if type(todo) is WriteDirectory:
- msg = self.rc.memory.get(k=1)[0]
- self.topic = msg.content
- resp = await todo.run(topic=self.topic)
- logger.info(resp)
- return await self._handle_directory(resp)
- resp = await todo.run(topic=self.topic)
- logger.info(resp)
- if self.total_content != "":
- self.total_content += "\n\n\n"
- self.total_content += resp
- return Message(content=resp, role=self.profile)
-
- async def _react(self) -> Message:
- """Execute the assistant's think and actions.
- Returns:
- A message containing the final result of the assistant's actions.
- """
- while True:
- await self._think()
- if self.rc.todo is None:
- break
- msg = await self._act()
- root_path = TUTORIAL_PATH / datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
- await File.write(root_path, f"{self.main_title}.md", self.total_content.encode('utf-8'))
- return msg
-
- async def main():
- msg = "摄影教程"
- role = TutorialAssistant()
- logger.info(msg)
- result = await role.run(msg)
- logger.info(result)
-
- asyncio.run(main())
运行python代码,结果如下:
代码解析:
详细解析请参考文章顶部项目地址中3.5章内容,这里仅对代码结构进行概述。
代码由以下及个类组成:
1. Action类WriteDirectory:根据用户输入主题,生成大纲
2. Action类WriteContent: 根据大纲生成教程细节
3. Role类TutorialAssistant:在内部改写了_think,_handle_directory,_act和_react函数,实现了运转流程的自定义。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。