diff --git a/metagpt/actions/intent_detect.py b/metagpt/actions/intent_detect.py index 6d13048b1c..ee7a01e42f 100644 --- a/metagpt/actions/intent_detect.py +++ b/metagpt/actions/intent_detect.py @@ -246,6 +246,7 @@ async def _get_sops(self): async def _merge(self): self.result = IntentDetectResult(clarifications=self._dialog_intentions.clarifications) distinct = {} + # Consolidate intentions under the same SOP. for i in self._intent_to_sops: if i.sop_index == 0: # 1-based index refs = self._get_intent_ref(i.intent) @@ -260,12 +261,14 @@ async def _merge(self): if len(intents) > 1: merge_intents[sop_index] = intents continue + # Merge single intention refs = self._get_intent_ref(intents[0]) item = IntentDetectIntentionSOP(intention=IntentDetectIntentionRef(intent=intents[0], refs=refs)) sop_index = intent_to_sops.get(intents[0]) item.sop = SOP_CONFIG[sop_index - 1] # 1-based index self.result.intentions.append(item) + # Merge repetitive intentions into one for sop_index, intents in merge_intents.items(): intent_ref = IntentDetectIntentionRef(intent="\n".join(intents), refs=[]) for i in intents: @@ -338,6 +341,7 @@ async def _get_sops(self): async def _merge(self): self.result = IntentDetectResult(clarifications=[]) distinct = {} + # Consolidate intentions under the same SOP. for i in self._intent_to_sops: if i.sop_index == 0: # 1-based index ref = self._get_intent_ref(i.intent) @@ -352,6 +356,7 @@ async def _merge(self): if len(intents) > 1: merge_intents[sop_index] = intents continue + # Merge single intention ref = self._get_intent_ref(intents[0]) item = IntentDetectIntentionSOP(intention=IntentDetectIntentionRef(intent=intents[0], refs=[ref])) sop_index = intent_to_sops.get(intents[0]) # 1-based @@ -359,6 +364,7 @@ async def _merge(self): item.sop = SOP_CONFIG[sop_index - 1] # 1-based index self.result.intentions.append(item) + # Merge repetitive intentions into one for sop_index, intents in merge_intents.items(): intent_ref = IntentDetectIntentionRef(intent="\n".join(intents), refs=[]) for i in intents: diff --git a/metagpt/logs.py b/metagpt/logs.py index e134afca37..c776174364 100644 --- a/metagpt/logs.py +++ b/metagpt/logs.py @@ -11,10 +11,34 @@ import sys from datetime import datetime from functools import partial +from typing import List from loguru import logger as _logger +from pydantic import BaseModel, Field from metagpt.const import METAGPT_ROOT +from metagpt.schema import BaseEnum + + +class ToolOutputItem(BaseModel): + type_: str = Field(alias="type", default="str", description="Data type of `value` field.") + name: str + value: str + + +class ToolName(str, BaseEnum): + Terminal = "Terminal" + Plan = "Plan" + Browser = "Browser" + Files = "Files" + WritePRD = "WritePRD" + WriteDesign = "WriteDesign" + WriteProjectPlan = "WriteProjectPlan" + WriteCode = "WriteCode" + WriteUntTest = "WriteUntTest" + FixBug = "FixBug" + GitArchive = "GitArchive" + ImportRepo = "ImportRepo" def define_log_level(print_level="INFO", logfile_level="DEBUG", name: str = None): @@ -36,9 +60,13 @@ def log_llm_stream(msg): _llm_stream_log(msg) -def log_tool_output(output: dict, tool_name: str = ""): +def log_tool_output(output: ToolOutputItem | List[ToolOutputItem], tool_name: str = ""): """interface for logging tool output, can be set to log tool output in different ways to different places with set_tool_output_logfunc""" - _tool_output_log(output) + if not _tool_output_log or not output: + return + + outputs = output if isinstance(output, list) else [output] + _tool_output_log(output=[i.model_dump() for i in outputs], tool_name=tool_name) def set_llm_stream_logfunc(func): diff --git a/metagpt/roles/di/mgx.py b/metagpt/roles/di/mgx.py index d7618778a7..5e2d897a2d 100644 --- a/metagpt/roles/di/mgx.py +++ b/metagpt/roles/di/mgx.py @@ -22,9 +22,11 @@ async def _intent_detect(self, user_msgs: List[Message] = None, **kwargs): # Extract intent and sop prompt intention_ref = "" for i in todo.result.intentions: + if not intention_ref: + intention_ref = "\n".join(i.intention.refs) if not i.sop: continue - intention_ref = "\n".join(i.intention.refs) + self.intents[intention_ref] = i.sop.sop logger.debug(f"refs: {intention_ref}, sop: {i.sop.sop}") sop_str = "\n".join([f"- {i}" for i in i.sop.sop]) diff --git a/metagpt/schema.py b/metagpt/schema.py index 071518d62e..7b50815f46 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -21,6 +21,7 @@ import uuid from abc import ABC from asyncio import Queue, QueueEmpty, wait_for +from enum import Enum from json import JSONDecodeError from pathlib import Path from typing import Any, Dict, Iterable, List, Optional, Type, TypeVar, Union @@ -785,3 +786,26 @@ def load_dot_class_info(cls, dot_class_info: DotClassInfo) -> UMLClassView: method.return_type = i.return_args.type_ class_view.methods.append(method) return class_view + + +class BaseEnum(Enum): + """Base class for enums.""" + + def __new__(cls, value, desc=None): + """ + Construct an instance of the enum member. + + Args: + cls: The class. + value: The value of the enum member. + desc: The description of the enum member. Defaults to None. + """ + if issubclass(cls, str): + obj = str.__new__(cls, value) + elif issubclass(cls, int): + obj = int.__new__(cls, value) + else: + obj = object.__new__(cls) + obj._value_ = value + obj.desc = desc + return obj diff --git a/metagpt/tools/libs/software_development.py b/metagpt/tools/libs/software_development.py index de05eacf95..92ac51f320 100644 --- a/metagpt/tools/libs/software_development.py +++ b/metagpt/tools/libs/software_development.py @@ -6,6 +6,7 @@ from typing import Optional from metagpt.const import BUGFIX_FILENAME, REQUIREMENT_FILENAME +from metagpt.logs import ToolName, ToolOutputItem, log_tool_output from metagpt.schema import BugFixContext, Message from metagpt.tools.tool_registry import register_tool from metagpt.utils.common import any_to_str @@ -48,6 +49,17 @@ async def write_prd(idea: str, project_path: Optional[str | Path] = None) -> Pat role = ProductManager(context=ctx) msg = await role.run(with_message=Message(content=idea, cause_by=UserRequirement)) await role.run(with_message=msg) + + outputs = [ + ToolOutputItem(name="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( + ToolOutputItem(name="Competitive Analysis", value=str(ctx.repo.resources.competitive_analysis.workdir / i)) + ) + log_tool_output(output=outputs, tool_name=ToolName.WritePRD) + return ctx.repo.docs.prd.workdir @@ -79,6 +91,21 @@ async def write_design(prd_path: str | Path) -> Path: role = Architect(context=ctx) await role.run(with_message=Message(content="", cause_by=WritePRD)) + + outputs = [ + ToolOutputItem(name="Intermedia Design File", value=str(ctx.repo.docs.system_design.workdir / i)) + for i in ctx.repo.docs.system_design.changed_files.keys() + ] + for i in ctx.repo.resources.system_design.changed_files.keys(): + outputs.append(ToolOutputItem(name="Design File", value=str(ctx.repo.resources.system_design.workdir / i))) + for i in ctx.repo.resources.data_api_design.changed_files.keys(): + outputs.append( + ToolOutputItem(name="Class Diagram File", value=str(ctx.repo.resources.data_api_design.workdir / i)) + ) + for i in ctx.repo.resources.seq_flow.changed_files.keys(): + outputs.append(ToolOutputItem(name="Sequence Diagram File", value=str(ctx.repo.resources.seq_flow.workdir / i))) + log_tool_output(output=outputs, tool_name=ToolName.WriteDesign) + return ctx.repo.docs.system_design.workdir @@ -110,6 +137,13 @@ async def write_project_plan(system_design_path: str | Path) -> Path: role = ProjectManager(context=ctx) await role.run(with_message=Message(content="", cause_by=WriteDesign)) + + outputs = [ + ToolOutputItem(name="Project Plan", value=str(ctx.repo.docs.task.workdir / i)) + for i in ctx.repo.docs.task.changed_files.key() + ] + log_tool_output(output=outputs, tool_name=ToolName.WriteProjectPlan) + return ctx.repo.docs.task.workdir @@ -153,6 +187,13 @@ async def write_codes(task_path: str | Path, inc: bool = False) -> Path: me = {any_to_str(role), role.name} while me.intersection(msg.send_to): msg = await role.run(with_message=msg) + + outputs = [ + ToolOutputItem(name="Source File", value=str(ctx.repo.srcs.workdir / i)) + for i in ctx.repo.srcs.changed_files.keys() + ] + log_tool_output(output=outputs, tool_name=ToolName.WriteCode) + return ctx.repo.srcs.workdir @@ -192,6 +233,13 @@ async def run_qa_test(src_path: str | Path) -> Path: while not env.is_idle: await env.run() + + outputs = [ + ToolOutputItem(name="Unit Test File", value=str(ctx.repo.tests.workdir / i)) + for i in ctx.repo.tests.changed_files.keys() + ] + log_tool_output(output=outputs, tool_name=ToolName.WriteUntTest) + return ctx.repo.tests.workdir @@ -237,6 +285,13 @@ async def fix_bug(project_path: str | Path, issue: str) -> Path: me = {any_to_str(role), role.name} while me.intersection(msg.send_to): msg = await role.run(with_message=msg) + + outputs = [ + ToolOutputItem(name="Changed File", value=str(ctx.repo.srcs.workdir / i)) + for i in ctx.repo.srcs.changed_files.keys() + ] + log_tool_output(output=outputs, tool_name=ToolName.FixBug) + return project_path @@ -269,6 +324,10 @@ async def git_archive(project_path: str | Path) -> str: ctx = Context() ctx.set_repo_dir(project_path) ctx.git_repo.archive() + + outputs = [ToolOutputItem(name="Git Commit", value=str(ctx.repo.workdir))] + log_tool_output(output=outputs, tool_name=ToolName.GitArchive) + return ctx.git_repo.log() @@ -298,4 +357,8 @@ async def import_git_repo(url: str) -> Path: ctx = Context() action = ImportRepo(repo_path=url, context=ctx) await action.run() + + outputs = [ToolOutputItem(name="MetaGPT Project", value=str(ctx.repo.workdir))] + log_tool_output(output=outputs, tool_name=ToolName.ImportRepo) + return ctx.repo.workdir