diff --git a/metagpt/actions/di/detect_intent.py b/metagpt/actions/di/detect_intent.py index 8f56f4ae8..1fb1c9089 100644 --- a/metagpt/actions/di/detect_intent.py +++ b/metagpt/actions/di/detect_intent.py @@ -1,12 +1,14 @@ from __future__ import annotations import asyncio +import re from enum import Enum from typing import Tuple from pydantic import BaseModel from metagpt.actions import Action +from metagpt.schema import Message class SOPItemDef(BaseModel): @@ -77,29 +79,41 @@ DETECT_PROMPT = """ # Intentions {intentions} # Task -Classify user requirement into one type of the above intentions, output the name of the intention directly. -Intention name: +Classify user requirement into one type of the above intentions, output the index of the intention directly. +Intention index: """ REQ_WITH_SOP = """ {user_requirement} -You should follow the following Standard Operating Procedure: +## Knowledge +To meet user requirements, the following standard operating procedure(SOP) must be used. +SOP descriptions cannot be modified; user requirements can only be appended to the end of corresponding steps. + {sop} """ class DetectIntent(Action): - async def run(self, user_requirement: str) -> Tuple[str, str]: - intentions = "\n".join([f"{si.type_name}: {si.value.description}" for si in SOPItem]) + async def run(self, with_message: Message, **kwargs) -> Tuple[str, str]: + user_requirement = with_message.content + mappings = {i + 1: si for i, si in enumerate(SOPItem)} + intentions = "\n".join([f"{i+1}. {si.type_name}: {si.value.description}" for i, si in enumerate(SOPItem)]) prompt = DETECT_PROMPT.format(user_requirement=user_requirement, intentions=intentions) - sop_type = await self._aask(prompt) - sop_type = sop_type.strip() - - sop = SOPItem.get_type(sop_type).sop + rsp = await self._aask(prompt) + match = re.search(r"\d+", rsp) + index = len(SOPItem) + 1 # 1-based + if match: + index = int(match.group()) # 1-based + sop = mappings[index].value.sop if index in mappings else None + sop_type = mappings[index].type_name if index in mappings else SOPItem.OTHER.type_name req_with_sop = ( - REQ_WITH_SOP.format(user_requirement=user_requirement, sop="\n".join(sop)) if sop else user_requirement + REQ_WITH_SOP.format( + user_requirement=user_requirement, sop="\n".join([f"{i+1}. {v}" for i, v in enumerate(sop)]) + ) + if sop + else user_requirement ) return req_with_sop, sop_type @@ -111,7 +125,7 @@ async def main(): detect_intent = DetectIntent() for user_requirement in user_requirements: - req_with_sop, sop_type = await detect_intent.run(user_requirement) + req_with_sop, sop_type = await detect_intent.run(Message(role="user", content=user_requirement)) print(req_with_sop) print(f"Detected SOP Type: {sop_type}") diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index 67a614d6f..b52616e37 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -70,6 +70,7 @@ class WriteTasks(Action): dependencies={system_design_doc.root_relative_path}, ) await self._update_requirements(task_doc) + await self.repo.resources.api_spec_and_task.save_pdf(doc=task_doc) return task_doc async def _run_new_tasks(self, context): diff --git a/metagpt/const.py b/metagpt/const.py index e4cebfd96..484987a03 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -135,3 +135,6 @@ AGGREGATION = "Aggregate" # Timeout USE_CONFIG_TIMEOUT = 0 # Using llm.timeout configuration. LLM_API_TIMEOUT = 300 + +# Assistant alias +ASSISTANT_ALIAS = "response" diff --git a/metagpt/roles/di/mgx.py b/metagpt/roles/di/mgx.py index b2caa930b..0fa7c77b6 100644 --- a/metagpt/roles/di/mgx.py +++ b/metagpt/roles/di/mgx.py @@ -2,7 +2,7 @@ # @Author : stellahong (stellahong@fuzhi.ai) # @Desc : import asyncio -from typing import Dict, List +from typing import Dict from metagpt.actions.di.detect_intent import DetectIntent from metagpt.logs import logger @@ -14,9 +14,9 @@ class MGX(DataInterpreter): use_intent: bool = True intents: Dict = {} - async def _detect_intent(self, user_msgs: List[Message] = None, **kwargs): + async def _detect_intent(self, user_msg: Message) -> str: todo = DetectIntent(context=self.context) - request_with_sop, sop_type = await todo.run(user_msgs) + request_with_sop, sop_type = await todo.run(user_msg) logger.info(f"{sop_type} {request_with_sop}") return request_with_sop @@ -24,10 +24,10 @@ class MGX(DataInterpreter): """first plan, then execute an action sequence, i.e. _think (of a plan) -> _act -> _act -> ... Use llm to come up with the plan dynamically.""" # create initial plan and update it until confirmation - goal = self.rc.memory.get()[-1].content # retreive latest user requirement + goal = self.rc.memory.get()[-1].content # retrieve latest user requirement if self.use_intent: # add mode user_message = Message(content=goal, role="user") - goal = await self._detect_intent(user_msgs=[user_message]) + goal = await self._detect_intent(user_message) logger.info(f"Goal is {goal}") await self.planner.update_plan(goal=goal) diff --git a/metagpt/tools/libs/software_development.py b/metagpt/tools/libs/software_development.py index acc3716b1..f8a409878 100644 --- a/metagpt/tools/libs/software_development.py +++ b/metagpt/tools/libs/software_development.py @@ -5,7 +5,7 @@ from __future__ import annotations from pathlib import Path from typing import Optional -from metagpt.const import BUGFIX_FILENAME, REQUIREMENT_FILENAME +from metagpt.const import ASSISTANT_ALIAS, BUGFIX_FILENAME, REQUIREMENT_FILENAME from metagpt.logs import ToolLogItem, log_tool_output from metagpt.schema import BugFixContext, Message from metagpt.tools.tool_registry import register_tool @@ -42,8 +42,10 @@ async def write_prd(idea: str, project_path: Optional[str | Path] = None) -> Pat from metagpt.context import Context from metagpt.roles import ProductManager + log_tool_output(output=[ToolLogItem(name=ASSISTANT_ALIAS, value=write_prd.__name__)], tool_name=write_prd.__name__) + ctx = Context() - if project_path: + if project_path and Path(project_path).exists(): ctx.config.project_path = Path(project_path) ctx.config.inc = True role = ProductManager(context=ctx) @@ -51,13 +53,21 @@ async def write_prd(idea: str, project_path: Optional[str | Path] = None) -> Pat await role.run(with_message=msg) outputs = [ - ToolLogItem(name="PRD File", value=str(ctx.repo.docs.prd.workdir / i)) + ToolLogItem(name="Intermedia PRD File", value=str(ctx.repo.docs.prd.workdir / i)) for i in ctx.repo.docs.prd.changed_files.keys() ] - for i in ctx.repo.resources.competitive_analysis.changed_files.keys(): - outputs.append( + outputs.extend( + [ + ToolLogItem(name="PRD File", value=str(ctx.repo.resources.prd.workdir / i)) + for i in ctx.repo.resources.prd.changed_files.keys() + ] + ) + outputs.extend( + [ ToolLogItem(name="Competitive Analysis", value=str(ctx.repo.resources.competitive_analysis.workdir / i)) - ) + for i in ctx.repo.resources.competitive_analysis.changed_files.keys() + ] + ) log_tool_output(output=outputs, tool_name=write_prd.__name__) return ctx.repo.docs.prd.workdir @@ -85,6 +95,10 @@ async def write_design(prd_path: str | Path) -> Path: from metagpt.context import Context from metagpt.roles import Architect + log_tool_output( + output=[ToolLogItem(name=ASSISTANT_ALIAS, value=write_design.__name__)], tool_name=write_design.__name__ + ) + ctx = Context() prd_path = Path(prd_path) project_path = (Path(prd_path) if not prd_path.is_file() else prd_path.parent) / "../.." @@ -132,6 +146,11 @@ async def write_project_plan(system_design_path: str | Path) -> Path: from metagpt.context import Context from metagpt.roles import ProjectManager + log_tool_output( + output=[ToolLogItem(name=ASSISTANT_ALIAS, value=write_project_plan.__name__)], + tool_name=write_project_plan.__name__, + ) + ctx = Context() system_design_path = Path(system_design_path) project_path = (system_design_path if not system_design_path.is_file() else system_design_path.parent) / "../.." @@ -141,9 +160,15 @@ async def write_project_plan(system_design_path: str | Path) -> Path: await role.run(with_message=Message(content="", cause_by=WriteDesign)) outputs = [ - ToolLogItem(name="Project Plan", value=str(ctx.repo.docs.task.workdir / i)) + ToolLogItem(name="Intermedia Project Plan", value=str(ctx.repo.docs.task.workdir / i)) for i in ctx.repo.docs.task.changed_files.keys() ] + outputs.extend( + [ + ToolLogItem(name="Project Plan", value=str(ctx.repo.resources.api_spec_and_task.workdir / i)) + for i in ctx.repo.resources.api_spec_and_task.changed_files.keys() + ] + ) log_tool_output(output=outputs, tool_name=write_project_plan.__name__) return ctx.repo.docs.task.workdir @@ -179,6 +204,10 @@ async def write_codes(task_path: str | Path, inc: bool = False) -> Path: from metagpt.context import Context from metagpt.roles import Engineer + log_tool_output( + output=[ToolLogItem(name=ASSISTANT_ALIAS, value=write_codes.__name__)], tool_name=write_codes.__name__ + ) + ctx = Context() ctx.config.inc = inc task_path = Path(task_path) @@ -222,6 +251,10 @@ async def run_qa_test(src_path: str | Path) -> Path: from metagpt.environment import Environment from metagpt.roles import QaEngineer + log_tool_output( + output=[ToolLogItem(name=ASSISTANT_ALIAS, value=run_qa_test.__name__)], tool_name=run_qa_test.__name__ + ) + ctx = Context() src_path = Path(src_path) project_path = (src_path if not src_path.is_file() else src_path.parent) / ".." @@ -270,6 +303,8 @@ async def fix_bug(project_path: str | Path, issue: str) -> Path: from metagpt.context import Context from metagpt.roles import Engineer + log_tool_output(output=[ToolLogItem(name=ASSISTANT_ALIAS, value=fix_bug.__name__)], tool_name=fix_bug.__name__) + ctx = Context() ctx.set_repo_dir(project_path) ctx.src_workspace = ctx.git_repo.workdir / ctx.git_repo.workdir.name @@ -325,11 +360,18 @@ async def git_archive(project_path: str | Path) -> str: """ from metagpt.context import Context + log_tool_output( + output=[ToolLogItem(name=ASSISTANT_ALIAS, value=git_archive.__name__)], tool_name=git_archive.__name__ + ) + ctx = Context() ctx.set_repo_dir(project_path) + files = " ".join(ctx.git_repo.changed_files.keys()) + outputs = [ToolLogItem(name="cmd", value=f"git add {files}")] + log_tool_output(output=outputs, tool_name=git_archive.__name__) ctx.git_repo.archive() - outputs = [ToolLogItem(name="Git Commit", value=str(ctx.repo.workdir))] + outputs = [ToolLogItem(name="cmd", value="git commit -m 'Archive'")] log_tool_output(output=outputs, tool_name=git_archive.__name__) return ctx.git_repo.log() @@ -358,6 +400,10 @@ async def import_git_repo(url: str) -> Path: from metagpt.actions.import_repo import ImportRepo from metagpt.context import Context + log_tool_output( + output=[ToolLogItem(name=ASSISTANT_ALIAS, value=import_git_repo.__name__)], tool_name=import_git_repo.__name__ + ) + ctx = Context() action = ImportRepo(repo_path=url, context=ctx) await action.run() diff --git a/tests/metagpt/actions/di/test_detect_intent.py b/tests/metagpt/actions/di/test_detect_intent.py index 7c9cf9eba..e555680ef 100644 --- a/tests/metagpt/actions/di/test_detect_intent.py +++ b/tests/metagpt/actions/di/test_detect_intent.py @@ -1,6 +1,7 @@ import pytest from metagpt.actions.di.detect_intent import DetectIntent +from metagpt.schema import Message SOFTWARE_DEV_REQ1 = """ I'd like to create a personalized website that features the 'Game of Life' simulation. @@ -51,5 +52,5 @@ git clone 'https://github.com/spec-first/connexion' and format to MetaGPT projec ) async def test_detect_intent(requirement, expected_intent_type): di = DetectIntent() - _, intent_type = await di.run(requirement) + _, intent_type = await di.run(Message(role="user", content=requirement)) assert intent_type == expected_intent_type