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/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..d5284025b3 --- /dev/null +++ b/metagpt/roles/PPTmaker.py @@ -0,0 +1,176 @@ +#!/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. +""" + +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.utils.common import handle_exception +from metagpt.actions.add_requirement import UserRequirement + + +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 + is_completed: bool = False + 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([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 + 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. + """ + workspace_dir = os.path.join(os.getcwd(), "workspace") + os.makedirs(workspace_dir, exist_ok=True) + save_path = os.path.join(workspace_dir, filename) + + 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: + """ + 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. + """ + 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 + + 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 + + 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"] + + 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_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 + } + + 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 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)) + + except Exception as 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=result_content_str or f"Step {self.curr_step}/{self.max_steps} ({tool_name}) completed.", + 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) + 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.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. Content length: {len(final_content_to_save)}") + + return Message(content=final_report_str, role=self.profile) \ No newline at end of file