From 23f9d42dc184e02905a844c41583670f61c3675b Mon Sep 17 00:00:00 2001 From: DengMingyi <121236312+Rubbisheep@users.noreply.github.com> Date: Thu, 15 May 2025 17:52:19 +0800 Subject: [PATCH 1/5] add role PPTmaker --- metagpt/prompts/PPTmaker.py | 94 +++++++++++++++++++ metagpt/roles/PPTmaker.py | 181 ++++++++++++++++++++++++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 metagpt/prompts/PPTmaker.py create mode 100644 metagpt/roles/PPTmaker.py diff --git a/metagpt/prompts/PPTmaker.py b/metagpt/prompts/PPTmaker.py new file mode 100644 index 0000000000..d7e441fb42 --- /dev/null +++ b/metagpt/prompts/PPTmaker.py @@ -0,0 +1,94 @@ +SYSTEM_PROMPT = """ +You are a LaTeX Beamer Presentation Generator. Your task is to generate a complete, informative, and ready-to-compile Beamer slide deck in LaTeX, based on the task description and any past drafts or feedback. + +## Goals: +- Each slide must be **self-contained**, meaning the audience should understand the slide without external explanations. +- The presentation must **teach** or **explain** the topic in sufficient detail using structured LaTeX slides. +- Each slide must contribute meaningfully to the overall structure and flow of the presentation. + + + +## Requirements: + +1. Preamble & Setup + - Start with `\\documentclass{beamer}`. + - Use packages such as `amsmath`, `amsfonts`, and `graphicx`. + - Use the `Madrid` theme unless otherwise specified. + - Include full metadata: `\\title{}`, `\\author{}`, and `\\date{\\today}`. + +2. Slide Design + - MUST mark each slide with a comment indicating its number, `% Slide 1`, `% Slide 2`. + - - Slides must follow a **logical order** that ensures smooth flow and coherence. + - AIM for a **minimum of 300 words per slide* Contain **enough detail** (text, bullets, equations, definitions, or examples) + +3. Depth of Content + - For important concept, include motivation, problem, intuitive explanation, mathematical formulation or equation (if applicable) + - practical example or application can also be included + +4. Completeness & Validity + - Reflect all provided feedback and correct deficiencies from past versions. + - MUST No placeholders or incomplete content. + - Your output will be used directly. Therefore, it must be a ready-to-use result. + - Include `\\end{document}`. + - Ensure valid LaTeX syntax. + +5. Style & Clarity + - Maintain consistent formatting and indentation. + - Use bullet points or short paragraphs for clarity. + - Keep math readable and contextualized with supporting text. + +**Only output the final LaTeX source code. Do not include explanations, notes, or comments.** +""" + +USER_CONTENT = """ +## Task +{request} + +## Past Drafts & Feedback +{history} +""" + +TEXT_VALIDATION_PROMPT = """ +You are a task result evaluator responsible for determining whether a task result meets the task requirements, if not, you need to improve it. + +# Objective and Steps +1. **Completeness and Quality Check:** + - Verify that the result includes all required elements of the task. + - Evaluate whether the output meets overall quality criteria (accuracy, clarity, formatting, and completeness). + +2. **Change Detection:** + - If this is a subsequent result, compare it with previous iterations. + - If the differences are minimal or the result has not significantly improved, consider it "good enough" for finalization. + +3. **Feedback and Escalation:** + - If the result meets the criteria or the improvements are negligible compared to previous iterations, return **"No further feedback"**. + - Otherwise, provide **direct and precise feedback** and **output the improved result in the required format** for finalization. + +4. **Ensure Completeness:** + - Your output must meet all requirements of the task. + - Include all necessary details so that the output is self-contained and can be directly used as input for downstream tasks. + +5. **Do NOT:** + - Leave any section with placeholders (e.g., "TODO", "Add content here"). + - Include any commentary or reminders to the writer or user (e.g., "We can add more later"). + - Output partial slides or omit essential details assuming future input. + +- **If the result meets the standard:** + - Return **"No further feedback."**. + +- **If the result does not meet the standard:** + - add detailed jusification for the change start with "here are some feedbacks" and directly write an improved new result start with "here are the changes". + +# Note that: Any output containing incomplete sections, placeholders is not allowed. +""" + +USER_VALIDATION_CONTENT = """ +## Current Task Requirement: +{request} + +--- + +## Current Task Latest Result: +{history} +""" + diff --git a/metagpt/roles/PPTmaker.py b/metagpt/roles/PPTmaker.py new file mode 100644 index 0000000000..7c1230555d --- /dev/null +++ b/metagpt/roles/PPTmaker.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@File : PPTmaker.py +@Time : 2025/05/14 18:16:47 +@Author : Deng Mingyi +@Desc : A role to create LaTeX-based presentations. +""" + +from typing import List, Dict, Any +from metagpt.actions import Action +from metagpt.roles.di.role_zero import RoleZero +from metagpt.schema import Message +from metagpt.logs import logger +from metagpt.prompts.PPTmaker import SYSTEM_PROMPT, USER_CONTENT, TEXT_VALIDATION_PROMPT, USER_VALIDATION_CONTENT + + +class LatexGeneratorAction(Action): + """ + Action to generate LaTeX content for presentations. + """ + name: str = "latexgenerator" + description: str = """ + This agent generates complete, high-quality LaTeX documents with a focus on Beamer presentations. It accepts topic-specific input and produces fully self-contained LaTeX source code, including all required packages, structures, and rich content elements such as equations, figures, and formatted text. The agent ensures completeness by avoiding any placeholders or incomplete sections. + + In addition to generation, the agent supports iterative refinement: it evaluates and improves the generated LaTeX code based on validation feedback to ensure correctness, formatting quality, and logical structure. The final output is ready for immediate compilation and professional presentation use. + """ + + async def generate(self, request: str, history: str) -> str: + """ + Generate LaTeX content. + Args: + request: Initial user request + history: String representation of historical messages + Returns: + Generated LaTeX code as string + """ + logger.info(f"Executing {self.name} Action: Generating LaTeX content for request '{request[:50]}...'") + + system_content = SYSTEM_PROMPT + user_content = USER_CONTENT.format(request=request, history=history) + + generated_latex = await self._aask(user_content, [system_content]) + + logger.debug(f"Generated LaTeX content (partial): {generated_latex[:200]}...") + return generated_latex + + async def run(self, request: str, history: str) -> str: + """Execute the current action""" + return await self.generate(request, history) + + +class ValidatorAction(Action): + """ + Action to validate LaTeX content. + """ + name: str = "validator" + description: str = """ + This tool evaluates the quality and completeness of a subtask result against a set of predefined criteria. + It checks whether the result fully satisfies task requirements, maintains high quality in terms of clarity, accuracy, and formatting, + and determines whether improvements have been made in comparison to prior versions. + If the result is satisfactory or improvements are minimal, it returns "The step result has already reached the requirement.". + Otherwise, it provides detailed feedback and a revised version of the result that meets all requirements and is ready for downstream use. + """ + + async def validate(self, request: str, history: str) -> str: + """ + Validate the generated content. + Args: + request: Initial user request + history: String representation of historical messages + Returns: + Validation feedback string + """ + logger.info(f"Executing {self.name} Action: Validating current content") + + system_content = TEXT_VALIDATION_PROMPT + user_content = USER_VALIDATION_CONTENT.format(request=request, history=history) + + feedback = await self._aask(user_content, [system_content]) + logger.debug(f"Validation feedback: {feedback[:200]}...") + + return feedback + + async def run(self, request: str, history: str) -> str: + """Execute the current action""" + return await self.validate(request, history) + + +class PPTMaker(RoleZero): + """ + Role responsible for creating LaTeX format presentations. Calls tools in a fixed sequence + and may terminate early based on validator feedback. + """ + name: str = "PPTMaker" + profile: str = "LaTeX Presentation Generator" + goal: str = "Generate high-quality LaTeX presentations in Beamer format" + constraints: str = "Call tools in predefined order, may terminate early based on validation feedback" + + max_steps: int = 7 + curr_step: int = 0 + + accumulated_latex: str = "" + is_completed: bool = False + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._init_actions([LatexGeneratorAction, ValidatorAction]) + self._reset_state() + + def _reset_state(self): + """Reset internal state to prepare for a new task""" + self.curr_step = 0 + self.accumulated_latex = "" + self.is_completed = False + logger.info(f"{self.name} state has been reset") + + async def react(self) -> Message: + """ + Process current state, decide next action, execute and return a Message. + """ + if self.curr_step >= self.max_steps or self.is_completed: + logger.info(f"{self.name} task completed or reached maximum steps ({self.max_steps})") + final_message = f"Generation task completed.\nGenerated LaTeX content:\n\n{self.accumulated_latex}" + self._reset_state() + return Message(content=final_message, role=self.profile, cause_by=self.__class__) + + tool_idx = self.curr_step % len(self.rc.actions) + tool_class = [LatexGeneratorAction, ValidatorAction][tool_idx] + tool_name = ["latexgenerator", "validator"][tool_idx] + + logger.info(f"{self.name} selected tool: {tool_name}") + + request = self.rc.history[0].content if self.rc.history else "No topic provided" + payload = { + "request": request, + "history": self.accumulated_latex, + } + + result = None + try: + if tool_name == "latexgenerator": + result = await self.rc.run_action( + LatexGeneratorAction, + request=payload["request"], + history=payload["history"] + ) + if result: + if not self.accumulated_latex: + self.accumulated_latex = result + else: + self.accumulated_latex += "\n" + result + logger.info(f"LaTeX content generated, current total length: {len(self.accumulated_latex)}") + message_content = f"Step {self.curr_step+1}/{self.max_steps}: Used tool {tool_name} to generate content" + + elif tool_name == "validator": + result = await self.rc.run_action( + ValidatorAction, + request=payload["request"], + history=payload["history"] + ) + message_content = f"Step {self.curr_step+1}/{self.max_steps}: Used tool {tool_name} to validate content" + + if "The step result has already reached the requirement." in result: + logger.info(f"{self.name}: Validator indicates task is complete") + self.is_completed = True + final_msg = f"Validation complete, task requirements met. Final LaTeX content:\n\n{self.accumulated_latex}" + self._reset_state() + return Message(content=final_msg, role=self.profile, cause_by=tool_class) + + else: + message_content = f"Unknown tool type: {tool_name}" + logger.warning(message_content) + + except Exception as e: + logger.error(f"Error executing tool {tool_name}: {e}", exc_info=True) + message_content = f"Error executing tool {tool_name}: {str(e)}" + + self.curr_step += 1 + + return Message(content=message_content, role=self.profile, cause_by=tool_class) From 07a48627e361d4d3e719ec15eb1399d43ecf1258 Mon Sep 17 00:00:00 2001 From: DengMingyi <121236312+Rubbisheep@users.noreply.github.com> Date: Thu, 15 May 2025 21:38:26 +0800 Subject: [PATCH 2/5] update save md and debug --- metagpt/roles/PPTmaker.py | 106 +++++++++++++++++++++++--------------- 1 file changed, 65 insertions(+), 41 deletions(-) diff --git a/metagpt/roles/PPTmaker.py b/metagpt/roles/PPTmaker.py index 7c1230555d..1feadebac4 100644 --- a/metagpt/roles/PPTmaker.py +++ b/metagpt/roles/PPTmaker.py @@ -35,14 +35,14 @@ async def generate(self, request: str, history: str) -> str: Returns: Generated LaTeX code as string """ - logger.info(f"Executing {self.name} Action: Generating LaTeX content for request '{request[:50]}...'") + logger.info(f"Executing {self.name} Action: Generating LaTeX content for request '{request[:]}...'") system_content = SYSTEM_PROMPT user_content = USER_CONTENT.format(request=request, history=history) generated_latex = await self._aask(user_content, [system_content]) - logger.debug(f"Generated LaTeX content (partial): {generated_latex[:200]}...") + logger.debug(f"Generated LaTeX content: {generated_latex[:]}...") return generated_latex async def run(self, request: str, history: str) -> str: @@ -78,7 +78,7 @@ async def validate(self, request: str, history: str) -> str: user_content = USER_VALIDATION_CONTENT.format(request=request, history=history) feedback = await self._aask(user_content, [system_content]) - logger.debug(f"Validation feedback: {feedback[:200]}...") + logger.debug(f"Validation feedback: {feedback[:]}...") return feedback @@ -102,10 +102,11 @@ class PPTMaker(RoleZero): accumulated_latex: str = "" is_completed: bool = False + optimized_result: str = None def __init__(self, **kwargs): super().__init__(**kwargs) - self._init_actions([LatexGeneratorAction, ValidatorAction]) + self.set_actions([LatexGeneratorAction, ValidatorAction]) self._reset_state() def _reset_state(self): @@ -113,69 +114,92 @@ def _reset_state(self): self.curr_step = 0 self.accumulated_latex = "" self.is_completed = False + self.validator_feedback = "" logger.info(f"{self.name} state has been reset") + @staticmethod + def save_md(content: str, filename: str = "presentation.md"): + """ + Save the generated LaTeX content to a file in the workspace directory. + """ + import os + + workspace_dir = os.path.join(os.getcwd(), "workspace") + + os.makedirs(workspace_dir, exist_ok=True) + + save_path = os.path.join(workspace_dir, filename) + + try: + with open(save_path, "w", encoding="utf-8") as f: + f.write(content) + + logger.info(f"markdown file saved at {save_path}") + return save_path + except Exception as e: + logger.error(f"Failed to save markdown content: {e}") + return None + async def react(self) -> Message: """ Process current state, decide next action, execute and return a Message. """ - if self.curr_step >= self.max_steps or self.is_completed: - logger.info(f"{self.name} task completed or reached maximum steps ({self.max_steps})") - final_message = f"Generation task completed.\nGenerated LaTeX content:\n\n{self.accumulated_latex}" - self._reset_state() - return Message(content=final_message, role=self.profile, cause_by=self.__class__) - - tool_idx = self.curr_step % len(self.rc.actions) + tool_idx = self.curr_step % len(self.actions) tool_class = [LatexGeneratorAction, ValidatorAction][tool_idx] tool_name = ["latexgenerator", "validator"][tool_idx] logger.info(f"{self.name} selected tool: {tool_name}") request = self.rc.history[0].content if self.rc.history else "No topic provided" - payload = { - "request": request, - "history": self.accumulated_latex, - } - result = None + try: if tool_name == "latexgenerator": - result = await self.rc.run_action( - LatexGeneratorAction, - request=payload["request"], - history=payload["history"] - ) + LatexGenerator = LatexGeneratorAction() + result = await LatexGenerator.run( + request=request, + history=self.accumulated_latex + ) + self.optimized_result = result if result: if not self.accumulated_latex: self.accumulated_latex = result else: self.accumulated_latex += "\n" + result + logger.info(f"LaTeX content generated, current total length: {len(self.accumulated_latex)}") message_content = f"Step {self.curr_step+1}/{self.max_steps}: Used tool {tool_name} to generate content" - + elif tool_name == "validator": - result = await self.rc.run_action( - ValidatorAction, - request=payload["request"], - history=payload["history"] - ) + Validator = ValidatorAction() + result = await Validator.run( + request=request, + history=self.accumulated_latex + ) message_content = f"Step {self.curr_step+1}/{self.max_steps}: Used tool {tool_name} to validate content" - - if "The step result has already reached the requirement." in result: + self.accumulated_latex += "\n" + result + + if "No further feedback" in result: logger.info(f"{self.name}: Validator indicates task is complete") self.is_completed = True - final_msg = f"Validation complete, task requirements met. Final LaTeX content:\n\n{self.accumulated_latex}" - self._reset_state() - return Message(content=final_msg, role=self.profile, cause_by=tool_class) - - else: - message_content = f"Unknown tool type: {tool_name}" - logger.warning(message_content) - + self.curr_step += 1 + return self.optimized_result + except Exception as e: - logger.error(f"Error executing tool {tool_name}: {e}", exc_info=True) - message_content = f"Error executing tool {tool_name}: {str(e)}" + logger.error(f"Error executing tool {tool_name}: {e}") + message_content = f"Error executing tool {tool_name}: {e}" self.curr_step += 1 - - return Message(content=message_content, role=self.profile, cause_by=tool_class) + Message(content=message_content, role=self.profile, cause_by=tool_class) + return self.optimized_result + + async def run(self, prompt: Message) -> Message: + """ Execute the current action""" + self.rc.memory.add(prompt) + while True: + if self.curr_step >= self.max_steps or self.is_completed: + logger.info(f"{self.name} task completed or reached maximum steps ({self.max_steps})") + final_message = f"Generation task completed.\nGenerated LaTeX content:\n\n{message}" + self.save_md(message, filename="presentation.md") + return Message(content=final_message, role=self.profile, cause_by=self.__class__) + message = await self.react() \ No newline at end of file From 80671e012e7b532f8d0c087111c991346c8a94fd Mon Sep 17 00:00:00 2001 From: DengMingyi <121236312+Rubbisheep@users.noreply.github.com> Date: Fri, 16 May 2025 01:31:59 +0800 Subject: [PATCH 3/5] Improve code coherence --- metagpt/roles/PPTmaker.py | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/metagpt/roles/PPTmaker.py b/metagpt/roles/PPTmaker.py index 1feadebac4..e17f78044b 100644 --- a/metagpt/roles/PPTmaker.py +++ b/metagpt/roles/PPTmaker.py @@ -35,7 +35,8 @@ async def generate(self, request: str, history: str) -> str: Returns: Generated LaTeX code as string """ - logger.info(f"Executing {self.name} Action: Generating LaTeX content for request '{request[:]}...'") + logger.info(f"Actual max_tokens used: {self.llm.config.max_token}") + logger.info(f"Executing {self.name} Action: Generating LaTeX content for request \n'{request[:]}...'") system_content = SYSTEM_PROMPT user_content = USER_CONTENT.format(request=request, history=history) @@ -112,7 +113,6 @@ def __init__(self, **kwargs): def _reset_state(self): """Reset internal state to prepare for a new task""" self.curr_step = 0 - self.accumulated_latex = "" self.is_completed = False self.validator_feedback = "" logger.info(f"{self.name} state has been reset") @@ -150,56 +150,50 @@ async def react(self) -> Message: logger.info(f"{self.name} selected tool: {tool_name}") - request = self.rc.history[0].content if self.rc.history else "No topic provided" result = None try: if tool_name == "latexgenerator": LatexGenerator = LatexGeneratorAction() result = await LatexGenerator.run( - request=request, - history=self.accumulated_latex + request=self.rc.history[0].content, + history=self.rc.history ) self.optimized_result = result if result: - if not self.accumulated_latex: - self.accumulated_latex = result - else: - self.accumulated_latex += "\n" + result + self.rc.memory.add(result) - logger.info(f"LaTeX content generated, current total length: {len(self.accumulated_latex)}") + logger.info(f"LaTeX content generated, current total length: {len(self.rc.history)}") message_content = f"Step {self.curr_step+1}/{self.max_steps}: Used tool {tool_name} to generate content" elif tool_name == "validator": Validator = ValidatorAction() result = await Validator.run( - request=request, - history=self.accumulated_latex + request=self.rc.history[0].content, + history=self.rc.history ) message_content = f"Step {self.curr_step+1}/{self.max_steps}: Used tool {tool_name} to validate content" - self.accumulated_latex += "\n" + result + self.rc.memory.add(result) if "No further feedback" in result: logger.info(f"{self.name}: Validator indicates task is complete") self.is_completed = True self.curr_step += 1 - return self.optimized_result + return Message(content=self.optimized_result, role=self.profile, cause_by=tool_class) except Exception as e: logger.error(f"Error executing tool {tool_name}: {e}") - message_content = f"Error executing tool {tool_name}: {e}" self.curr_step += 1 - Message(content=message_content, role=self.profile, cause_by=tool_class) - return self.optimized_result + return Message(content=self.optimized_result or message_content, role=self.profile, cause_by=tool_class) async def run(self, prompt: Message) -> Message: - """ Execute the current action""" self.rc.memory.add(prompt) + message = await self.react() while True: if self.curr_step >= self.max_steps or self.is_completed: logger.info(f"{self.name} task completed or reached maximum steps ({self.max_steps})") - final_message = f"Generation task completed.\nGenerated LaTeX content:\n\n{message}" - self.save_md(message, filename="presentation.md") - return Message(content=final_message, role=self.profile, cause_by=self.__class__) + final_message = f"Generation task completed.\nGenerated LaTeX content:\n\n{message.content}" + self.save_md(final_message, filename="presentation.md") + return Message(content=final_message, role=self.profile) message = await self.react() \ No newline at end of file From fc23e0dcc7b56ed1abcf1cda2e81206921ce7764 Mon Sep 17 00:00:00 2001 From: DengMingyi <121236312+Rubbisheep@users.noreply.github.com> Date: Sat, 24 May 2025 14:31:34 +0800 Subject: [PATCH 4/5] improve code coherence --- metagpt/actions/generate_latex.py | 103 +++++++++++ metagpt/roles/PPTmaker.py | 296 ++++++++++++++++-------------- 2 files changed, 257 insertions(+), 142 deletions(-) create mode 100644 metagpt/actions/generate_latex.py diff --git a/metagpt/actions/generate_latex.py b/metagpt/actions/generate_latex.py new file mode 100644 index 0000000000..6bf855dd8f --- /dev/null +++ b/metagpt/actions/generate_latex.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@File : gernerate_latex.py +@Time : 2025/05/20 20:15:13 +@Author : Deng Mingyi +@Desc : Action to generate LaTeX content for presentations. Action to validate LaTeX content. +""" +from metagpt.prompts.PPTmaker import ( + SYSTEM_PROMPT, + USER_CONTENT, + TEXT_VALIDATION_PROMPT, + USER_VALIDATION_CONTENT, +) +from metagpt.actions import Action +from metagpt.logs import logger +from typing import List +from metagpt.schema import Message + + +class LatexGeneratorAction(Action): + """ + Action to generate LaTeX content for presentations. + """ + + name: str = "latexgenerator" + description: str = """ + This agent generates complete, high-quality LaTeX documents with a focus on Beamer presentations. It accepts topic-specific input and produces fully self-contained LaTeX source code, including all required packages, structures, and rich content elements such as equations, figures, and formatted text. The agent ensures completeness by avoiding any placeholders or incomplete sections. + + In addition to generation, the agent supports iterative refinement: it evaluates and improves the generated LaTeX code based on validation feedback to ensure correctness, formatting quality, and logical structure. The final output is ready for immediate compilation and professional presentation use. + """ + + async def generate(self, request: str, history_messages: List[Message]) -> str: + """ + Generate LaTeX content. + Args: + request: Initial user request or refined request with feedback. + history_messages: List of historical messages for context. + Returns: + Generated LaTeX code as string + """ + logger.info(f"Actual max_tokens used: {self.llm.config.max_token}") + logger.info(f"Executing {self.name} Action: Generating LaTeX content for request \n'{request}'") + + system_content = SYSTEM_PROMPT + history_str = "\n".join([f"{msg.role}: {msg.content}" for msg in history_messages]) + user_content = USER_CONTENT.format(request=request, history=history_str) + + generated_latex = await self._aask(user_content, [system_content]) + + logger.debug(f"Generated LaTeX content: {generated_latex}") + return generated_latex + + async def run(self, request: str, history: List[Message]) -> str: + """Execute the current action""" + return await self.generate(request, history) + + +class ValidatorAction(Action): + """ + Action to validate LaTeX content. + """ + + name: str = "validator" + description: str = """ + This tool evaluates the quality and completeness of a subtask result against a set of predefined criteria. + It checks whether the result fully satisfies task requirements, maintains high quality in terms of clarity, accuracy, and formatting, + and determines whether improvements have been made in comparison to prior versions. + If the result is satisfactory or improvements are minimal, it returns "No further feedback." or "The step result has already reached the requirement.". + Otherwise, it provides detailed feedback and a revised version of the result that meets all requirements and is ready for downstream use. + """ + + async def validate(self, request: str, current_latex_to_validate: str, history_messages: List[Message]) -> str: + """ + Validate the generated content. + Args: + request: Initial user request. + current_latex_to_validate: The LaTeX content to be validated. + history_messages: List of historical messages for context. + Returns: + Validation feedback string + """ + logger.info(f"Executing {self.name} Action: Validating current content") + + system_content = TEXT_VALIDATION_PROMPT + history_str = "\n".join([f"{msg.role}: {msg.content}" for msg in history_messages]) + validation_input_for_prompt = f"Initial Request: {request}\n\nLaTeX to Validate:\n{current_latex_to_validate}\n\nConversation History:\n{history_str}" + + user_content = USER_VALIDATION_CONTENT.format(request=validation_input_for_prompt, history="") # Adjust as per prompt needs + + feedback = await self._aask(user_content, [system_content]) + logger.debug(f"Validation feedback: {feedback}") + + return feedback + + async def run(self, request: str, history: List[Message]) -> str: + """Execute the current action. + 'request' here is the 'input_for_action' passed from PPTMaker._act, + which is self.optimized_result for ValidatorAction. + """ + initial_user_request = history[0].content if history else "" + content_to_validate = request + return await self.validate(initial_user_request, content_to_validate, history) \ No newline at end of file diff --git a/metagpt/roles/PPTmaker.py b/metagpt/roles/PPTmaker.py index e17f78044b..63dc35a68c 100644 --- a/metagpt/roles/PPTmaker.py +++ b/metagpt/roles/PPTmaker.py @@ -7,85 +7,16 @@ @Desc : A role to create LaTeX-based presentations. """ +import os from typing import List, Dict, Any +from metagpt.logs import logger from metagpt.actions import Action +from metagpt.actions.generate_latex import LatexGeneratorAction, ValidatorAction from metagpt.roles.di.role_zero import RoleZero +from metagpt.roles.role import RoleReactMode from metagpt.schema import Message -from metagpt.logs import logger -from metagpt.prompts.PPTmaker import SYSTEM_PROMPT, USER_CONTENT, TEXT_VALIDATION_PROMPT, USER_VALIDATION_CONTENT - - -class LatexGeneratorAction(Action): - """ - Action to generate LaTeX content for presentations. - """ - name: str = "latexgenerator" - description: str = """ - This agent generates complete, high-quality LaTeX documents with a focus on Beamer presentations. It accepts topic-specific input and produces fully self-contained LaTeX source code, including all required packages, structures, and rich content elements such as equations, figures, and formatted text. The agent ensures completeness by avoiding any placeholders or incomplete sections. - - In addition to generation, the agent supports iterative refinement: it evaluates and improves the generated LaTeX code based on validation feedback to ensure correctness, formatting quality, and logical structure. The final output is ready for immediate compilation and professional presentation use. - """ - - async def generate(self, request: str, history: str) -> str: - """ - Generate LaTeX content. - Args: - request: Initial user request - history: String representation of historical messages - Returns: - Generated LaTeX code as string - """ - logger.info(f"Actual max_tokens used: {self.llm.config.max_token}") - logger.info(f"Executing {self.name} Action: Generating LaTeX content for request \n'{request[:]}...'") - - system_content = SYSTEM_PROMPT - user_content = USER_CONTENT.format(request=request, history=history) - - generated_latex = await self._aask(user_content, [system_content]) - - logger.debug(f"Generated LaTeX content: {generated_latex[:]}...") - return generated_latex - - async def run(self, request: str, history: str) -> str: - """Execute the current action""" - return await self.generate(request, history) - - -class ValidatorAction(Action): - """ - Action to validate LaTeX content. - """ - name: str = "validator" - description: str = """ - This tool evaluates the quality and completeness of a subtask result against a set of predefined criteria. - It checks whether the result fully satisfies task requirements, maintains high quality in terms of clarity, accuracy, and formatting, - and determines whether improvements have been made in comparison to prior versions. - If the result is satisfactory or improvements are minimal, it returns "The step result has already reached the requirement.". - Otherwise, it provides detailed feedback and a revised version of the result that meets all requirements and is ready for downstream use. - """ - - async def validate(self, request: str, history: str) -> str: - """ - Validate the generated content. - Args: - request: Initial user request - history: String representation of historical messages - Returns: - Validation feedback string - """ - logger.info(f"Executing {self.name} Action: Validating current content") - - system_content = TEXT_VALIDATION_PROMPT - user_content = USER_VALIDATION_CONTENT.format(request=request, history=history) - - feedback = await self._aask(user_content, [system_content]) - logger.debug(f"Validation feedback: {feedback[:]}...") - - return feedback - - async def run(self, request: str, history: str) -> str: - """Execute the current action""" - return await self.validate(request, history) +from metagpt.utils.common import handle_exception +from metagpt.actions.add_requirement import UserRequirement class PPTMaker(RoleZero): @@ -93,107 +24,188 @@ class PPTMaker(RoleZero): Role responsible for creating LaTeX format presentations. Calls tools in a fixed sequence and may terminate early based on validator feedback. """ + name: str = "PPTMaker" profile: str = "LaTeX Presentation Generator" goal: str = "Generate high-quality LaTeX presentations in Beamer format" constraints: str = "Call tools in predefined order, may terminate early based on validation feedback" - max_steps: int = 7 - curr_step: int = 0 - - accumulated_latex: str = "" + max_steps: int = 7 is_completed: bool = False - optimized_result: str = None + + optimized_result: str = "" + validator_feedback: str = "" + curr_step: int = 0 + + ACTION_SEQUENCE_METADATA: List[Dict[str, Any]] = [ + {"action_class": LatexGeneratorAction, "save_result": True, "name": "latexgenerator"}, + {"action_class": ValidatorAction, "save_result": False, "name": "validator"}, + ] def __init__(self, **kwargs): super().__init__(**kwargs) - self.set_actions([LatexGeneratorAction, ValidatorAction]) + self.set_actions([meta["action_class"] for meta in self.ACTION_SEQUENCE_METADATA]) + self._set_react_mode(react_mode=RoleReactMode.BY_ORDER, max_react_loop=self.max_steps) + self.use_fixed_sop = False self._reset_state() def _reset_state(self): """Reset internal state to prepare for a new task""" self.curr_step = 0 self.is_completed = False - self.validator_feedback = "" + self.validator_feedback = "" + self.optimized_result = "" logger.info(f"{self.name} state has been reset") @staticmethod + @handle_exception(exception_msg="Fail to save markdown file", default_return="Error Occurred") def save_md(content: str, filename: str = "presentation.md"): """ Save the generated LaTeX content to a file in the workspace directory. """ - import os - workspace_dir = os.path.join(os.getcwd(), "workspace") - os.makedirs(workspace_dir, exist_ok=True) - save_path = os.path.join(workspace_dir, filename) - - try: - with open(save_path, "w", encoding="utf-8") as f: - f.write(content) - - logger.info(f"markdown file saved at {save_path}") - return save_path - except Exception as e: - logger.error(f"Failed to save markdown content: {e}") - return None - async def react(self) -> Message: + with open(save_path, "w", encoding="utf-8") as f: + f.write(content) + + logger.info(f"Markdown file saved at {save_path}") + return save_path + + async def _think(self) -> bool: """ - Process current state, decide next action, execute and return a Message. + Decide the next action or if the task should stop. + This is called by the base RoleZero's _react loop. + Manages cycling through actions for BY_ORDER mode up to max_steps. """ - tool_idx = self.curr_step % len(self.actions) - tool_class = [LatexGeneratorAction, ValidatorAction][tool_idx] - tool_name = ["latexgenerator", "validator"][tool_idx] - - logger.info(f"{self.name} selected tool: {tool_name}") + if self.is_completed: + logger.info(f"{self.name} task marked as completed. Stopping.") + return False + + if self.curr_step >= self.max_steps: + logger.info(f"{self.name} reached maximum steps ({self.max_steps}). Stopping.") + return False + + # Determine the next action index in the sequence based on current step + next_action_idx_in_sequence = self.curr_step % len(self.ACTION_SEQUENCE_METADATA) + self._set_state(next_action_idx_in_sequence) # Set self.rc.state and self.rc.todo + + if self.rc.todo: + logger.info(f"{self.name} decided next action: {self.rc.todo.name} (Current Step: {self.curr_step + 1}, Action Index: {next_action_idx_in_sequence})") + else: + logger.error(f"{self.name} _think decided no action despite conditions to continue. Action index: {next_action_idx_in_sequence}") + return False + return True + + async def _act(self) -> Message: + """ + Perform the current action decided by _think and process its result. + This is called by the base RoleZero's _react loop. + """ + action_idx_in_sequence = self.rc.state - result = None + if not (0 <= action_idx_in_sequence < len(self.actions)): + logger.error(f"Invalid action index {action_idx_in_sequence} in _act. Stopping.") + return Message(content=f"Error: Invalid action index {action_idx_in_sequence}.", role=self.profile) + current_action_meta = self.ACTION_SEQUENCE_METADATA[action_idx_in_sequence] + tool_instance: Action = self.actions[action_idx_in_sequence] + + tool_name = current_action_meta["name"] + save_result_flag = current_action_meta["save_result"] + + logger.info(f"{self.name} performing action: {tool_name} (Overall Step: {self.curr_step + 1}, Action Index: {action_idx_in_sequence})") + + result_content_str = "" try: - if tool_name == "latexgenerator": - LatexGenerator = LatexGeneratorAction() - result = await LatexGenerator.run( - request=self.rc.history[0].content, - history=self.rc.history - ) - self.optimized_result = result - if result: - self.rc.memory.add(result) - - logger.info(f"LaTeX content generated, current total length: {len(self.rc.history)}") - message_content = f"Step {self.curr_step+1}/{self.max_steps}: Used tool {tool_name} to generate content" - - elif tool_name == "validator": - Validator = ValidatorAction() - result = await Validator.run( - request=self.rc.history[0].content, - history=self.rc.history - ) - message_content = f"Step {self.curr_step+1}/{self.max_steps}: Used tool {tool_name} to validate content" - self.rc.memory.add(result) - - if "No further feedback" in result: - logger.info(f"{self.name}: Validator indicates task is complete") + initial_request_str = self.rc.history[0].content if self.rc.history else "" + input_for_action = self.optimized_result if isinstance(tool_instance, ValidatorAction) else initial_request_str + + if isinstance(tool_instance, LatexGeneratorAction) and self.validator_feedback: + # Append feedback to the original request for the generator + input_for_action = f"Original Request:\n{initial_request_str}\n\nFeedback for improvement:\n{self.validator_feedback}" + + result_content_str = await tool_instance.run( + request=input_for_action, + history=self.rc.history + ) + + if save_result_flag: # update self.optimized_result + self.optimized_result = result_content_str + + if isinstance(tool_instance, ValidatorAction): + self.validator_feedback = result_content_str + if "No further feedback" in result_content_str: self.is_completed = True - self.curr_step += 1 - return Message(content=self.optimized_result, role=self.profile, cause_by=tool_class) + logger.info(f"{self.name} task deemed completed by validator.") + elif isinstance(tool_instance, LatexGeneratorAction): + self.validator_feedback = "" + + # Add action's direct result to memory + self.rc.memory.add(Message(content=result_content_str, role=self.profile, cause_by=type(tool_instance), sent_from=self.name)) except Exception as e: - logger.error(f"Error executing tool {tool_name}: {e}") - + logger.error(f"Error executing {tool_name} in {self.name}: {e}", exc_info=True) + self.curr_step += 1 + return Message(content=f"Error executing {tool_name}: {str(e)}", role=self.profile, cause_by=type(tool_instance)) + self.curr_step += 1 - return Message(content=self.optimized_result or message_content, role=self.profile, cause_by=tool_class) - + + display_content = result_content_str + + return Message( + content=display_content or f"Step {self.curr_step}/{self.max_steps} ({tool_name}) completed.", # self.curr_step is already incremented + role=self.profile, + cause_by=type(tool_instance) + ) + + @handle_exception(exception_msg="Error in PPTMaker execution", default_return=Message(content="Error occurred", role="system")) async def run(self, prompt: Message) -> Message: + """ + Launch the PPTMaker role execution flow. + """ + self._reset_state() + + logger.info(f"{self.name} run: Starting with prompt: '{prompt.content[:50]}...'") + + prompt.role = "user" + prompt.cause_by = UserRequirement + prompt.sent_from = self.name + self.rc.memory.add(prompt) - message = await self.react() - while True: - if self.curr_step >= self.max_steps or self.is_completed: - logger.info(f"{self.name} task completed or reached maximum steps ({self.max_steps})") - final_message = f"Generation task completed.\nGenerated LaTeX content:\n\n{message.content}" - self.save_md(final_message, filename="presentation.md") - return Message(content=final_message, role=self.profile) - message = await self.react() \ No newline at end of file + self.rc.news.append(prompt) + + logger.info(f"{self.name} run: Added prompt to memory and news. Memory: {len(self.rc.history)}, News: {len(self.rc.news)}") + + await self._react() + + # Finalize the task and save the result + final_content_to_save = self.optimized_result + status_message = "Unknown" + + if self.is_completed: + logger.info(f"{self.name} task completed successfully.") + status_message = "Completed" + elif self.curr_step >= self.max_steps: + logger.info(f"{self.name} reached maximum steps ({self.max_steps}).") + status_message = "Reached max steps" + else: + logger.info(f"{self.name} task did not complete normally (curr_step: {self.curr_step}, is_completed: {self.is_completed}).") + status_message = "Stopped or Errored" + + if not final_content_to_save: + final_content_to_save = "No content was finalized." + + final_report_str = f"Generation task status: {status_message}.\n\n" + if self.is_completed or self.optimized_result: + final_report_str += f"Final LaTeX content:\n\n{final_content_to_save}" + else: + final_report_str += f"Last relevant output:\n\n{final_content_to_save}" + + self.save_md(final_content_to_save, filename="presentation_output.md") + logger.info(f"Final result saved. Optimized result length: {len(final_content_to_save)}") + + return Message(content=final_report_str, role=self.profile) + + \ No newline at end of file From 65468b8b8bdafa89a373bc6f3f763172514dded6 Mon Sep 17 00:00:00 2001 From: DengMingyi <121236312+Rubbisheep@users.noreply.github.com> Date: Mon, 26 May 2025 19:17:32 +0800 Subject: [PATCH 5/5] Simplify code and enhance readability --- metagpt/roles/PPTmaker.py | 83 +++++++++++---------------------------- 1 file changed, 24 insertions(+), 59 deletions(-) diff --git a/metagpt/roles/PPTmaker.py b/metagpt/roles/PPTmaker.py index 63dc35a68c..d5284025b3 100644 --- a/metagpt/roles/PPTmaker.py +++ b/metagpt/roles/PPTmaker.py @@ -30,11 +30,8 @@ class PPTMaker(RoleZero): goal: str = "Generate high-quality LaTeX presentations in Beamer format" constraints: str = "Call tools in predefined order, may terminate early based on validation feedback" - max_steps: int = 7 + max_steps: int = 7 is_completed: bool = False - - optimized_result: str = "" - validator_feedback: str = "" curr_step: int = 0 ACTION_SEQUENCE_METADATA: List[Dict[str, Any]] = [ @@ -53,8 +50,6 @@ def _reset_state(self): """Reset internal state to prepare for a new task""" self.curr_step = 0 self.is_completed = False - self.validator_feedback = "" - self.optimized_result = "" logger.info(f"{self.name} state has been reset") @staticmethod @@ -79,14 +74,9 @@ async def _think(self) -> bool: This is called by the base RoleZero's _react loop. Manages cycling through actions for BY_ORDER mode up to max_steps. """ - if self.is_completed: - logger.info(f"{self.name} task marked as completed. Stopping.") - return False - - if self.curr_step >= self.max_steps: - logger.info(f"{self.name} reached maximum steps ({self.max_steps}). Stopping.") + if self.is_completed or self.curr_step >= self.max_steps: return False - + # Determine the next action index in the sequence based on current step next_action_idx_in_sequence = self.curr_step % len(self.ACTION_SEQUENCE_METADATA) self._set_state(next_action_idx_in_sequence) # Set self.rc.state and self.rc.todo @@ -111,36 +101,33 @@ async def _act(self) -> Message: current_action_meta = self.ACTION_SEQUENCE_METADATA[action_idx_in_sequence] tool_instance: Action = self.actions[action_idx_in_sequence] - tool_name = current_action_meta["name"] - save_result_flag = current_action_meta["save_result"] - logger.info(f"{self.name} performing action: {tool_name} (Overall Step: {self.curr_step + 1}, Action Index: {action_idx_in_sequence})") + logger.info(f"{self.name} performing action: {tool_name} (Current Step: {self.curr_step + 1}, Action Index: {action_idx_in_sequence})") result_content_str = "" try: initial_request_str = self.rc.history[0].content if self.rc.history else "" - input_for_action = self.optimized_result if isinstance(tool_instance, ValidatorAction) else initial_request_str + + input_mapping = { + LatexGeneratorAction: lambda: ( + f"Original Request:\n{initial_request_str}\n\nFeedback for improvement:\n{self.rc.history[-1].content}" + if self.curr_step > 1 else initial_request_str + ), + ValidatorAction: lambda: self.rc.history[-1].content + } - if isinstance(tool_instance, LatexGeneratorAction) and self.validator_feedback: - # Append feedback to the original request for the generator - input_for_action = f"Original Request:\n{initial_request_str}\n\nFeedback for improvement:\n{self.validator_feedback}" + input_provider = input_mapping.get(type(tool_instance), lambda: initial_request_str) + input_for_action = input_provider() result_content_str = await tool_instance.run( request=input_for_action, history=self.rc.history ) - - if save_result_flag: # update self.optimized_result - self.optimized_result = result_content_str - - if isinstance(tool_instance, ValidatorAction): - self.validator_feedback = result_content_str - if "No further feedback" in result_content_str: - self.is_completed = True - logger.info(f"{self.name} task deemed completed by validator.") - elif isinstance(tool_instance, LatexGeneratorAction): - self.validator_feedback = "" + + if isinstance(tool_instance, ValidatorAction) and "No further feedback" in result_content_str: + self.is_completed = True + logger.info(f"{self.name} task deemed completed by validator.") # Add action's direct result to memory self.rc.memory.add(Message(content=result_content_str, role=self.profile, cause_by=type(tool_instance), sent_from=self.name)) @@ -151,11 +138,9 @@ async def _act(self) -> Message: return Message(content=f"Error executing {tool_name}: {str(e)}", role=self.profile, cause_by=type(tool_instance)) self.curr_step += 1 - - display_content = result_content_str return Message( - content=display_content or f"Step {self.curr_step}/{self.max_steps} ({tool_name}) completed.", # self.curr_step is already incremented + content=result_content_str or f"Step {self.curr_step}/{self.max_steps} ({tool_name}) completed.", role=self.profile, cause_by=type(tool_instance) ) @@ -181,31 +166,11 @@ async def run(self, prompt: Message) -> Message: await self._react() # Finalize the task and save the result - final_content_to_save = self.optimized_result - status_message = "Unknown" - - if self.is_completed: - logger.info(f"{self.name} task completed successfully.") - status_message = "Completed" - elif self.curr_step >= self.max_steps: - logger.info(f"{self.name} reached maximum steps ({self.max_steps}).") - status_message = "Reached max steps" - else: - logger.info(f"{self.name} task did not complete normally (curr_step: {self.curr_step}, is_completed: {self.is_completed}).") - status_message = "Stopped or Errored" - - if not final_content_to_save: - final_content_to_save = "No content was finalized." - - final_report_str = f"Generation task status: {status_message}.\n\n" - if self.is_completed or self.optimized_result: - final_report_str += f"Final LaTeX content:\n\n{final_content_to_save}" - else: - final_report_str += f"Last relevant output:\n\n{final_content_to_save}" + final_content_to_save = self.rc.history[-1].content if "LatexGeneratorAction" in self.rc.history[-1].cause_by else self.rc.history[-2].content + status_message = "Completed" if self.is_completed else ("Reached max steps" if self.curr_step >= self.max_steps else "Stopped or Errored") + final_report_str = f"Generation task status: {status_message}.\n\nFinal LaTeX content:\n\n{final_content_to_save}" self.save_md(final_content_to_save, filename="presentation_output.md") - logger.info(f"Final result saved. Optimized result length: {len(final_content_to_save)}") + logger.info(f"Final result saved. Content length: {len(final_content_to_save)}") - return Message(content=final_report_str, role=self.profile) - - \ No newline at end of file + return Message(content=final_report_str, role=self.profile) \ No newline at end of file