diff --git a/mm_agents/aworldguiagent/README.md b/mm_agents/aworldguiagent/README.md new file mode 100644 index 0000000..dcc43a2 --- /dev/null +++ b/mm_agents/aworldguiagent/README.md @@ -0,0 +1,70 @@ +# aworldGUIAgent-v1 + +aworldGUIAgent-v1 built on the [AWorld Framework](https://github.com/inclusionAI/AWorld), specifically designed to tackle complex desktop automation tasks within the [OSWorld-verified](https://os-world.github.io/) benchmark. + +The core logic for our agent's perception and reasoning is adapted from the great work of the [Agent-S project](https://github.com/simular-ai/Agent-S). We have built upon their foundation by introducing a suite of new executable tools that enhance the agent's ability to interact with the OS environment. + +## Quick Start + +Follow these steps to set up the environment and reproduce our results. + +1. **Create Environment & Set Up OSWorld**: + * First, create a dedicated Conda environment with **Python 3.11**. + ```bash + conda create -n osworld_env python=3.11 + conda activate osworld_env + ``` + * Next, follow the official setup guide in the [OSWorld README](https://github.com/xlang-ai/OSWorld) to install OSWorld and its dependencies. + +2. **Install AWorld Framework**: + * Install the specific version of the AWorld Framework into the **same environment**. + ```bash + # Make sure your osworld_env is still activated + git clone https://github.com/inclusionAI/AWorld.git + cd AWorld + git checkout osworld_benchmark + python setup.py install + ``` + +3. **Run the Evaluation Script**: + * Our results were achieved using `openai/o3` for reasoning and `bytedance/ui-tars-1.5-7b` for visual grounding, both accessed via OpenRouter. + * Remember to replace placeholders like `YOUR_OPENROUTER_API_KEY` and `/path/to/your/vm/Ubuntu.vmx` with your actual credentials and paths. + + ```bash + # Activate your OSWorld conda environment (e.g., osworld_env) + conda activate osworld_env + + # Run the evaluation with the recommended settings + python run_multienv_aworldguiagent.py \ + --headless \ + --ground_url YOUR_BASE_URL \ + --ground_api_key YOUR_API_KEY \ + --ground_model bytedance/ui-tars-1.5-7b \ + --ground_provider open_router \ + --model_url YOUR_BASE_URL \ + --model_api_key YOUR_API_KEY \ + --model_temperature 1.0 \ + --provider_name vmware \ + --path_to_vm /path/to/your/vm/Ubuntu.vmx \ + --max_steps 50 \ + --model_provider open_router \ + --model openai/o3 \ + --grounding_width 1920 \ + --grounding_height 1080 \ + --test_all_meta_path evaluation_examples/test_all.json \ + --result_dir ./results \ + --observation_type screenshot \ + --num_envs 1 \ + --region us-east-1 \ + --client_password osworld-public-evaluation + ``` + +## Acknowledgements + +This work would not have been possible without building upon the foundations of several incredible open-source projects. + +- **AWorld Framework**: We thank the developers of the [AWorld Framework](https://github.com/inclusionAI/AWorld) for providing a powerful and flexible platform for agent development. The AWorld Framework is designed for agent training and is especially suited for complex multi-agent scenarios. If you have requirements for designing or experimenting with multi-agent systems, we highly recommend you explore the AWorld Framework further. + +- **Agent-S**: We extend our sincere gratitude to the creators of the [Agent-S project](https://github.com/simular-ai/Agent-S). The core agent logic in our implementation is adapted and enhanced from their codebase. We built upon their work by adding a suite of executable tools to improve the agent's interaction with the OS environment, which effectively boosted the stability and capability of our CUA Agent. + +- **OSWorld Benchmark**: We are grateful to the creators of the [OSWorld Benchmark](https://os-world.github.io/) for developing a challenging and comprehensive testbed for GUI agents. \ No newline at end of file diff --git a/mm_agents/aworldguiagent/agent.py b/mm_agents/aworldguiagent/agent.py new file mode 100644 index 0000000..6dc21f9 --- /dev/null +++ b/mm_agents/aworldguiagent/agent.py @@ -0,0 +1,99 @@ +""" +This code is adapted from AgentS2 (https://github.com/simular-ai/Agent-S) +with modifications to suit specific requirements. +""" +import logging +import platform +from typing import Dict, List, Tuple + +from mm_agents.aworldguiagent.grounding import ACI +from mm_agents.aworldguiagent.workflow import Worker + +logger = logging.getLogger("desktopenv.agent") + + +class UIAgent: + """Base class for UI automation agents""" + + """""" + + def __init__( + self, + engine_params: Dict, + grounding_agent: ACI, + platform: str = platform.system().lower(), + ): + """Initialize UIAgent + + Args: + engine_params: Configuration parameters for the LLM engine + grounding_agent: Instance of ACI class for UI interaction + platform: Operating system platform (macos, linux, windows) + """ + self.engine_params = engine_params + self.grounding_agent = grounding_agent + self.platform = platform + + def reset(self) -> None: + """Reset agent state""" + pass + + def predict(self, instruction: str, observation: Dict) -> Tuple[Dict, List[str]]: + """Generate next action prediction + + Args: + instruction: Natural language instruction + observation: Current UI state observation + + Returns: + Tuple containing agent info dictionary and list of actions + """ + pass + + +class AworldGUIAgent(UIAgent): + """Agent that uses no hierarchy for less inference time""" + + def __init__( + self, + engine_params: Dict, + grounding_agent: ACI, + platform: str = platform.system().lower(), + max_trajectory_length: int = 8, + enable_reflection: bool = True, + ): + """Initialize a minimalist AgentS2 without hierarchy + + Args: + engine_params: Configuration parameters for the LLM engine + grounding_agent: Instance of ACI class for UI interaction + platform: Operating system platform (darwin, linux, windows) + max_trajectory_length: Maximum number of image turns to keep + enable_reflection: Creates a reflection agent to assist the worker agent + """ + + super().__init__(engine_params, grounding_agent, platform) + self.max_trajectory_length = max_trajectory_length + self.enable_reflection = enable_reflection + self.reset() + + def reset(self) -> None: + """Reset agent state and initialize components""" + self.executor = Worker( + engine_params=self.engine_params, + grounding_agent=self.grounding_agent, + platform=self.platform, + max_trajectory_length=self.max_trajectory_length, + enable_reflection=self.enable_reflection, + ) + + def predict(self, instruction: str, observation: Dict) -> Tuple[Dict, List[str]]: + # Initialize the three info dictionaries + executor_info, actions = self.executor.generate_next_action( + instruction=instruction, obs=observation + ) + + # concatenate the three info dictionaries + info = {**{k: v for d in [executor_info or {}] for k, v in d.items()}} + + return info, actions diff --git a/mm_agents/aworldguiagent/grounding.py b/mm_agents/aworldguiagent/grounding.py new file mode 100644 index 0000000..2410329 --- /dev/null +++ b/mm_agents/aworldguiagent/grounding.py @@ -0,0 +1,5252 @@ +""" +This code is adapted from AgentS2 (https://github.com/simular-ai/Agent-S) +with modifications to suit specific requirements. +""" +import ast +import re +import textwrap +from collections import defaultdict +from io import BytesIO +from typing import Any, Dict, List, Optional, Tuple, Union + +import pytesseract +from PIL import Image +from pytesseract import Output + +from aworld.models.llm import get_llm_model, call_llm_model +from aworld.config.conf import AgentConfig + +from mm_agents.aworldguiagent.utils import encode_image, parse_single_code_from_string + +class ACI: + def __init__(self): + self.notes: List[str] = [] + + +# Agent action decorator +def agent_action(func): + func.is_agent_action = True + return func + + +UBUNTU_APP_SETUP = f"""import subprocess; +import difflib; +import pyautogui; +pyautogui.press('escape'); +time.sleep(0.5); +output = subprocess.check_output(['wmctrl', '-lx']); +output = output.decode('utf-8').splitlines(); +window_titles = [line.split(None, 4)[2] for line in output]; +closest_matches = difflib.get_close_matches('APP_NAME', window_titles, n=1, cutoff=0.1); +if closest_matches: + closest_match = closest_matches[0]; + for line in output: + if closest_match in line: + window_id = line.split()[0] + break; +subprocess.run(['wmctrl', '-ia', window_id]) +subprocess.run(['wmctrl', '-ir', window_id, '-b', 'add,maximized_vert,maximized_horz']) +""" + + +SET_CELL_VALUES_CMD = """import uno +import subprocess + +def identify_document_type(component): + if component.supportsService("com.sun.star.sheet.SpreadsheetDocument"): + return "Calc" + + if component.supportsService("com.sun.star.text.TextDocument"): + return "Writer" + + if component.supportsService("com.sun.star.sheet.PresentationDocument"): + return "Impress" + + return None + +def cell_ref_to_indices(cell_ref): + column_letters = ''.join(filter(str.isalpha, cell_ref)) + row_number = ''.join(filter(str.isdigit, cell_ref)) + + col = sum((ord(char.upper()) - ord('A') + 1) * (26**idx) for idx, char in enumerate(reversed(column_letters))) - 1 + row = int(row_number) - 1 + return col, row + +def set_cell_values(new_cell_values: dict[str, str], app_name: str = "Untitled 1", sheet_name: str = "Sheet1"): + new_cell_values_idx = {{}} + for k, v in new_cell_values.items(): + try: + col, row = cell_ref_to_indices(k) + except: + col = row = None + + if col is not None and row is not None: + new_cell_values_idx[(col, row)] = v + + # Clean up previous TCP connections. + # subprocess.run( + # 'echo \"osworld-public-evaluation\" | sudo -S ss --kill --tcp state TIME-WAIT sport = :2002', + # shell=True, + # check=True, + # text=True, + # capture_output=True + # ) + + # Dynamically allow soffice to listen on port 2002. + subprocess.run( + [ + "soffice", + "--accept=socket,host=localhost,port=2002;urp;StarOffice.Service" + ] + ) + + local_context = uno.getComponentContext() + resolver = local_context.ServiceManager.createInstanceWithContext( + "com.sun.star.bridge.UnoUrlResolver", local_context + ) + context = resolver.resolve( + f"uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext" + ) + desktop = context.ServiceManager.createInstanceWithContext( + "com.sun.star.frame.Desktop", context + ) + + # Collect all LibreOffice-related opened windows. + documents = [] + for i, component in enumerate(desktop.Components): + title = component.Title + doc_type = identify_document_type(component) + documents.append((i, component, title, doc_type)) + + # Find the LibreOffice Calc app and the sheet of interest. + spreadsheet = [doc for doc in documents if doc[3] == "Calc"] + selected_spreadsheet = [doc for doc in spreadsheet if doc[2] == app_name] + if spreadsheet: + try: + if selected_spreadsheet: + spreadsheet = selected_spreadsheet[0][1] + else: + spreadsheet = spreadsheet[0][1] + + sheet = spreadsheet.Sheets.getByName(sheet_name) + except: + raise ValueError(f"Could not find sheet {{sheet_name}} in {{app_name}}.") + + for (col, row), value in new_cell_values_idx.items(): + cell = sheet.getCellByPosition(col, row) + + # Set the cell value. + if isinstance(value, (int, float)): + cell.Value = value + elif isinstance(value, str): + if value.startswith("="): + cell.Formula = value + else: + cell.String = value + elif isinstance(value, bool): + cell.Value = 1 if value else 0 + elif value is None: + cell.clearContents(0) + else: + raise ValueError(f"Unsupported cell value type: {{type(value)}}") + + else: + raise ValueError(f"Could not find LibreOffice Calc app corresponding to {{app_name}}.") + +set_cell_values(new_cell_values={cell_values}, app_name="{app_name}", sheet_name="{sheet_name}") +""" + + +# ACI primitives are parameterized by description, and coordinate generation uses a pretrained grounding model +class OSWorldACI(ACI): + def __init__( + self, + platform: str, + engine_params_for_generation: Dict, + engine_params_for_grounding: Dict, + width: int = 1920, + height: int = 1080, + ): + self.platform = ( + platform # Dictates how the switch_applications agent action works. + ) + + # Configure scaling + self.width = width + self.height = height + + # Maintain state for save_to_knowledge + self.notes = [] + + # Coordinates used during ACI execution + self.coords1 = None + self.coords2 = None + + # Configure the visual grounding model responsible for coordinate generation + llm_config = AgentConfig( + llm_provider=engine_params_for_grounding.get("engine_type", "openai"), + llm_model_name=engine_params_for_grounding.get("model", "bytedance/ui-tars-1.5-7b"), + llm_temperature=1, + llm_base_url=engine_params_for_grounding.get("base_url", "https://openrouter.ai/api/v1"), + llm_api_key=engine_params_for_grounding.get("api_key", "") + ) + + self.grounding_model = get_llm_model(llm_config) + self.engine_params_for_grounding = engine_params_for_grounding + + # Configure text grounding agent + self.text_span_model = get_llm_model(llm_config) + + # Given the state and worker's referring expression, use the grounding model to generate (x,y) + def generate_coords(self, ref_expr: str, obs: Dict) -> List[int]: + + # Reset the grounding model state + # self.grounding_model.reset() + + # Configure the context, UI-TARS demo does not use system prompt + prompt = f"Query:{ref_expr}\nOutput only the coordinate of one point in your response.\n" + # self.grounding_model.add_message( + # text_content=prompt, image_content=obs["screenshot"], put_text_last=True + # ) + + grounding_message = [{ + "role": "user", + "content": [ + { + "type": "text", + "text": prompt + }, + { + "type": "image_url", + "image_url": { + "url": "data:image/png;base64," + encode_image(obs["screenshot"]) + } + } + ] + }] + + response = call_llm_model( + llm_model=self.grounding_model, + messages=grounding_message + ).content + + + # Generate and parse coordinates + # response = call_llm_safe(self.grounding_model) + print("RAW GROUNDING MODEL RESPONSE:", response) + numericals = re.findall(r"\d+", response) + assert len(numericals) >= 2 + return [int(numericals[0]), int(numericals[1])] + + # Calls pytesseract to generate word level bounding boxes for text grounding + def get_ocr_elements(self, b64_image_data: str) -> Tuple[str, List]: + image = Image.open(BytesIO(b64_image_data)) + image_data = pytesseract.image_to_data(image, output_type=Output.DICT) + + # Clean text by removing leading and trailing spaces and non-alphabetical characters, but keeping punctuation + for i, word in enumerate(image_data["text"]): + image_data["text"][i] = re.sub( + r"^[^a-zA-Z\s.,!?;:\-\+]+|[^a-zA-Z\s.,!?;:\-\+]+$", "", word + ) + + ocr_elements = [] + ocr_table = "Text Table:\nWord id\tText\n" + # Obtain the for each valid element + grouping_map = defaultdict(list) + ocr_id = 0 + for i in range(len(image_data["text"])): + block_num = image_data["block_num"][i] + if image_data["text"][i]: + grouping_map[block_num].append(image_data["text"][i]) + ocr_table += f"{ocr_id}\t{image_data['text'][i]}\n" + ocr_elements.append( + { + "id": ocr_id, + "text": image_data["text"][i], + "group_num": block_num, + "word_num": len(grouping_map[block_num]), + "left": image_data["left"][i], + "top": image_data["top"][i], + "width": image_data["width"][i], + "height": image_data["height"][i], + } + ) + ocr_id += 1 + + return ocr_table, ocr_elements + # Given the state and worker's text phrase, generate the coords of the first/last word in the phrase + def generate_text_coords( + self, phrase: str, obs: Dict, alignment: str = "" + ) -> List[int]: + + ocr_table, ocr_elements = self.get_ocr_elements(obs["screenshot"]) + + PHRASE_TO_WORD_COORDS_PROMPT = textwrap.dedent( + """ + You are an expert in graphical user interfaces. Your task is to process a phrase of text, and identify the most relevant word on the computer screen. + You are provided with a phrase, a table with all the text on the screen, and a screenshot of the computer screen. You will identify the single word id that is best associated with the provided phrase. + This single word must be displayed on the computer screenshot, and its location on the screen should align with the provided phrase. + Each row in the text table provides 2 pieces of data in the following order. 1st is the unique word id. 2nd is the corresponding word. + + To be successful, it is very important to follow all these rules: + 1. First, think step by step and generate your reasoning about which word id to click on. + 2. Then, output the unique word id. Remember, the word id is the 1st number in each row of the text table. + 3. If there are multiple occurrences of the same word, use the surrounding context in the phrase to choose the correct one. Pay very close attention to punctuation and capitalization. + + """ + ) + + + grounding_message = [] + system_message = { + "role": "system", + "content": [{ + "type": "text", + "text": PHRASE_TO_WORD_COORDS_PROMPT + }]} + + grounding_message.append(system_message) + + alignment_prompt = "" + if alignment == "start": + alignment_prompt = "**Important**: Output the word id of the FIRST word in the provided phrase.\n" + elif alignment == "end": + alignment_prompt = "**Important**: Output the word id of the LAST word in the provided phrase.\n" + + # Load LLM prompt + # self.text_span_agent.reset() + grounding_message.append( + { + "role": "user", + "content": [{ + "type": "text", + "text": alignment_prompt + "Phrase: " + phrase + "\n" + ocr_table + }]} + ) + + # Obtain the target element + # response = call_llm_safe(self.text_span_agent) + response = call_llm_model( + llm_model=self.text_span_model, + messages=grounding_message + ).content + + print("TEXT SPAN AGENT RESPONSE:", response) + numericals = re.findall(r"\d+", response) + if len(numericals) > 0: + text_id = int(numericals[-1]) + else: + text_id = 0 + elem = ocr_elements[text_id] + + # Compute the element coordinates + if alignment == "start": + coords = [elem["left"], elem["top"] + (elem["height"] // 2)] + elif alignment == "end": + coords = [elem["left"] + elem["width"], elem["top"] + (elem["height"] // 2)] + else: + coords = [ + elem["left"] + (elem["width"] // 2), + elem["top"] + (elem["height"] // 2), + ] + return coords + + # Takes a description based action and assigns the coordinates for any coordinate based action + # Raises an error if function can't be parsed + def assign_coordinates(self, plan: str, obs: Dict): + + # Reset coords from previous action generation + self.coords1, self.coords2 = None, None + + try: + # Extract the function name and args + action = parse_single_code_from_string(plan.split("Grounded Action")[-1]) + function_name = re.match(r"(\w+\.\w+)\(", action).group(1) + args = self.parse_function_args(action) + except Exception as e: + raise RuntimeError(f"Error in parsing grounded action: {e}") from e + + # arg0 is a description + if ( + function_name in ["agent.click", "agent.type", "agent.scroll"] + and len(args) >= 1 + and args[0] != None + ): + self.coords1 = self.generate_coords(args[0], obs) + # arg0 and arg1 are descriptions + elif function_name == "agent.drag_and_drop" and len(args) >= 2: + self.coords1 = self.generate_coords(args[0], obs) + self.coords2 = self.generate_coords(args[1], obs) + # arg0 and arg1 are text phrases + elif function_name == "agent.highlight_text_span" and len(args) >= 2: + self.coords1 = self.generate_text_coords(args[0], obs, alignment="start") + self.coords2 = self.generate_text_coords(args[1], obs, alignment="end") + + # Resize from grounding model dim into OSWorld dim (1920 * 1080) + def resize_coordinates(self, coordinates: List[int]) -> List[int]: + grounding_width = self.engine_params_for_grounding["grounding_width"] + grounding_height = self.engine_params_for_grounding["grounding_height"] + + return [ + round(coordinates[0] * self.width / grounding_width), + round(coordinates[1] * self.height / grounding_height), + ] + + # Given a generated ACI function, returns a list of argument values, where descriptions are at the front of the list + def parse_function_args(self, function: str) -> List[str]: + tree = ast.parse(function) + call_node = tree.body[0].value + + def safe_eval(node): + if isinstance( + node, ast.Constant + ): # Handles literals like numbers, strings, etc. + return node.value + else: + return ast.unparse(node) # Return as a string if not a literal + + positional_args = [safe_eval(arg) for arg in call_node.args] + keyword_args = {kw.arg: safe_eval(kw.value) for kw in call_node.keywords} + + res = [] + + for key, val in keyword_args.items(): + if "description" in key: + res.append(val) + + for arg in positional_args: + res.append(arg) + + return res + + @agent_action + def click( + self, + element_description: str, + num_clicks: int = 1, + button_type: str = "left", + hold_keys: List = [], + ): + """Click on the element + Args: + element_description:str, a detailed descriptions of which element to click on. This description should be at least a full sentence. + num_clicks:int, number of times to click the element + button_type:str, which mouse button to press can be "left", "middle", or "right" + hold_keys:List, list of keys to hold while clicking + """ + x, y = self.resize_coordinates(self.coords1) + command = "import pyautogui; " + + # TODO: specified duration? + for k in hold_keys: + command += f"pyautogui.keyDown({repr(k)}); " + command += f"""import pyautogui; pyautogui.click({x}, {y}, clicks={num_clicks}, button={repr(button_type)}); """ + for k in hold_keys: + command += f"pyautogui.keyUp({repr(k)}); " + + # === 新增代码:在这里添加1秒的等待 === + command += "time.sleep(2); " + # Return pyautoguicode to click on the element + return command + + @agent_action + def switch_applications(self, app_code): + """Switch to a different application that is already open + Args: + app_code:str the code name of the application to switch to from the provided list of open applications + """ + if self.platform == "darwin": + return f"import pyautogui; import time; pyautogui.hotkey('command', 'space', interval=0.5); pyautogui.typewrite({repr(app_code)}); pyautogui.press('enter'); time.sleep(1.0)" + elif self.platform == "linux": + return UBUNTU_APP_SETUP.replace("APP_NAME", app_code) + elif self.platform == "windows": + return f"import pyautogui; import time; pyautogui.hotkey('win', 'd', interval=0.5); pyautogui.typewrite({repr(app_code)}); pyautogui.press('enter'); time.sleep(1.0)" + + @agent_action + def open(self, app_or_filename: str): + """Open any application or file with name app_or_filename. Use this action to open applications or files on the desktop, do not open manually. + Args: + app_or_filename:str, the name of the application or filename to open + """ + if self.platform == "linux": + return f"import pyautogui; pyautogui.hotkey('win'); time.sleep(0.5); pyautogui.write({repr(app_or_filename)}); time.sleep(1.0); pyautogui.hotkey('enter'); time.sleep(0.5)" + elif self.platform == "darwin": + return f"import pyautogui; import time; pyautogui.hotkey('command', 'space', interval=0.5); pyautogui.typewrite({repr(app_or_filename)}); pyautogui.press('enter'); time.sleep(1.0)" + + @agent_action + def type( + self, + element_description: Optional[str] = None, + text: str = "", + overwrite: bool = False, + enter: bool = False, + ): + """Type text into a specific element + Args: + element_description:str, a detailed description of which element to enter text in. This description should be at least a full sentence. + text:str, the text to type + overwrite:bool, Assign it to True if the text should overwrite the existing text, otherwise assign it to False. Using this argument clears all text in an element. + enter:bool, Assign it to True if the enter key should be pressed after typing the text, otherwise assign it to False. + """ + + select_mod = "command" if self.platform == "darwin" else "ctrl" + + if self.coords1 is not None: + # If a node is found, retrieve its coordinates and size + # Start typing at the center of the element + + x, y = self.resize_coordinates(self.coords1) + + command = "import pyautogui; " + # command += f"pyautogui.click({x}, {y}); " + # 修改成double click + command += f"pyautogui.doubleClick({x}, {y}); " + + if overwrite: + command += ( + f"pyautogui.hotkey({repr(select_mod)}, 'a'); " + "pyautogui.press('backspace'); " + ) + + command += f"pyautogui.write({repr(text)}); " + + if enter: + command += "pyautogui.press('enter'); " + else: + # If no element is found, start typing at the current cursor location + command = "import pyautogui; " + + if overwrite: + command += ( + f"pyautogui.hotkey({repr(select_mod)}, 'a'); " + "pyautogui.press('backspace'); " + ) + + command += f"pyautogui.write({repr(text)}); " + + if enter: + command += "pyautogui.press('enter'); " + + # === 新增代码:在这里添加1秒的等待 === + command += "time.sleep(2); " + + return command + + @agent_action + def save_to_knowledge(self, text: List[str]): + """Save facts, elements, texts, etc. to a long-term knowledge bank for reuse during this task. Can be used for copy-pasting text, saving elements, etc. + Args: + text:List[str] the text to save to the knowledge + """ + self.notes.extend(text) + return """WAIT""" + + @agent_action + def drag_and_drop( + self, starting_description: str, ending_description: str, hold_keys: List = [] + ): + """Drag from the starting description to the ending description + Args: + starting_description:str, a very detailed description of where to start the drag action. This description should be at least a full sentence. + ending_description:str, a very detailed description of where to end the drag action. This description should be at least a full sentence. + hold_keys:List list of keys to hold while dragging + """ + x1, y1 = self.resize_coordinates(self.coords1) + x2, y2 = self.resize_coordinates(self.coords2) + + command = "import pyautogui; " + + command += f"pyautogui.moveTo({x1}, {y1}); " + # TODO: specified duration? + for k in hold_keys: + command += f"pyautogui.keyDown({repr(k)}); " + command += f"pyautogui.dragTo({x2}, {y2}, duration=1., button='left'); pyautogui.mouseUp(); " + for k in hold_keys: + command += f"pyautogui.keyUp({repr(k)}); " + + # Return pyautoguicode to drag and drop the elements + + # === 新增代码:在这里添加1秒的等待 === + command += "time.sleep(2); " + + return command + + @agent_action + def highlight_text_span( + self, starting_phrase: str, ending_phrase: str, button: str = "left" + ): + """Highlight a text span between a provided starting phrase and ending phrase. Use this to highlight words, lines, and paragraphs. + Args: + starting_phrase:str, the phrase that denotes the start of the text span you want to highlight. If you only want to highlight one word, just pass in that single word. + ending_phrase:str, the phrase that denotes the end of the text span you want to highlight. If you only want to highlight one word, just pass in that single word. + button:str, the button to use to highlight the text span. Defaults to "left". Can be "left", "right", or "middle". + """ + + x1, y1 = self.coords1 + x2, y2 = self.coords2 + + command = "import pyautogui; " + command += f"pyautogui.moveTo({x1}, {y1}); " + command += f"pyautogui.dragTo({x2}, {y2}, duration=1., button='{button}'); pyautogui.mouseUp(); " + + # Return pyautoguicode to drag and drop the elements + + # === 新增代码:在这里添加1秒的等待 === + command += "time.sleep(2); " + + return command + + @agent_action + def set_cell_values( + self, cell_values: Dict[str, Any], app_name: str, sheet_name: str + ): + """Use this to set individual cell values in a spreadsheet. For example, setting A2 to "hello" would be done by passing {"A2": "hello"} as cell_values. The sheet must be opened before this command can be used. + Args: + cell_values: Dict[str, Any], A dictionary of cell values to set in the spreadsheet. The keys are the cell coordinates in the format "A1", "B2", etc. + Supported value types include: float, int, string, bool, formulas. + app_name: str, The name of the spreadsheet application. For example, "Some_sheet.xlsx". + sheet_name: str, The name of the sheet in the spreadsheet. For example, "Sheet1". + """ + return SET_CELL_VALUES_CMD.format( + cell_values=cell_values, app_name=app_name, sheet_name=sheet_name + ) + + @agent_action + def scroll(self, element_description: str, clicks: int, shift: bool = False): + """Scroll the element in the specified direction + Args: + element_description:str, a very detailed description of which element to enter scroll in. This description should be at least a full sentence. + clicks:int, the number of clicks to scroll can be positive (up) or negative (down). + shift:bool, whether to use shift+scroll for horizontal scrolling + """ + + x, y = self.resize_coordinates(self.coords1) + + if shift: + return f"import pyautogui; import time; pyautogui.moveTo({x}, {y}); time.sleep(0.5); pyautogui.hscroll({clicks})" + else: + return f"import pyautogui; import time; pyautogui.moveTo({x}, {y}); time.sleep(0.5); pyautogui.vscroll({clicks})" + + @agent_action + def hotkey(self, keys: List): + """Press a hotkey combination + Args: + keys:List the keys to press in combination in a list format (e.g. ['ctrl', 'c']) + """ + # add quotes around the keys + keys = [f"'{key}'" for key in keys] + return f"import pyautogui; pyautogui.hotkey({', '.join(keys)}); time.sleep(2);" + + + @agent_action + def hold_and_press(self, hold_keys: List, press_keys: List): + """Hold a list of keys and press a list of keys + Args: + hold_keys:List, list of keys to hold + press_keys:List, list of keys to press in a sequence + """ + + press_keys_str = "[" + ", ".join([f"'{key}'" for key in press_keys]) + "]" + command = "import pyautogui; " + for k in hold_keys: + command += f"pyautogui.keyDown({repr(k)}); " + command += f"pyautogui.press({press_keys_str}); " + for k in hold_keys: + command += f"pyautogui.keyUp({repr(k)}); " + + return command + + @agent_action + def wait(self, time: float): + """Wait for a specified amount of time + Args: + time:float the amount of time to wait in seconds + """ + return f"""import time; time.sleep({time})""" + + @agent_action + def done( + self, + return_value: Optional[Union[Dict, str, List, Tuple, int, float, bool]] = None, + ): + """End the current task with a success and the required return value""" + self.returned_info = return_value + return """DONE""" + + @agent_action + def fail(self): + """End the current task with a failure, and replan the whole task.""" + return """FAIL""" + + CODE_LAUNCH_VSCODE_CMD = """import json +import os +import subprocess +from pathlib import Path +ret = "" + +def code_launch_vscode(path): + global ret + try: + subprocess.run(["code", "-r", path], check=True) + ret = "Successfully launched VS Code" + except subprocess.CalledProcessError as e: + ret = f"Error launching VS Code: {{e}}" + except Exception as e: + ret = f"Unexpected error: {{e}}" + + return ret +code_launch_vscode(path={path}) +print(ret) +""" + + @agent_action + def code_launch_vscode(self, path): + + return self.CODE_LAUNCH_VSCODE_CMD.format(path=repr(path)) + + CODE_COMPARE_FILES_CMD = """import json +import os +import subprocess +from pathlib import Path +ret = "" + +def code_compare_files(file1, file2): + global ret + try: + # 获取compare结果 + subprocess.run(["code", "-d", file1, file2], check=True) + ret = "The compared files are opened in VSCode" + except subprocess.CalledProcessError as e: + ret = f"Error comparing files: {{e}}" + except Exception as e: + ret = f"Unexpected error: {{e}}" + + return ret +code_compare_files(file1={file1}, file2={file2}) +print(ret) +""" + + @agent_action + def code_compare_files(self, file1, file2): + + return self.CODE_COMPARE_FILES_CMD.format(file1=repr(file1), file2=repr(file2)) + + CODE_ADD_FOLDER_CMD = """import json +import os +import subprocess +from pathlib import Path +ret = "" + +def code_add_folder(folder): + global ret + try: + subprocess.run(["code", "-a", folder], check=True) + ret = "Successfully added folder" + except subprocess.CalledProcessError as e: + ret = f"Error adding folder: {{e}}" + except Exception as e: + ret = f"Unexpected error: {{e}}" + + return ret +code_add_folder(folder={folder}) +print(ret) +""" + + @agent_action + def code_add_folder(self, folder): + + return self.CODE_ADD_FOLDER_CMD.format(folder=repr(folder)) + + CODE_GOTO_FILE_CMD = """import json +import os +import subprocess +from pathlib import Path +ret = "" + +def code_goto_file(file_path, line=1, character=1): + global ret + try: + command = f"{{file_path}}:{{line}}:{{character}}" + subprocess.run(["code", "-g", command], check=True) + ret = "Successfully opened file, line: {{}}, character: {{}}".format(line, character) + except subprocess.CalledProcessError as e: + ret = f"Error going to file: {{e}}" + except Exception as e: + ret = f"Unexpected error: {{e}}" + + return ret +code_goto_file(file_path={file_path}, line={line}, character={character}) +print(ret) +""" + + @agent_action + def code_goto_file(self, file_path, line, character): + + return self.CODE_GOTO_FILE_CMD.format(file_path=repr(file_path), line=repr(line), character=repr(character)) + + LIBREOFFICE_CALC_SET_COLUMN_AS_TEXT_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_get_column_index(column_name): + try: + return ord(column_name[0]) - ord("A") + except ValueError: + return None + +def libreoffice_calc_get_last_used_row(): + cursor = sheet.createCursor() + cursor.gotoEndOfUsedArea(False) + return cursor.RangeAddress.EndRow + +def libreoffice_calc_set_column_as_text(column_name): + global ret + try: + # 获取列索引 + column_index = libreoffice_calc_get_column_index(column_name) + if column_index is None: + ret = f"Error: Invalid column name '{{column_name}}'" + return False + + # 获取最后使用的行 + last_row = libreoffice_calc_get_last_used_row() + if last_row == -1: + ret = "Error: No data found in sheet" + return False + + # 获取整列的范围 + cell_range = sheet.getCellRangeByPosition(column_index, 0, column_index, last_row) + + # 设置数字格式为文本格式 + # 使用 "@" 表示文本格式 + cell_range.NumberFormat = 0 # 重置格式 + + # 获取格式对象并设置为文本 + formats = doc.NumberFormats + locale = uno.createUnoStruct("com.sun.star.lang.Locale") + locale.Language = "en" + locale.Country = "US" + + # 创建文本格式 + text_format_key = formats.queryKey("@", locale, False) + if text_format_key == -1: + text_format_key = formats.addNew("@", locale) + + # 应用文本格式到整列 + cell_range.NumberFormat = text_format_key + + # 将现有数值转换为文本 + data_array = cell_range.getDataArray() + for row_idx, row in enumerate(data_array): + cell = sheet.getCellByPosition(column_index, row_idx) + current_value = cell.getValue() if cell.getType().value == "VALUE" else cell.getString() + if current_value != 0 or cell.getString() != "": + # 将值设置为字符串 + cell.setString(str(current_value)) + + ret = f"Successfully set column {{column_name}} as text format" + return True + + except Exception as e: + ret = f"Error: {{e}}" + return False +libreoffice_calc_set_column_as_text(column_name={column_name}) +print(ret) +""" + + @agent_action + def libreoffice_calc_set_column_as_text(self, column_name): + + return self.LIBREOFFICE_CALC_SET_COLUMN_AS_TEXT_CMD.format(column_name=repr(column_name)) + + CODE_PERFORM_MERGE_CMD = """import json +import os +import subprocess +from pathlib import Path +ret = "" + +def code_perform_merge(path1, path2, base, result): + global ret + try: + subprocess.run(["code", "-m", path1, path2, base, result], check=True) + ret = "Successfully performed merge" + except subprocess.CalledProcessError as e: + ret = f"Error performing merge: {{e}}" + except Exception as e: + ret = f"Unexpected error: {{e}}" + + return ret +code_perform_merge(path1={path1}, path2={path2}, base={base}, result={result}) +print(ret) +""" + + @agent_action + def code_perform_merge(self, path1, path2, base, result): + + return self.CODE_PERFORM_MERGE_CMD.format(path1=repr(path1), path2=repr(path2), base=repr(base), + result=repr(result)) + + CODE_REMOVE_FOLDER_CMD = """import json +import os +import subprocess +from pathlib import Path +ret = "" + +def code_remove_folder(folder): + global ret + try: + subprocess.run(["code", "--remove", folder], check=True) + ret = "Successfully removed folder" + except subprocess.CalledProcessError as e: + ret = f"Error removing folder: {{e}}" + except Exception as e: + ret = f"Unexpected error: {{e}}" + + return ret +code_remove_folder(folder={folder}) +print(ret) +""" + + @agent_action + def code_remove_folder(self, folder): + + return self.CODE_REMOVE_FOLDER_CMD.format(folder=repr(folder)) + + CODE_INSTALL_EXTENSION_CMD = """import json +import os +import subprocess +from pathlib import Path +ret = "" + +def code_install_extension(extension_id, pre_release=False): + global ret + try: + command = ["code", "--install-extension", extension_id] + if pre_release: + command.append("--pre-release") + subprocess.run(command, check=True) + ret = "Successfully installed extension" + except subprocess.CalledProcessError as e: + ret = f"Error installing extension: {{e}}" + except Exception as e: + ret = f"Unexpected error: {{e}}" + + return ret +code_install_extension(extension_id={extension_id}, pre_release={pre_release}) +print(ret) +""" + + @agent_action + def code_install_extension(self, extension_id, pre_release): + + return self.CODE_INSTALL_EXTENSION_CMD.format(extension_id=repr(extension_id), pre_release=repr(pre_release)) + + CODE_UNINSTALL_EXTENSION_CMD = """import json +import os +import subprocess +from pathlib import Path +ret = "" + +def code_uninstall_extension(extension_id): + global ret + try: + subprocess.run(["code", "--uninstall-extension", extension_id], check=True) + ret = "Successfully uninstalled extension" + except subprocess.CalledProcessError as e: + ret = f"Error uninstalling extension: {{e}}" + except Exception as e: + ret = f"Unexpected error: {{e}}" + + return ret +code_uninstall_extension(extension_id={extension_id}) +print(ret) +""" + + @agent_action + def code_uninstall_extension(self, extension_id): + + return self.CODE_UNINSTALL_EXTENSION_CMD.format(extension_id=repr(extension_id)) + + CODE_LIST_EXTENSIONS_CMD = """import json +import os +import subprocess +from pathlib import Path +ret = "" + +def code_list_extensions(show_versions=False, category=None): + global ret + try: + command = ["code", "--list-extensions"] + if show_versions: + command.append("--show-versions") + if category: + command.extend(["--category", category]) + ret = subprocess.run(command, check=True, capture_output=True, text=True).stdout + except subprocess.CalledProcessError as e: + ret = f"Error listing extensions: {{e}}" + except Exception as e: + ret = f"Unexpected error: {{e}}" + + return ret +code_list_extensions(show_versions={show_versions}, category={category}) +print(ret) +""" + + @agent_action + def code_list_extensions(self, show_versions, category): + + return self.CODE_LIST_EXTENSIONS_CMD.format(show_versions=repr(show_versions), category=repr(category)) + + CODE_UPDATE_EXTENSIONS_CMD = """import json +import os +import subprocess +from pathlib import Path +ret = "" + +def code_update_extensions(): + global ret + try: + subprocess.run(["code", "--update-extensions"], check=True) + ret = "Successfully updated extensions" + except subprocess.CalledProcessError as e: + ret = f"Error updating extensions: {{e}}" + except Exception as e: + ret = f"Unexpected error: {{e}}" + + return ret +code_update_extensions() +print(ret) +""" + + @agent_action + def code_update_extensions(self): + + return self.CODE_UPDATE_EXTENSIONS_CMD.format() + + CODE_DISABLE_EXTENSION_CMD = """import json +import os +import subprocess +from pathlib import Path +ret = "" + +def code_disable_extension(extension_id): + global ret + try: + subprocess.run(["code", "--disable-extension", extension_id], check=True) + ret = "Successfully disabled extension" + except subprocess.CalledProcessError as e: + ret = f"Error disabling extension: {{e}}" + except Exception as e: + ret = f"Unexpected error: {{e}}" + + return ret +code_disable_extension(extension_id={extension_id}) +print(ret) +""" + + @agent_action + def code_disable_extension(self, extension_id): + + return self.CODE_DISABLE_EXTENSION_CMD.format(extension_id=repr(extension_id)) + + CODE_TOGGLE_SYNC_CMD = """import json +import os +import subprocess +from pathlib import Path +ret = "" + +def code_toggle_sync(state): + global ret + try: + command = ["code", "--sync", state] + subprocess.run(command, check=True) + ret = "Successfully toggled sync" + except subprocess.CalledProcessError as e: + ret = f"Error toggling sync: {{e}}" + except Exception as e: + ret = f"Unexpected error: {{e}}" + + return ret +code_toggle_sync(state={state}) +print(ret) +""" + + @agent_action + def code_toggle_sync(self, state): + + return self.CODE_TOGGLE_SYNC_CMD.format(state=repr(state)) + + LIBREOFFICE_IMPRESS_SAVE_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_save(): + global ret + try: + if doc.hasLocation(): + doc.store() + ret = "Success" + else: + ret = "Error: Document has no save location" + return True + except Exception as e: + ret = f"Error: {{e}}" + return False +libreoffice_impress_save() +print(ret) +""" + + @agent_action + def libreoffice_impress_save(self): + + return self.LIBREOFFICE_IMPRESS_SAVE_CMD.format() + + LIBREOFFICE_IMPRESS_GO_TO_SLIDE_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_go_to_slide(slide_index): + global ret + try: + zero_based_index = slide_index - 1 + controller = doc.getCurrentController() + if not controller: + ret = "Error: Could not get document controller" + return False + pages = doc.getDrawPages() + if zero_based_index < 0 or zero_based_index >= pages.getCount(): + ret = f"Error: Slide index {{slide_index}} is out of range. Valid range is 1-{{pages.getCount()}}" + return False + target_slide = pages.getByIndex(zero_based_index) + controller.setCurrentPage(target_slide) + ret = f"Successfully navigated to slide {{slide_index}}" + return True + except Exception as e: + ret = f"Error navigating to slide: {{str(e)}}" + return False +libreoffice_impress_go_to_slide(slide_index={slide_index}) +print(ret) +""" + + @agent_action + def libreoffice_impress_go_to_slide(self, slide_index): + + return self.LIBREOFFICE_IMPRESS_GO_TO_SLIDE_CMD.format(slide_index=repr(slide_index)) + + LIBREOFFICE_IMPRESS_GET_SLIDE_COUNT_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_get_slide_count(): + global ret + try: + pages = doc.getDrawPages() + count = pages.getCount() + ret = count + return count + except Exception as e: + ret = f"Error: {{str(e)}}" + return 0 +libreoffice_impress_get_slide_count() +print(ret) +""" + + @agent_action + def libreoffice_impress_get_slide_count(self): + + return self.LIBREOFFICE_IMPRESS_GET_SLIDE_COUNT_CMD.format() + + LIBREOFFICE_IMPRESS_DUPLICATE_SLIDE_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_duplicate_slide(slide_index): + global ret + try: + zero_based_index = slide_index - 1 + draw_pages = doc.getDrawPages() + if zero_based_index < 0 or zero_based_index >= draw_pages.getCount(): + ret = f"Error: Invalid slide index {{slide_index}}. Valid range is 1 to {{draw_pages.getCount()}}" + return False + controller = doc.getCurrentController() + controller.setCurrentPage(draw_pages.getByIndex(zero_based_index)) + dispatcher = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.DispatchHelper", ctx) + frame = controller.getFrame() + dispatcher.executeDispatch(frame, ".uno:DuplicatePage", "", 0, ()) + duplicated_slide_index = zero_based_index + 1 + slide_count = draw_pages.getCount() + if duplicated_slide_index < slide_count - 1: + controller.setCurrentPage(draw_pages.getByIndex(duplicated_slide_index)) + moves_needed = slide_count - duplicated_slide_index - 1 + for _ in range(moves_needed): + dispatcher.executeDispatch(frame, ".uno:MovePageDown", "", 0, ()) + ret = f"Slide {{slide_index}} duplicated successfully and moved to the end" + return True + except Exception as e: + ret = f"Error: {{str(e)}}" + return False +libreoffice_impress_duplicate_slide(slide_index={slide_index}) +print(ret) +""" + + @agent_action + def libreoffice_impress_duplicate_slide(self, slide_index): + + return self.LIBREOFFICE_IMPRESS_DUPLICATE_SLIDE_CMD.format(slide_index=repr(slide_index)) + + LIBREOFFICE_IMPRESS_SET_SLIDE_FONT_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_set_slide_font(slide_index, font_name): + global ret + try: + zero_based_index = slide_index - 1 + slides = doc.getDrawPages() + if zero_based_index < 0 or zero_based_index >= slides.getCount(): + ret = f"Error: Slide index {{slide_index}} is out of range. Valid range is 1 to {{slides.getCount()}}." + return False + slide = slides.getByIndex(zero_based_index) + for i in range(slide.getCount()): + shape = slide.getByIndex(i) + if hasattr(shape, "getText"): + text = shape.getText() + if text: + cursor = text.createTextCursor() + cursor.gotoStart(False) + cursor.gotoEnd(True) + cursor.setPropertyValue("CharFontName", font_name) + ret = f"Successfully set font to '{{font_name}}' for all text elements in slide {{slide_index}}." + return True + except Exception as e: + ret = f"Error setting font: {{str(e)}}" + return False +libreoffice_impress_set_slide_font(slide_index={slide_index}, font_name={font_name}) +print(ret) +""" + + @agent_action + def libreoffice_impress_set_slide_font(self, slide_index, font_name): + + return self.LIBREOFFICE_IMPRESS_SET_SLIDE_FONT_CMD.format(slide_index=repr(slide_index), + font_name=repr(font_name)) + + LIBREOFFICE_IMPRESS_WRITE_TEXT_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_write_text(content, page_index, box_index, bold=False, italic=False, size=None, append=False): + global ret + try: + zero_based_page_index = page_index - 1 + pages = doc.getDrawPages() + if zero_based_page_index < 0 or zero_based_page_index >= pages.getCount(): + ret = f"Error: Page index {{page_index}} is out of range" + return False + page = pages.getByIndex(zero_based_page_index) + if box_index < 0 or box_index >= page.getCount(): + ret = f"Error: Box index {{box_index}} is out of range" + return False + shape = page.getByIndex(box_index) + if not hasattr(shape, "String"): + ret = f"Error: The shape at index {{box_index}} cannot contain text" + return False + if append: + shape.String = shape.String + content + else: + shape.String = content + if hasattr(shape, "getCharacterProperties"): + char_props = shape.getCharacterProperties() + if bold: + char_props.CharWeight = BOLD + else: + char_props.CharWeight = NORMAL + if italic: + char_props.CharPosture = ITALIC + else: + char_props.CharPosture = NONE + if size is not None: + char_props.CharHeight = size + + ret = f"Text successfully written to page {{page_index}}, box {{box_index}}" + return True + except Exception as e: + ret = f"Error: {{str(e)}}" + return False +libreoffice_impress_write_text(content={content}, page_index={page_index}, box_index={box_index}, bold={bold}, italic={italic}, size={size}, append={append}) +print(ret) +""" + + @agent_action + def libreoffice_impress_write_text(self, content, page_index, box_index, bold, italic, size, append): + + return self.LIBREOFFICE_IMPRESS_WRITE_TEXT_CMD.format(content=repr(content), page_index=repr(page_index), + box_index=repr(box_index), bold=repr(bold), + italic=repr(italic), size=repr(size), append=repr(append)) + + LIBREOFFICE_IMPRESS_SET_STYLE_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_set_style(slide_index, box_index, bold=None, italic=None, underline=None): + global ret + try: + pages = doc.getDrawPages() + if slide_index < 1 or slide_index > pages.getCount(): + ret = f"Error: Invalid slide index {{slide_index}}. Valid range is 1 to {{pages.getCount()}}" + return False + page = pages.getByIndex(slide_index - 1) + if box_index < 0 or box_index >= page.getCount(): + ret = f"Error: Invalid box index {{box_index}}. Valid range is 0 to {{page.getCount() - 1}}" + return False + shape = page.getByIndex(box_index) + if not hasattr(shape, "getText"): + ret = "Error: The specified shape does not contain text" + return False + text = shape.getText() + cursor = text.createTextCursor() + cursor.gotoStart(False) + cursor.gotoEnd(True) + if bold is not None: + cursor.setPropertyValue("CharWeight", BOLD if bold else NORMAL) + if italic is not None: + cursor.setPropertyValue("CharPosture", ITALIC if italic else NONE) + if underline is not None: + cursor.setPropertyValue("CharUnderline", 1 if underline else 0) + ret = "Style applied successfully" + return True + except Exception as e: + ret = f"Error: {{str(e)}}" + return False +libreoffice_impress_set_style(slide_index={slide_index}, box_index={box_index}, bold={bold}, italic={italic}, underline={underline}) +print(ret) +""" + + @agent_action + def libreoffice_impress_set_style(self, slide_index, box_index, bold, italic, underline): + + return self.LIBREOFFICE_IMPRESS_SET_STYLE_CMD.format(slide_index=repr(slide_index), box_index=repr(box_index), + bold=repr(bold), italic=repr(italic), + underline=repr(underline)) + + LIBREOFFICE_IMPRESS_CONFIGURE_AUTO_SAVE_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_configure_auto_save(enabled, interval_minutes): + global ret + try: + if interval_minutes < 1: + interval_minutes = 1 + config_provider = ctx.ServiceManager.createInstanceWithContext( + "com.sun.star.configuration.ConfigurationProvider", ctx + ) + prop = PropertyValue() + prop.Name = "nodepath" + prop.Value = "/org.openoffice.Office.Common/Save/Document" + config_access = config_provider.createInstanceWithArguments( + "com.sun.star.configuration.ConfigurationUpdateAccess", (prop,) + ) + config_access.setPropertyValue("AutoSave", enabled) + config_access.setPropertyValue("AutoSaveTimeIntervall", interval_minutes) + config_access.commitChanges() + ret = f"Auto-save {{'enabled' if enabled else 'disabled'}} with interval of {{interval_minutes}} minutes" + return True + except Exception as e: + ret = f"Error configuring auto-save: {{str(e)}}" + return False +libreoffice_impress_configure_auto_save(enabled={enabled}, interval_minutes={interval_minutes}) +print(ret) +""" + + @agent_action + def libreoffice_impress_configure_auto_save(self, enabled, interval_minutes): + + return self.LIBREOFFICE_IMPRESS_CONFIGURE_AUTO_SAVE_CMD.format(enabled=repr(enabled), + interval_minutes=repr(interval_minutes)) + + LIBREOFFICE_IMPRESS_SET_BACKGROUND_COLOR_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_set_background_color(slide_index, box_index, color): + global ret + try: + zero_based_slide_index = slide_index - 1 + slides = doc.getDrawPages() + if zero_based_slide_index < 0 or zero_based_slide_index >= slides.getCount(): + ret = f"Error: Slide index {{slide_index}} is out of range" + return False + slide = slides.getByIndex(zero_based_slide_index) + if box_index < 0 or box_index >= slide.getCount(): + ret = f"Error: Box index {{box_index}} is out of range" + return False + shape = slide.getByIndex(box_index) + color_int = 0 + color_map = {{ + "red": 16711680, + "green": 65280, + "blue": 255, + "yellow": 16776960, + "black": 0, + "white": 16777215, + "purple": 8388736, + "orange": 16753920, + "pink": 16761035, + "gray": 8421504, + "brown": 10824234, + "cyan": 65535, + "magenta": 16711935, + }} + if color.lower() in color_map: + color_int = color_map[color.lower()] + elif color.startswith("#") and len(color) == 7: + color_int = int(color[1:], 16) + else: + ret = f"Error: Invalid color format: {{color}}" + return False + shape.FillStyle = uno.Enum("com.sun.star.drawing.FillStyle", "SOLID") + shape.FillColor = color_int + ret = f"Background color of textbox {{box_index}} on slide {{slide_index}} set to {{color}}" + return True + except Exception as e: + ret = f"Error: {{str(e)}}" + return False +libreoffice_impress_set_background_color(slide_index={slide_index}, box_index={box_index}, color={color}) +print(ret) +""" + + @agent_action + def libreoffice_impress_set_background_color(self, slide_index, box_index, color): + + return self.LIBREOFFICE_IMPRESS_SET_BACKGROUND_COLOR_CMD.format(slide_index=repr(slide_index), + box_index=repr(box_index), color=repr(color)) + + LIBREOFFICE_IMPRESS_SET_TEXT_COLOR_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_set_text_color(slide_index, box_index, color): + global ret + try: + zero_based_slide_index = slide_index - 1 + slides = doc.getDrawPages() + if zero_based_slide_index < 0 or zero_based_slide_index >= slides.getCount(): + ret = f"Error: Slide index {{slide_index}} is out of range" + return False + slide = slides.getByIndex(zero_based_slide_index) + if box_index < 0 or box_index >= slide.getCount(): + ret = f"Error: Box index {{box_index}} is out of range" + return False + shape = slide.getByIndex(box_index) + if not hasattr(shape, "getText"): + ret = f"Error: Shape at index {{box_index}} does not contain text" + return False + color_int = 0 + if color.startswith("#"): + color_int = int(color[1:], 16) + else: + color_map = {{ + "red": 16711680, + "green": 43315, + "blue": 255, + "black": 0, + "white": 16777215, + "yellow": 16776960, + "cyan": 65535, + "magenta": 16711935, + "gray": 8421504, + }} + if color.lower() in color_map: + color_int = color_map[color.lower()] + else: + ret = f"Error: Unsupported color '{{color}}'" + return False + text = shape.getText() + cursor = text.createTextCursor() + cursor.gotoStart(False) + cursor.gotoEnd(True) + cursor.setPropertyValue("CharColor", color_int) + ret = f"Successfully set text color to {{color}} for textbox {{box_index}} on slide {{slide_index}}" + return True + except Exception as e: + ret = f"Error: {{str(e)}}" + return False +libreoffice_impress_set_text_color(slide_index={slide_index}, box_index={box_index}, color={color}) +print(ret) +""" + + @agent_action + def libreoffice_impress_set_text_color(self, slide_index, box_index, color): + + return self.LIBREOFFICE_IMPRESS_SET_TEXT_COLOR_CMD.format(slide_index=repr(slide_index), + box_index=repr(box_index), color=repr(color)) + + LIBREOFFICE_IMPRESS_DELETE_CONTENT_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_delete_content(slide_index, box_index): + global ret + try: + pages = doc.getDrawPages() + zero_based_slide_index = slide_index - 1 + if zero_based_slide_index < 0 or zero_based_slide_index >= pages.getCount(): + ret = f"Error: Invalid slide index {{slide_index}}. Valid range is 1 to {{pages.getCount()}}" + return False + slide = pages.getByIndex(zero_based_slide_index) + if box_index < 0 or box_index >= slide.getCount(): + ret = f"Error: Invalid box index {{box_index}}. Valid range is 0 to {{slide.getCount() - 1}}" + return False + shape = slide.getByIndex(box_index) + slide.remove(shape) + ret = f"Successfully deleted textbox {{box_index}} from slide {{slide_index}}" + return True + except Exception as e: + ret = f"Error: {{str(e)}}" + return False +libreoffice_impress_delete_content(slide_index={slide_index}, box_index={box_index}) +print(ret) +""" + + @agent_action + def libreoffice_impress_delete_content(self, slide_index, box_index): + + return self.LIBREOFFICE_IMPRESS_DELETE_CONTENT_CMD.format(slide_index=repr(slide_index), + box_index=repr(box_index)) + + LIBREOFFICE_IMPRESS_SET_SLIDE_ORIENTATION_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_set_slide_orientation(orientation): + global ret + try: + draw_pages = doc.getDrawPages() + first_page = draw_pages.getByIndex(0) + current_width = first_page.Width + current_height = first_page.Height + if orientation == "portrait" and current_width > current_height: + new_width, new_height = current_height, current_width + elif orientation == "landscape" and current_width < current_height: + new_width, new_height = current_height, current_width + else: + ret = f"Slides are already in {{orientation}} orientation" + return True + for i in range(draw_pages.getCount()): + page = draw_pages.getByIndex(i) + page.Width = new_width + page.Height = new_height + ret = f"Changed slide orientation to {{orientation}}" + return True + except Exception as e: + ret = f"Error changing slide orientation: {{str(e)}}" + return False +libreoffice_impress_set_slide_orientation(orientation={orientation}) +print(ret) +""" + + @agent_action + def libreoffice_impress_set_slide_orientation(self, orientation): + + return self.LIBREOFFICE_IMPRESS_SET_SLIDE_ORIENTATION_CMD.format(orientation=repr(orientation)) + + LIBREOFFICE_IMPRESS_POSITION_BOX_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_position_box(slide_index, box_index, position): + global ret + try: + pages = doc.getDrawPages() + if slide_index < 1 or slide_index > pages.getCount(): + ret = f"Error: Invalid slide index {{slide_index}}" + return False + page = pages.getByIndex(slide_index - 1) + if box_index < 0 or box_index >= page.getCount(): + ret = f"Error: Invalid box index {{box_index}}" + return False + shape = page.getByIndex(box_index) + controller = doc.getCurrentController() + slide_width = 28000 + slide_height = 21000 + shape_width = shape.Size.Width + shape_height = shape.Size.Height + margin = 500 + if position == "left": + new_x = margin + new_y = (slide_height - shape_height) / 2 + elif position == "right": + new_x = slide_width - shape_width - margin + new_y = (slide_height - shape_height) / 2 + elif position == "center": + new_x = (slide_width - shape_width) / 2 + new_y = (slide_height - shape_height) / 2 + elif position == "top": + new_x = (slide_width - shape_width) / 2 + new_y = margin + elif position == "bottom": + new_x = (slide_width - shape_width) / 2 + new_y = slide_height - shape_height - margin + elif position == "top-left": + new_x = margin + new_y = margin + elif position == "top-right": + new_x = slide_width - shape_width - margin + new_y = margin + elif position == "bottom-left": + new_x = margin + new_y = slide_height - shape_height - margin + elif position == "bottom-right": + new_x = slide_width - shape_width - margin + new_y = slide_height - shape_height - margin + else: + ret = f"Error: Invalid position '{{position}}'" + return False + try: + shape.Position.X = int(new_x) + shape.Position.Y = int(new_y) + except: + try: + shape.setPropertyValue("PositionX", int(new_x)) + shape.setPropertyValue("PositionY", int(new_y)) + except: + point = uno.createUnoStruct("com.sun.star.awt.Point", int(new_x), int(new_y)) + shape.setPosition(point) + ret = f"Box positioned at {{position}} (X: {{new_x}}, Y: {{new_y}})" + return True + except Exception as e: + ret = f"Error positioning box: {{str(e)}}" + return False +libreoffice_impress_position_box(slide_index={slide_index}, box_index={box_index}, position={position}) +print(ret) +""" + + @agent_action + def libreoffice_impress_position_box(self, slide_index, box_index, position): + + return self.LIBREOFFICE_IMPRESS_POSITION_BOX_CMD.format(slide_index=repr(slide_index), + box_index=repr(box_index), position=repr(position)) + + LIBREOFFICE_IMPRESS_INSERT_FILE_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_insert_file(file_path, slide_index=None, position=None, size=None, autoplay=False): + global ret + try: + expanded_file_path = os.path.expanduser(file_path) + if not os.path.exists(expanded_file_path): + ret = f"Error: File not found: {{expanded_file_path}}" + return False + file_url = uno.systemPathToFileUrl(os.path.abspath(expanded_file_path)) + pages = doc.getDrawPages() + if slide_index is not None: + zero_based_index = slide_index - 1 + if zero_based_index < 0 or zero_based_index >= pages.getCount(): + ret = f"Error: Invalid slide index: {{slide_index}}" + return False + slide = pages.getByIndex(zero_based_index) + else: + controller = doc.getCurrentController() + slide = controller.getCurrentPage() + slide_width = 21000 + slide_height = 12750 + if position is None: + position = {{"x": 10, "y": 10}} + if size is None: + size = {{"width": 80, "height": 60}} + x = int(position["x"] * slide_width / 100) + y = int(position["y"] * slide_height / 100) + width = int(size["width"] * slide_width / 100) + height = int(size["height"] * slide_height / 100) + media_shape = doc.createInstance("com.sun.star.presentation.MediaShape") + slide.add(media_shape) + media_shape.setPosition(uno.createUnoStruct("com.sun.star.awt.Point", x, y)) + media_shape.setSize(uno.createUnoStruct("com.sun.star.awt.Size", width, height)) + media_shape.setPropertyValue("MediaURL", file_url) + if autoplay: + try: + media_shape.setPropertyValue("MediaIsAutoPlay", True) + except: + pass + ret = f"Video inserted successfully from {{expanded_file_path}}" + return True + except Exception as e: + ret = f"Error inserting video: {{str(e)}}" + return False +libreoffice_impress_insert_file(file_path={file_path}, slide_index={slide_index}, position={position}, size={size}, autoplay={autoplay}) +print(ret) +""" + + @agent_action + def libreoffice_impress_insert_file(self, file_path, slide_index, position, size, autoplay): + + return self.LIBREOFFICE_IMPRESS_INSERT_FILE_CMD.format(file_path=repr(file_path), slide_index=repr(slide_index), + position=repr(position), size=repr(size), + autoplay=repr(autoplay)) + + LIBREOFFICE_IMPRESS_SET_SLIDE_BACKGROUND_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_set_slide_background(slide_index=None, color=None, image_path=None): + global ret + try: + if not color and not image_path: + ret = "Error: Either color or image_path must be provided" + return False + pages = doc.getDrawPages() + page_count = pages.getCount() + rgb_color = None + if color: + if color.startswith("#"): + color = color.lstrip("#") + rgb_color = int(color, 16) + else: + color_map = {{ + "red": 16711680, + "green": 43315, + "blue": 255, + "black": 0, + "white": 16777215, + "yellow": 16776960, + "cyan": 65535, + "magenta": 16711935, + "gray": 8421504, + }} + rgb_color = color_map.get(color.lower(), 0) + if slide_index is not None: + slide_index = slide_index - 1 + if slide_index < 0 or slide_index >= page_count: + ret = f"Error: Slide index {{slide_index + 1}} is out of range (1-{{page_count}})" + return False + slides_to_modify = [pages.getByIndex(slide_index)] + else: + slides_to_modify = [pages.getByIndex(i) for i in range(page_count)] + for slide in slides_to_modify: + fill_props = ctx.ServiceManager.createInstanceWithContext( + "com.sun.star.drawing.FillProperties", ctx + ) + if image_path and os.path.exists(image_path): + abs_path = os.path.abspath(image_path) + file_url = uno.systemPathToFileUrl(abs_path) + fill_props.FillStyle = uno.Enum("com.sun.star.drawing.FillStyle", "BITMAP") + fill_props.FillBitmapURL = file_url + fill_props.FillBitmapMode = uno.Enum("com.sun.star.drawing.BitmapMode", "STRETCH") + elif rgb_color is not None: + fill_props.FillStyle = uno.Enum("com.sun.star.drawing.FillStyle", "SOLID") + fill_props.FillColor = rgb_color + slide.setPropertyValue("Background", fill_props) + ret = "Background set successfully" + return True + except Exception as e: + ret = f"Error setting background: {{str(e)}}" + return False +libreoffice_impress_set_slide_background(slide_index={slide_index}, color={color}, image_path={image_path}) +print(ret) +""" + + @agent_action + def libreoffice_impress_set_slide_background(self, slide_index, color, image_path): + + return self.LIBREOFFICE_IMPRESS_SET_SLIDE_BACKGROUND_CMD.format(slide_index=repr(slide_index), + color=repr(color), image_path=repr(image_path)) + + LIBREOFFICE_IMPRESS_SAVE_AS_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_save_as(file_path, overwrite=False): + global ret + try: + if os.path.exists(file_path) and not overwrite: + ret = f"File already exists and overwrite is set to False: {{file_path}}" + return False + abs_path = os.path.abspath(file_path) + if os.name == "nt": + url = "file:///" + abs_path.replace("\\", "/") + else: + url = "file://" + abs_path + properties = [] + overwrite_prop = PropertyValue() + overwrite_prop.Name = "Overwrite" + overwrite_prop.Value = overwrite + properties.append(overwrite_prop) + extension = os.path.splitext(file_path)[1].lower() + if extension == ".odp": + filter_name = "impress8" + elif extension == ".ppt": + filter_name = "MS PowerPoint 97" + elif extension == ".pptx": + filter_name = "Impress MS PowerPoint 2007 XML" + elif extension == ".pdf": + filter_name = "impress_pdf_Export" + else: + filter_name = "impress8" + filter_prop = PropertyValue() + filter_prop.Name = "FilterName" + filter_prop.Value = filter_name + properties.append(filter_prop) + doc.storeAsURL(url, tuple(properties)) + ret = f"Document saved successfully to {{file_path}}" + return True + except Exception as e: + ret = f"Error saving document: {{str(e)}}" + return False +libreoffice_impress_save_as(file_path={file_path}, overwrite={overwrite}) +print(ret) +""" + + @agent_action + def libreoffice_impress_save_as(self, file_path, overwrite): + + return self.LIBREOFFICE_IMPRESS_SAVE_AS_CMD.format(file_path=repr(file_path), overwrite=repr(overwrite)) + + LIBREOFFICE_IMPRESS_INSERT_IMAGE_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_insert_image(slide_index, image_path, width=None, height=None, position=None): + global ret + try: + if not os.path.exists(image_path): + ret = f"Error: Image file not found at {{image_path}}" + return False + zero_based_index = slide_index - 1 + slides = doc.getDrawPages() + if zero_based_index < 0 or zero_based_index >= slides.getCount(): + ret = f"Error: Slide index {{slide_index}} is out of range. Valid range is 1 to {{slides.getCount()}}" + return False + slide = slides.getByIndex(zero_based_index) + bitmap = doc.createInstance("com.sun.star.drawing.BitmapTable") + image_url = uno.systemPathToFileUrl(os.path.abspath(image_path)) + shape = doc.createInstance("com.sun.star.drawing.GraphicObjectShape") + shape.setPropertyValue("GraphicURL", image_url) + slide.add(shape) + x_pos = 0 + y_pos = 0 + slide_width = slide.Width + slide_height = slide.Height + if position: + if "x" in position: + x_pos = int(position["x"] / 100 * slide_width) + if "y" in position: + y_pos = int(position["y"] / 100 * slide_height) + current_width = shape.Size.Width + current_height = shape.Size.Height + new_width = int(width * 1000) if width is not None else current_width + new_height = int(height * 1000) if height is not None else current_height + size = uno.createUnoStruct("com.sun.star.awt.Size") + size.Width = new_width + size.Height = new_height + point = uno.createUnoStruct("com.sun.star.awt.Point") + point.X = x_pos + point.Y = y_pos + shape.Size = size + shape.Position = point + ret = f"Image inserted successfully on slide {{slide_index}}" + return True + except Exception as e: + ret = f"Error inserting image: {{str(e)}}" + return False +libreoffice_impress_insert_image(slide_index={slide_index}, image_path={image_path}, width={width}, height={height}, position={position}) +print(ret) +""" + + @agent_action + def libreoffice_impress_insert_image(self, slide_index, image_path, width, height, position): + + return self.LIBREOFFICE_IMPRESS_INSERT_IMAGE_CMD.format(slide_index=repr(slide_index), + image_path=repr(image_path), width=repr(width), + height=repr(height), position=repr(position)) + + LIBREOFFICE_IMPRESS_CONFIGURE_DISPLAY_SETTINGS_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_configure_display_settings(use_presenter_view=None, primary_monitor_only=None, monitor_for_presentation=None + ): + global ret + try: + controller = doc.getCurrentController() + if not hasattr(controller, "getPropertyValue"): + ret = "Error: Not an Impress presentation or controller not available" + return False + if use_presenter_view is not None: + try: + controller.setPropertyValue("IsPresentationViewEnabled", use_presenter_view) + except Exception as e: + ret = f"Warning: Could not set presenter view: {{str(e)}}" + if primary_monitor_only is not None: + try: + controller.setPropertyValue("UsePrimaryMonitorOnly", primary_monitor_only) + except Exception as e: + ret = f"Warning: Could not set primary monitor usage: {{str(e)}}" + if monitor_for_presentation is not None: + try: + controller.setPropertyValue("MonitorForPresentation", monitor_for_presentation - 1) + except Exception as e: + ret = f"Warning: Could not set presentation monitor: {{str(e)}}" + ret = "Display settings configured successfully" + return True + except Exception as e: + ret = f"Error configuring display settings: {{str(e)}}" + return False +libreoffice_impress_configure_display_settings(use_presenter_view={use_presenter_view}, primary_monitor_only={primary_monitor_only}, monitor_for_presentation={monitor_for_presentation}) +print(ret) +""" + + @agent_action + def libreoffice_impress_configure_display_settings(self, use_presenter_view, primary_monitor_only, + monitor_for_presentation): + + return self.LIBREOFFICE_IMPRESS_CONFIGURE_DISPLAY_SETTINGS_CMD.format( + use_presenter_view=repr(use_presenter_view), primary_monitor_only=repr(primary_monitor_only), + monitor_for_presentation=repr(monitor_for_presentation)) + + LIBREOFFICE_IMPRESS_SET_TEXT_STRIKETHROUGH_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_set_text_strikethrough(slide_index, box_index, line_numbers, apply): + global ret + try: + slides = doc.getDrawPages() + slide = slides.getByIndex(slide_index - 1) + shape = slide.getByIndex(box_index) + if not hasattr(shape, "getText"): + ret = f"Error: Shape at index {{box_index}} does not contain text" + return False + text = shape.getText() + cursor = text.createTextCursor() + text_content = text.getString() + lines = text_content.split("\n") + for line_number in line_numbers: + if 1 <= line_number <= len(lines): + start_pos = 0 + for i in range(line_number - 1): + start_pos += len(lines[i]) + 1 + end_pos = start_pos + len(lines[line_number - 1]) + cursor.gotoStart(False) + cursor.goRight(start_pos, False) + cursor.goRight(len(lines[line_number - 1]), True) + cursor.CharStrikeout = apply + ret = f"Strike-through {{'applied' if apply else 'removed'}} successfully" + return True + except Exception as e: + ret = f"Error: {{str(e)}}" + return False +libreoffice_impress_set_text_strikethrough(slide_index={slide_index}, box_index={box_index}, line_numbers={line_numbers}, apply={apply}) +print(ret) +""" + + @agent_action + def libreoffice_impress_set_text_strikethrough(self, slide_index, box_index, line_numbers, apply): + + return self.LIBREOFFICE_IMPRESS_SET_TEXT_STRIKETHROUGH_CMD.format(slide_index=repr(slide_index), + box_index=repr(box_index), + line_numbers=repr(line_numbers), + apply=repr(apply)) + + LIBREOFFICE_IMPRESS_SET_TEXTBOX_ALIGNMENT_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_set_textbox_alignment(slide_index, box_index, alignment): + global ret + try: + zero_based_slide_index = slide_index - 1 + slides = doc.getDrawPages() + if zero_based_slide_index < 0 or zero_based_slide_index >= slides.getCount(): + ret = f"Error: Slide index {{slide_index}} out of range" + return False + slide = slides.getByIndex(zero_based_slide_index) + if box_index < 0 or box_index >= slide.getCount(): + ret = f"Error: Box index {{box_index}} out of range" + return False + shape = slide.getByIndex(box_index) + if not hasattr(shape, "getText"): + ret = "Error: Selected shape does not support text" + return False + if alignment == "left": + shape.TextHorizontalAdjust = LEFT + elif alignment == "center": + shape.TextHorizontalAdjust = CENTER + elif alignment == "right": + shape.TextHorizontalAdjust = RIGHT + elif alignment == "justify": + text = shape.getText() + cursor = text.createTextCursor() + cursor.gotoStart(False) + cursor.gotoEnd(True) + cursor.ParaAdjust = 3 + else: + ret = f"Error: Invalid alignment value: {{alignment}}" + return False + ret = f"Successfully set text alignment to {{alignment}} for textbox {{box_index}} on slide {{slide_index}}" + return True + except Exception as e: + ret = f"Error: {{str(e)}}" + return False +libreoffice_impress_set_textbox_alignment(slide_index={slide_index}, box_index={box_index}, alignment={alignment}) +print(ret) +""" + + @agent_action + def libreoffice_impress_set_textbox_alignment(self, slide_index, box_index, alignment): + + return self.LIBREOFFICE_IMPRESS_SET_TEXTBOX_ALIGNMENT_CMD.format(slide_index=repr(slide_index), + box_index=repr(box_index), + alignment=repr(alignment)) + + LIBREOFFICE_IMPRESS_SET_SLIDE_NUMBER_COLOR_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_set_slide_number_color(color): + global ret + try: + color_map = {{ + "black": 0, + "white": 16777215, + "red": 16711680, + "green": 65280, + "blue": 255, + "yellow": 16776960, + "cyan": 65535, + "magenta": 16711935, + "gray": 8421504, + "orange": 16753920, + "purple": 8388736, + }} + if color.lower() in color_map: + rgb_color = color_map[color.lower()] + else: + if color.startswith("#"): + color = color[1:] + try: + if len(color) == 6: + rgb_color = int(color, 16) + else: + rgb_color = 0 + except ValueError: + rgb_color = 0 + found = False + master_pages = doc.getMasterPages() + for i in range(master_pages.getCount()): + master_page = master_pages.getByIndex(i) + for j in range(master_page.getCount()): + shape = master_page.getByIndex(j) + if hasattr(shape, "getText") and shape.getText() is not None: + text = shape.getText() + try: + enum = text.createEnumeration() + while enum.hasMoreElements(): + para = enum.nextElement() + if hasattr(para, "createEnumeration"): + para_enum = para.createEnumeration() + while para_enum.hasMoreElements(): + portion = para_enum.nextElement() + if ( + hasattr(portion, "TextPortionType") + and portion.TextPortionType == "TextField" + ): + if hasattr(portion, "TextField") and portion.TextField is not None: + field = portion.TextField + if hasattr(field, "supportsService") and ( + field.supportsService( + "com.sun.star.presentation.TextField.PageNumber" + ) + or field.supportsService("com.sun.star.text.TextField.PageNumber") + ): + portion.CharColor = rgb_color + found = True + except Exception as e: + continue + draw_pages = doc.getDrawPages() + for i in range(draw_pages.getCount()): + page = draw_pages.getByIndex(i) + for j in range(page.getCount()): + shape = page.getByIndex(j) + if hasattr(shape, "getText") and shape.getText() is not None: + text = shape.getText() + try: + enum = text.createEnumeration() + while enum.hasMoreElements(): + para = enum.nextElement() + if hasattr(para, "createEnumeration"): + para_enum = para.createEnumeration() + while para_enum.hasMoreElements(): + portion = para_enum.nextElement() + if ( + hasattr(portion, "TextPortionType") + and portion.TextPortionType == "TextField" + ): + if hasattr(portion, "TextField") and portion.TextField is not None: + field = portion.TextField + if hasattr(field, "supportsService") and ( + field.supportsService( + "com.sun.star.presentation.TextField.PageNumber" + ) + or field.supportsService("com.sun.star.text.TextField.PageNumber") + ): + portion.CharColor = rgb_color + found = True + except Exception as e: + continue + for i in range(draw_pages.getCount()): + page = draw_pages.getByIndex(i) + for j in range(page.getCount()): + shape = page.getByIndex(j) + if hasattr(shape, "getText") and shape.getText() is not None: + text = shape.getText() + text_string = text.getString() + if text_string.isdigit() and len(text_string) <= 3: + try: + cursor = text.createTextCursor() + cursor.gotoStart(False) + cursor.gotoEnd(True) + cursor.CharColor = rgb_color + found = True + except Exception as e: + continue + if found: + ret = f"Slide number color set to {{color}}" + return True + else: + ret = "Could not find slide numbers to change color" + return False + except Exception as e: + ret = f"Error setting slide number color: {{str(e)}}" + return False +libreoffice_impress_set_slide_number_color(color={color}) +print(ret) +""" + + @agent_action + def libreoffice_impress_set_slide_number_color(self, color): + + return self.LIBREOFFICE_IMPRESS_SET_SLIDE_NUMBER_COLOR_CMD.format(color=repr(color)) + + LIBREOFFICE_IMPRESS_EXPORT_TO_IMAGE_CMD = """import json +import os +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_impress_export_to_image(file_path, format, slide_index=None): + global ret + try: + format = format.lower() + valid_formats = ["png", "jpeg", "jpg", "gif", "bmp", "tiff"] + if format not in valid_formats: + ret = f"Error: Invalid format '{{format}}'. Valid formats are: {{', '.join(valid_formats)}}" + return False + if format == "jpg": + format = "jpeg" + pages = doc.getDrawPages() + page_count = pages.getCount() + if slide_index is not None: + slide_index = slide_index - 1 + if slide_index < 0 or slide_index >= page_count: + ret = f"Error: Invalid slide index {{slide_index + 1}}. Valid range is 1 to {{page_count}}" + return False + controller = doc.getCurrentController() + filter_name = f"draw_{{format}}_Export" + filter_data = PropertyValue(Name="FilterData", Value=()) + if slide_index is not None: + controller.setCurrentPage(pages.getByIndex(slide_index)) + props = PropertyValue(Name="FilterName", Value=filter_name), filter_data + doc.storeToURL(uno.systemPathToFileUrl(file_path), props) + ret = f"Successfully exported slide {{slide_index + 1}} to {{file_path}}" + return True + else: + base_name, ext = os.path.splitext(file_path) + for i in range(page_count): + controller.setCurrentPage(pages.getByIndex(i)) + if page_count == 1: + current_file = f"{{base_name}}.{{format}}" + else: + current_file = f"{{base_name}}_{{i + 1}}.{{format}}" + props = PropertyValue(Name="FilterName", Value=filter_name), filter_data + doc.storeToURL(uno.systemPathToFileUrl(current_file), props) + + if page_count == 1: + ret = f"Successfully exported {{page_count}} slides to {{base_name}}.{{format}}" + else: + ret = f"Successfully exported {{page_count}} slides to {{base_name}}_[1-{{page_count}}].{{format}}" + return True + except Exception as e: + ret = f"Error exporting to image: {{str(e)}}" + return False +libreoffice_impress_export_to_image(file_path={file_path}, format={format}, slide_index={slide_index}) +print(ret) +""" + + @agent_action + def libreoffice_impress_export_to_image(self, file_path, format, slide_index): + + return self.LIBREOFFICE_IMPRESS_EXPORT_TO_IMAGE_CMD.format(file_path=repr(file_path), format=repr(format), + slide_index=repr(slide_index)) + + LIBREOFFICE_WRITER_SAVE_CMD = """import os +import re +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE, OBLIQUE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.style.ParagraphAdjust import CENTER, LEFT, RIGHT +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +text = doc.Text +cursor = text.createTextCursor() +ret = "" + +def libreoffice_writer_save(): + global ret + try: + if doc.hasLocation(): + doc.store() + else: + raise Exception("文档没有保存位置,请使用另存为功能") + return True + except Exception as e: + return False +libreoffice_writer_save() +print(ret) +""" + + @agent_action + def libreoffice_writer_save(self): + + return self.LIBREOFFICE_WRITER_SAVE_CMD.format() + + LIBREOFFICE_WRITER_WRITE_TEXT_CMD = """import os +import re +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE, OBLIQUE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.style.ParagraphAdjust import CENTER, LEFT, RIGHT +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +global_text = doc.Text +cursor = global_text.createTextCursor() +ret = "" + +def libreoffice_writer_write_text(text, bold=False, italic=False, size=None): + global ret + cursor.CharWeight = 150 if bold else 100 + cursor.CharPosture = ITALIC if italic else NONE + if size: + cursor.CharHeight = size + global_text.insertString(cursor, text, False) + ret = "Success" +libreoffice_writer_write_text(text={text}, bold={bold}, italic={italic}, size={size}) +print(ret) +""" + + @agent_action + def libreoffice_writer_write_text(self, text, bold, italic, size): + + return self.LIBREOFFICE_WRITER_WRITE_TEXT_CMD.format(text=repr(text), bold=repr(bold), italic=repr(italic), + size=repr(size)) + + LIBREOFFICE_WRITER_SET_COLOR_CMD = """import os +import re +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE, OBLIQUE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.style.ParagraphAdjust import CENTER, LEFT, RIGHT +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +text = doc.Text +cursor = text.createTextCursor() +ret = "" + +def libreoffice_writer_set_color(pattern, color, paragraph_indices=None): + global ret + try: + enum = doc.Text.createEnumeration() + paragraphs = [] + while enum.hasMoreElements(): + paragraphs.append(enum.nextElement()) + if not paragraph_indices: + paragraphs_to_process = range(len(paragraphs)) + else: + paragraphs_to_process = paragraph_indices + regex = re.compile(pattern) + for idx in paragraphs_to_process: + if idx < 0 or idx >= len(paragraphs): + continue + paragraph = paragraphs[idx] + if not paragraph.supportsService("com.sun.star.text.Paragraph"): + continue + para_text = paragraph.getString() + matches = regex.finditer(para_text) + for match in matches: + para_cursor = text.createTextCursorByRange(paragraph.getStart()) + para_cursor.goRight(match.start(), False) + para_cursor.goRight(match.end() - match.start(), True) + para_cursor.CharColor = color + ret = "Success" + return True + except Exception as e: + ret = f"Error: {{str(e)}}" + return False +libreoffice_writer_set_color(pattern={pattern}, color={color}, paragraph_indices={paragraph_indices}) +print(ret) +""" + + @agent_action + def libreoffice_writer_set_color(self, pattern, color, paragraph_indices): + + return self.LIBREOFFICE_WRITER_SET_COLOR_CMD.format(pattern=repr(pattern), color=repr(color), + paragraph_indices=repr(paragraph_indices)) + + LIBREOFFICE_WRITER_FIND_AND_REPLACE_CMD = """import os +import re +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE, OBLIQUE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.style.ParagraphAdjust import CENTER, LEFT, RIGHT +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +text = doc.Text +cursor = text.createTextCursor() +ret = "" + +def libreoffice_writer_find_and_replace(pattern, replacement, paragraph_indices=None): + global ret + try: + enum = doc.Text.createEnumeration() + paragraphs = [] + while enum.hasMoreElements(): + paragraphs.append(enum.nextElement()) + total_replacements = 0 + if not paragraph_indices: + paragraphs_to_process = list(range(len(paragraphs))) + else: + paragraphs_to_process = [i for i in paragraph_indices if 0 <= i < len(paragraphs)] + regex = re.compile(pattern) + for idx in paragraphs_to_process: + if idx >= len(paragraphs): + continue + paragraph = paragraphs[idx] + if paragraph.supportsService("com.sun.star.text.Paragraph"): + text_content = paragraph.getString() + new_text, count = regex.subn(replacement, text_content) + if count > 0: + paragraph.setString(new_text) + total_replacements += count + ret = f"Successfully made {{total_replacements}} replacements" + return ret + except Exception as e: + ret = f"Error during find and replace: {{str(e)}}" + return ret +libreoffice_writer_find_and_replace(pattern={pattern}, replacement={replacement}, paragraph_indices={paragraph_indices}) +print(ret) +""" + + @agent_action + def libreoffice_writer_find_and_replace(self, pattern, replacement, paragraph_indices=None): + + return self.LIBREOFFICE_WRITER_FIND_AND_REPLACE_CMD.format(pattern=repr(pattern), replacement=repr(replacement), + paragraph_indices=repr(paragraph_indices)) + + LIBREOFFICE_WRITER_SET_FONT_CMD = """import os +import re +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE, OBLIQUE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.style.ParagraphAdjust import CENTER, LEFT, RIGHT +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +text = doc.Text +cursor = text.createTextCursor() +ret = "" + +def libreoffice_writer_set_font(font_name, paragraph_indices=None): + global ret + try: + text = doc.getText() + enum = text.createEnumeration() + paragraphs = [] + while enum.hasMoreElements(): + paragraphs.append(enum.nextElement()) + if not paragraph_indices: + paragraph_indices = range(len(paragraphs)) + for idx in paragraph_indices: + if 0 <= idx < len(paragraphs): + paragraph = paragraphs[idx] + cursor = text.createTextCursorByRange(paragraph) + cursor.CharFontName = font_name + ret = "Success" + return True + except Exception as e: + ret = f"Error: {{str(e)}}" + return False +libreoffice_writer_set_font(font_name={font_name}, paragraph_indices={paragraph_indices}) +print(ret) +""" + + # @agent_action + # def libreoffice_writer_set_font(self, font_name, paragraph_indices): + # + # return self.LIBREOFFICE_WRITER_SET_FONT_CMD.format(font_name=repr(font_name), + # paragraph_indices=repr(paragraph_indices)) + @agent_action + def libreoffice_writer_set_font(self, font_name, paragraph_indices=None): + + return self.LIBREOFFICE_WRITER_SET_FONT_CMD.format(font_name=repr(font_name), + paragraph_indices=repr(paragraph_indices)) + + LIBREOFFICE_WRITER_SET_LINE_SPACING_CMD = """import os +import re +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE, OBLIQUE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.style.ParagraphAdjust import CENTER, LEFT, RIGHT +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +text = doc.Text +cursor = text.createTextCursor() +ret = "" + +def libreoffice_writer_set_line_spacing(spacing_value, paragraph_indices=None): + global ret + try: + text = doc.getText() + paragraph_enum = text.createEnumeration() + line_spacing_value = int(spacing_value * 100) + current_index = 0 + + while paragraph_enum.hasMoreElements(): + paragraph = paragraph_enum.nextElement() + + if not paragraph_indices or current_index in paragraph_indices: + line_spacing = uno.createUnoStruct("com.sun.star.style.LineSpacing") + line_spacing.Mode = 0 + line_spacing.Height = line_spacing_value + paragraph.ParaLineSpacing = line_spacing + + if paragraph.String.strip(): + current_index += 1 + + ret = "Success" + return True + except Exception as e: + ret = f"Error: {{str(e)}}" + return False +libreoffice_writer_set_line_spacing(spacing_value={spacing_value}, paragraph_indices={paragraph_indices}) +print(ret) +""" + + @agent_action + def libreoffice_writer_set_line_spacing(self, spacing_value, paragraph_indices): + + return self.LIBREOFFICE_WRITER_SET_LINE_SPACING_CMD.format(spacing_value=repr(spacing_value), + paragraph_indices=repr(paragraph_indices)) + + LIBREOFFICE_WRITER_REMOVE_HIGHLIGHTING_CMD = """import os +import re +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE, OBLIQUE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.style.ParagraphAdjust import CENTER, LEFT, RIGHT +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +text = doc.Text +cursor = text.createTextCursor() +ret = "" + +def libreoffice_writer_remove_highlighting(paragraph_indices=None): + global ret + try: + text = doc.getText() + paragraphs = text.createEnumeration() + target_indices = set(paragraph_indices) if paragraph_indices else None + current_index = 0 + + while paragraphs.hasMoreElements(): + paragraph = paragraphs.nextElement() + if target_indices is None or current_index in target_indices: + if paragraph.supportsService("com.sun.star.text.Paragraph"): + para_cursor = text.createTextCursorByRange(paragraph) + # Remove all highlighting by setting back color to -1 + para_cursor.CharBackColor = -1 + + # Additional cleanup for individual text portions (optional) + text_portions = paragraph.createEnumeration() + while text_portions.hasMoreElements(): + text_portion = text_portions.nextElement() + if hasattr(text_portion, "CharBackColor"): + portion_cursor = text.createTextCursorByRange(text_portion) + portion_cursor.CharBackColor = -1 + current_index += 1 + + ret = "Successfully removed all highlighting" + return ret + except Exception as e: + ret = f"Error removing highlighting: {{str(e)}}" + return ret +libreoffice_writer_remove_highlighting(paragraph_indices={paragraph_indices}) +print(ret) +""" + + @agent_action + def libreoffice_writer_remove_highlighting(self, paragraph_indices): + + return self.LIBREOFFICE_WRITER_REMOVE_HIGHLIGHTING_CMD.format(paragraph_indices=repr(paragraph_indices)) + + LIBREOFFICE_WRITER_FIND_HIGHLIGHTED_TEXT_CMD = """import os +import re +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE, OBLIQUE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.style.ParagraphAdjust import CENTER, LEFT, RIGHT +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +text = doc.Text +cursor = text.createTextCursor() +ret = "" + +def libreoffice_writer_find_highlighted_text(highlight_color): + global ret + color_map = {{ + "yellow": 16776960, + "green": 65280, + "blue": 255, + "red": 16711680, + "cyan": 65535, + "magenta": 16711935, + "black": 0, + "white": 16777215, + "gray": 8421504, + "lightgray": 12632256, + }} + target_color = None + if highlight_color.lower() in color_map: + target_color = color_map[highlight_color.lower()] + elif highlight_color.startswith("#") and len(highlight_color) == 7: + try: + hex_color = highlight_color[1:] + r = int(hex_color[0:2], 16) + g = int(hex_color[2:4], 16) + b = int(hex_color[4:6], 16) + target_color = (r << 16) + (g << 8) + b + except ValueError: + ret = f"Invalid hex color format: {{highlight_color}}" + return [] + else: + ret = f"Unsupported color format: {{highlight_color}}" + return [] + highlighted_text = [] + text = doc.getText() + enum_paragraphs = text.createEnumeration() + while enum_paragraphs.hasMoreElements(): + paragraph = enum_paragraphs.nextElement() + if paragraph.supportsService("com.sun.star.text.Paragraph"): + enum_portions = paragraph.createEnumeration() + while enum_portions.hasMoreElements(): + text_portion = enum_portions.nextElement() + if hasattr(text_portion, "CharBackColor") and text_portion.CharBackColor == target_color: + if text_portion.getString().strip(): + highlighted_text.append(text_portion.getString()) + ret = f"Found {{len(highlighted_text)}} text segments with highlight color {{highlight_color}}" + return highlighted_text +libreoffice_writer_find_highlighted_text(highlight_color={highlight_color}) +print(ret) +""" + + @agent_action + def libreoffice_writer_find_highlighted_text(self, highlight_color): + + return self.LIBREOFFICE_WRITER_FIND_HIGHLIGHTED_TEXT_CMD.format(highlight_color=repr(highlight_color)) + + LIBREOFFICE_WRITER_INSERT_FORMULA_AT_CURSOR_CMD = """import os +import re +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE, OBLIQUE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.style.ParagraphAdjust import CENTER, LEFT, RIGHT +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +text = doc.Text +cursor = text.createTextCursor() +ret = "" + +def libreoffice_writer_insert_formula_at_cursor(formula): + global ret + try: + embedded_obj = doc.createInstance("com.sun.star.text.TextEmbeddedObject") + embedded_obj.setPropertyValue("CLSID", "078B7ABA-54FC-457F-8551-6147e776a997") + embedded_obj.setPropertyValue("AnchorType", AS_CHARACTER) + text.insertTextContent(cursor, embedded_obj, False) + math_obj = embedded_obj.getEmbeddedObject() + math_obj.Formula = formula + ret = "Formula inserted successfully" + return True + except Exception as e: + ret = f"Error inserting formula: {{str(e)}}" + return False +libreoffice_writer_insert_formula_at_cursor(formula={formula}) +print(ret) +""" + + @agent_action + def libreoffice_writer_insert_formula_at_cursor(self, formula): + + return self.LIBREOFFICE_WRITER_INSERT_FORMULA_AT_CURSOR_CMD.format(formula=repr(formula)) + + LIBREOFFICE_WRITER_INSERT_IMAGE_AT_CURSOR_CMD = """import os +import re +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE, OBLIQUE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.style.ParagraphAdjust import CENTER, LEFT, RIGHT +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +text = doc.Text +cursor = text.createTextCursor() +ret = "" + +def libreoffice_writer_insert_image_at_cursor(image_path, width=None, height=None): + global ret + try: + if image_path.startswith("~"): + image_path = os.path.expanduser(image_path) + if not os.path.exists(image_path): + ret = f"Error: Image file not found at {{image_path}}" + return ret + image_path = os.path.abspath(image_path) + if os.name == "nt": + file_url = "file:///" + image_path.replace("\\", "/") + else: + file_url = "file://" + image_path + graphic = doc.createInstance("com.sun.star.text.GraphicObject") + graphic.GraphicURL = file_url + graphic.AnchorType = AS_CHARACTER + if width is not None: + graphic.Width = width * 100 + if height is not None: + graphic.Height = height * 100 + text.insertTextContent(cursor, graphic, False) + ret = "Success: Image inserted" + return ret + except Exception as e: + ret = f"Error: {{str(e)}}" + return ret +libreoffice_writer_insert_image_at_cursor(image_path={image_path}, width={width}, height={height}) +print(ret) +""" + + @agent_action + def libreoffice_writer_insert_image_at_cursor(self, image_path, width, height): + + return self.LIBREOFFICE_WRITER_INSERT_IMAGE_AT_CURSOR_CMD.format(image_path=repr(image_path), width=repr(width), + height=repr(height)) + + LIBREOFFICE_WRITER_SET_STRIKETHROUGH_CMD = """import os +import re +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE, OBLIQUE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.style.ParagraphAdjust import CENTER, LEFT, RIGHT +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +text = doc.Text +cursor = text.createTextCursor() +ret = "" + +def libreoffice_writer_set_strikethrough(pattern, paragraph_indices=None): + global ret + try: + paragraphs = doc.getText().createEnumeration() + para_index = 0 + found_matches = 0 + while paragraphs.hasMoreElements(): + paragraph = paragraphs.nextElement() + if paragraph.supportsService("com.sun.star.text.Paragraph"): + if paragraph_indices and para_index not in paragraph_indices: + para_index += 1 + continue + para_text = paragraph.getString() + matches = list(re.finditer(pattern, para_text)) + for match in matches: + text_range = paragraph.getStart() + cursor = doc.getText().createTextCursorByRange(text_range) + cursor.goRight(match.start(), False) + cursor.goRight(match.end() - match.start(), True) + cursor.CharStrikeout = 1 + found_matches += 1 + para_index += 1 + ret = f"Successfully applied strikethrough to {{found_matches}} matches of pattern: {{pattern}}" + return ret + except Exception as e: + ret = f"Error applying strikethrough: {{str(e)}}" + return ret +libreoffice_writer_set_strikethrough(pattern={pattern}, paragraph_indices={paragraph_indices}) +print(ret) +""" + + @agent_action + def libreoffice_writer_set_strikethrough(self, pattern, paragraph_indices): + + return self.LIBREOFFICE_WRITER_SET_STRIKETHROUGH_CMD.format(pattern=repr(pattern), + paragraph_indices=repr(paragraph_indices)) + + LIBREOFFICE_WRITER_SET_FONT_SIZE_CMD = """import os +import re +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE, OBLIQUE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.style.ParagraphAdjust import CENTER, LEFT, RIGHT +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +text = doc.Text +cursor = text.createTextCursor() +ret = "" + +def libreoffice_writer_set_font_size(font_size, pattern, paragraph_indices=None): + global ret + try: + regex = re.compile(pattern) + paragraphs = doc.getText().createEnumeration() + current_index = 0 + while paragraphs.hasMoreElements(): + paragraph = paragraphs.nextElement() + if paragraph_indices and current_index not in paragraph_indices: + current_index += 1 + continue + if paragraph.supportsService("com.sun.star.text.Paragraph"): + para_cursor = text.createTextCursorByRange(paragraph) + para_text = paragraph.getString() + matches = list(regex.finditer(para_text)) + for match in reversed(matches): + start_pos = match.start() + end_pos = match.end() + para_cursor.gotoStart(False) + para_cursor.goRight(start_pos, False) + para_cursor.goRight(end_pos - start_pos, True) + para_cursor.CharHeight = font_size + current_index += 1 + ret = f"Successfully changed font size to {{font_size}} for text matching '{{pattern}}'" + return ret + except Exception as e: + ret = f"Error changing font size: {{str(e)}}" + return ret +libreoffice_writer_set_font_size(font_size={font_size}, pattern={pattern}, paragraph_indices={paragraph_indices}) +print(ret) +""" + + @agent_action + def libreoffice_writer_set_font_size(self, font_size, pattern, paragraph_indices): + + return self.LIBREOFFICE_WRITER_SET_FONT_SIZE_CMD.format(font_size=repr(font_size), pattern=repr(pattern), + paragraph_indices=repr(paragraph_indices)) + + LIBREOFFICE_WRITER_EXPORT_TO_PDF_CMD = """import os +import re +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE, OBLIQUE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.style.ParagraphAdjust import CENTER, LEFT, RIGHT +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +text = doc.Text +cursor = text.createTextCursor() +ret = "" + +def libreoffice_writer_export_to_pdf(output_path=None, output_filename=None, include_comments=False, quality="standard"): + global ret + try: + doc_url = doc.getURL() + if not doc_url and not output_path: + return "Error: Document has not been saved and no output path provided" + if doc_url: + doc_path = uno.fileUrlToSystemPath(os.path.dirname(doc_url)) + doc_filename = os.path.basename(doc_url) + doc_name = os.path.splitext(doc_filename)[0] + else: + doc_path = "" + doc_name = "export" + final_path = output_path if output_path else doc_path + final_filename = output_filename if output_filename else f"{{doc_name}}.pdf" + if not final_filename.lower().endswith(".pdf"): + final_filename += ".pdf" + full_output_path = os.path.join(final_path, final_filename) + output_url = uno.systemPathToFileUrl(full_output_path) + export_props = [] + if quality == "high": + export_props.append(PropertyValue(Name="SelectPdfVersion", Value=1)) + elif quality == "print": + export_props.append(PropertyValue(Name="SelectPdfVersion", Value=2)) + else: + export_props.append(PropertyValue(Name="SelectPdfVersion", Value=0)) + export_props.append(PropertyValue(Name="ExportNotes", Value=include_comments)) + export_props.extend( + [ + PropertyValue(Name="FilterName", Value="writer_pdf_Export"), + PropertyValue(Name="Overwrite", Value=True), + ] + ) + doc.storeToURL(output_url, tuple(export_props)) + ret = f"PDF exported to: {{full_output_path}}" + return full_output_path + except Exception as e: + ret = f"Error exporting to PDF: {{str(e)}}" + return ret +libreoffice_writer_export_to_pdf(output_path={output_path}, output_filename={output_filename}, include_comments={include_comments}, quality={quality}) +print(ret) +""" + + @agent_action + def libreoffice_writer_export_to_pdf(self, output_path=None, output_filename=None, include_comments=False, + quality="standard"): + + return self.LIBREOFFICE_WRITER_EXPORT_TO_PDF_CMD.format(output_path=repr(output_path), + output_filename=repr(output_filename), + include_comments=repr(include_comments), + quality=repr(quality)) + # @agent_action + # def libreoffice_writer_export_to_pdf(self, output_path, output_filename, include_comments, quality): + # + # return self.LIBREOFFICE_WRITER_EXPORT_TO_PDF_CMD.format(output_path=repr(output_path), + # output_filename=repr(output_filename), + # include_comments=repr(include_comments), + # quality=repr(quality)) + + LIBREOFFICE_WRITER_SET_PARAGRAPH_ALIGNMENT_CMD = """import os +import re +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE, OBLIQUE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.style.ParagraphAdjust import CENTER, LEFT, RIGHT +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +text = doc.Text +cursor = text.createTextCursor() +ret = "" + +def libreoffice_writer_set_paragraph_alignment(alignment, paragraph_indices=None): + global ret + try: + alignment_map = {{"left": LEFT, "center": CENTER, "right": RIGHT, "justify": 3}} + if alignment.lower() not in alignment_map: + ret = f"Error: Invalid alignment '{{alignment}}'. Use 'left', 'center', 'right', or 'justify'." + return ret + alignment_value = alignment_map[alignment.lower()] + text = doc.getText() + paragraph_enum = text.createEnumeration() + paragraphs = [] + while paragraph_enum.hasMoreElements(): + paragraph = paragraph_enum.nextElement() + if paragraph.supportsService("com.sun.star.text.Paragraph"): + paragraphs.append(paragraph) + if paragraph_indices: + valid_indices = [i for i in paragraph_indices if 0 <= i < len(paragraphs)] + if len(valid_indices) != len(paragraph_indices): + ret = f"Warning: Some paragraph indices were out of range (0-{{len(paragraphs) - 1}})" + for idx in valid_indices: + paragraphs[idx].ParaAdjust = alignment_value + else: + for paragraph in paragraphs: + paragraph.ParaAdjust = alignment_value + ret = f"Successfully applied '{{alignment}}' alignment to paragraphs" + return ret + except Exception as e: + ret = f"Error setting paragraph alignment: {{str(e)}}" + return ret +libreoffice_writer_set_paragraph_alignment(alignment={alignment}, paragraph_indices={paragraph_indices}) +print(ret) +""" + + @agent_action + def libreoffice_writer_set_paragraph_alignment(self, alignment, paragraph_indices): + + return self.LIBREOFFICE_WRITER_SET_PARAGRAPH_ALIGNMENT_CMD.format(alignment=repr(alignment), + paragraph_indices=repr(paragraph_indices)) + + LIBREOFFICE_WRITER_CAPITALIZE_WORDS_CMD = """import os +import re +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE, OBLIQUE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.style.ParagraphAdjust import CENTER, LEFT, RIGHT +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +text = doc.Text +cursor = text.createTextCursor() +ret = "" + +def libreoffice_writer_capitalize_words(paragraph_indices=None): + global ret + try: + text = doc.getText() + enum = text.createEnumeration() + paragraphs = [] + while enum.hasMoreElements(): + paragraph = enum.nextElement() + if paragraph.supportsService("com.sun.star.text.Paragraph"): + paragraphs.append(paragraph) + if not paragraph_indices: + target_paragraphs = list(range(len(paragraphs))) + else: + target_paragraphs = paragraph_indices + valid_indices = [idx for idx in target_paragraphs if 0 <= idx < len(paragraphs)] + for idx in valid_indices: + paragraph = paragraphs[idx] + text_content = paragraph.getString() + if not text_content.strip(): + continue + capitalized_text = " ".join(word.capitalize() if word else "" for word in text_content.split(" ")) + para_cursor = text.createTextCursorByRange(paragraph.getStart()) + para_cursor.gotoRange(paragraph.getEnd(), True) + para_cursor.setString(capitalized_text) + ret = f"Successfully capitalized words in {{len(valid_indices)}} paragraphs" + return ret + except Exception as e: + ret = f"Error capitalizing words: {{str(e)}}" + return ret +libreoffice_writer_capitalize_words(paragraph_indices={paragraph_indices}) +print(ret) +""" + + @agent_action + def libreoffice_writer_capitalize_words(self, paragraph_indices): + + return self.LIBREOFFICE_WRITER_CAPITALIZE_WORDS_CMD.format(paragraph_indices=repr(paragraph_indices)) + + LIBREOFFICE_WRITER_SET_DEFAULT_FONT_CMD = """import os +import re +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE, OBLIQUE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.style.ParagraphAdjust import CENTER, LEFT, RIGHT +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +text = doc.Text +cursor = text.createTextCursor() +ret = "" + +def libreoffice_writer_set_default_font(font_name, font_size=None): + global ret + try: + style_families = doc.getStyleFamilies() + paragraph_styles = style_families.getByName("ParagraphStyles") + default_style_names = ["Default", "Standard", "Normal"] + standard_style = None + for style_name in default_style_names: + if paragraph_styles.hasByName(style_name): + standard_style = paragraph_styles.getByName(style_name) + break + if standard_style is None: + style_names = paragraph_styles.getElementNames() + if style_names: + standard_style = paragraph_styles.getByName(style_names[0]) + else: + raise Exception("Could not find default paragraph style") + standard_style.setPropertyValue("CharFontName", font_name) + standard_style.setPropertyValue("CharFontNameAsian", font_name) + standard_style.setPropertyValue("CharFontNameComplex", font_name) + if font_size is not None: + standard_style.setPropertyValue("CharHeight", float(font_size)) + standard_style.setPropertyValue("CharHeightAsian", float(font_size)) + standard_style.setPropertyValue("CharHeightComplex", float(font_size)) + cursor.setPropertyValue("CharFontName", font_name) + cursor.setPropertyValue("CharFontNameAsian", font_name) + cursor.setPropertyValue("CharFontNameComplex", font_name) + if font_size is not None: + cursor.setPropertyValue("CharHeight", float(font_size)) + cursor.setPropertyValue("CharHeightAsian", float(font_size)) + cursor.setPropertyValue("CharHeightComplex", float(font_size)) + ret = f"Default font set to '{{font_name}}'" + (f" with size {{font_size}}pt" if font_size else "") + return ret + except Exception as e: + ret = f"Error setting default font: {{str(e)}}" + return ret +libreoffice_writer_set_default_font(font_name={font_name}, font_size={font_size}) +print(ret) +""" + + @agent_action + def libreoffice_writer_set_default_font(self, font_name, font_size=None): + + return self.LIBREOFFICE_WRITER_SET_DEFAULT_FONT_CMD.format(font_name=repr(font_name), font_size=repr(font_size)) + + # @agent_action + # def libreoffice_writer_set_default_font(self, font_name, font_size): + # + # return self.LIBREOFFICE_WRITER_SET_DEFAULT_FONT_CMD.format(font_name=repr(font_name), font_size=repr(font_size)) + + LIBREOFFICE_WRITER_ADD_PAGE_NUMBERS_CMD = """import os +import re +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE, OBLIQUE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.style.ParagraphAdjust import CENTER, LEFT, RIGHT +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +text = doc.Text +cursor = text.createTextCursor() +ret = "" + +def libreoffice_writer_add_page_numbers(position, start_number=1, format=None): + global ret + try: + page_styles = doc.StyleFamilies.getByName("PageStyles") + default_style = page_styles.getByName("Standard") + try: + default_style.setPropertyValue("PageNumberOffset", start_number) + except: + pass + if position.startswith("top"): + default_style.HeaderIsOn = True + target = default_style.HeaderText + else: + default_style.FooterIsOn = True + target = default_style.FooterText + cursor = target.createTextCursor() + cursor.gotoStart(False) + cursor.gotoEnd(True) + cursor.setString("") + cursor.gotoStart(False) + if position.endswith("_left"): + cursor.ParaAdjust = LEFT + elif position.endswith("_center"): + cursor.ParaAdjust = CENTER + elif position.endswith("_right"): + cursor.ParaAdjust = RIGHT + if not format or format == "1": + page_number = doc.createInstance("com.sun.star.text.TextField.PageNumber") + page_number.NumberingType = 4 + target.insertTextContent(cursor, page_number, False) + elif format == "Page 1" or "Page" in format and "of" not in format: + target.insertString(cursor, "Page ", False) + page_number = doc.createInstance("com.sun.star.text.TextField.PageNumber") + page_number.NumberingType = 4 + target.insertTextContent(cursor, page_number, False) + elif format == "1 of N" or format == "Page {{page}} of {{total}}" or "of" in format: + if "Page" in format: + target.insertString(cursor, "Page ", False) + page_number = doc.createInstance("com.sun.star.text.TextField.PageNumber") + page_number.NumberingType = 4 + target.insertTextContent(cursor, page_number, False) + target.insertString(cursor, " of ", False) + page_count = doc.createInstance("com.sun.star.text.TextField.PageCount") + page_count.NumberingType = 4 + target.insertTextContent(cursor, page_count, False) + else: + page_number = doc.createInstance("com.sun.star.text.TextField.PageNumber") + page_number.NumberingType = 4 + target.insertTextContent(cursor, page_number, False) + ret = "Successfully added page numbers" + return ret + except Exception as e: + ret = f"Error adding page numbers: {{str(e)}}" + return ret +libreoffice_writer_add_page_numbers(position={position}, start_number={start_number}, format={format}) +print(ret) +""" + + @agent_action + def libreoffice_writer_add_page_numbers(self, position, start_number=1, format=None): + return self.LIBREOFFICE_WRITER_ADD_PAGE_NUMBERS_CMD.format(position=repr(position), + start_number=repr(start_number), + format=repr(format)) + + # @agent_action + # def libreoffice_writer_add_page_numbers(self, position, start_number, format): + # + # return self.LIBREOFFICE_WRITER_ADD_PAGE_NUMBERS_CMD.format(position=repr(position), + # start_number=repr(start_number), format=repr(format)) + + LIBREOFFICE_WRITER_INSERT_PAGE_BREAK_CMD = """import os +import re +import uno +from com.sun.star.awt.FontSlant import ITALIC, NONE, OBLIQUE +from com.sun.star.awt.FontWeight import BOLD, NORMAL +from com.sun.star.beans import PropertyValue +from com.sun.star.style.ParagraphAdjust import CENTER, LEFT, RIGHT +from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK +from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +text = doc.Text +cursor = text.createTextCursor() +ret = "" + +def libreoffice_writer_insert_page_break(position="at_cursor"): + global ret + try: + if position == "end_of_document": + cursor.gotoEnd(False) + text.insertControlCharacter(cursor, PARAGRAPH_BREAK, False) + cursor.gotoStartOfParagraph(True) + cursor.BreakType = uno.Enum("com.sun.star.style.BreakType", "PAGE_BEFORE") + ret = "Page break inserted successfully" + return True + except Exception as e: + ret = f"Error inserting page break: {{str(e)}}" + return False +libreoffice_writer_insert_page_break(position={position}) +print(ret) +""" + + @agent_action + def libreoffice_writer_insert_page_break(self, position): + + return self.LIBREOFFICE_WRITER_INSERT_PAGE_BREAK_CMD.format(position=repr(position)) + + LIBREOFFICE_CALC_SAVE_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_save(): + global ret + try: + # Just save the document + doc.store() + ret = "Success" + return True + except Exception as e: + ret = f"Error: {{e}}" + return False +libreoffice_calc_save() +print(ret) +""" + + @agent_action + def libreoffice_calc_save(self): + + return self.LIBREOFFICE_CALC_SAVE_CMD.format() + + LIBREOFFICE_CALC_GET_WORKBOOK_INFO_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_get_workbook_info(): + global ret + try: + info = {{ + "file_path": doc.getLocation(), + "file_title": doc.getTitle(), + "sheets": [], + "active_sheet": sheet.Name, + }} + + # Get sheets information + sheets = doc.getSheets() + info["sheet_count"] = sheets.getCount() + + # Get all sheet names and info + for i in range(sheets.getCount()): + sheet = sheets.getByIndex(i) + cursor = sheet.createCursor() + cursor.gotoEndOfUsedArea(False) + end_col = cursor.getRangeAddress().EndColumn + end_row = cursor.getRangeAddress().EndRow + + sheet_info = {{ + "name": sheet.getName(), + "index": i, + "visible": sheet.IsVisible, + "row_count": end_row + 1, + "column_count": end_col + 1, + }} + info["sheets"].append(sheet_info) + + # Check if this is the active sheet + if sheet == sheet: + info["active_sheet"] = sheet_info + + ret = json.dumps(info, ensure_ascii=False) + return info + + except Exception as e: + ret = f"Error: {{e}}" +libreoffice_calc_get_workbook_info() +print(ret) +""" + + @agent_action + def libreoffice_calc_get_workbook_info(self): + + return self.LIBREOFFICE_CALC_GET_WORKBOOK_INFO_CMD.format() + + LIBREOFFICE_CALC_GET_COLUMN_DATA_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_get_column_index(column_name, sheet=None): + + try: + return ord(column_name[0]) - ord("A") + except ValueError: + return None +def libreoffice_calc_get_last_used_row(): + + cursor = sheet.createCursor() + cursor.gotoEndOfUsedArea(False) + return cursor.RangeAddress.EndRow + +def libreoffice_calc_get_column_data(column_name): + global ret + column_index = libreoffice_calc_get_column_index(column_name) + if column_index is None: + return "Column not found" + last_row = libreoffice_calc_get_last_used_row() + _range = sheet.getCellRangeByPosition(column_index, 0, column_index, last_row) + # 获取数据数组并展平 + ret = json.dumps([row[0] for row in _range.getDataArray()], ensure_ascii=False) + return [row[0] for row in _range.getDataArray()] +libreoffice_calc_get_column_data(column_name={column_name}) +print(ret) +""" + + @agent_action + def libreoffice_calc_get_column_data(self, column_name): + + return self.LIBREOFFICE_CALC_GET_COLUMN_DATA_CMD.format(column_name=repr(column_name)) + + LIBREOFFICE_CALC_SWITCH_ACTIVE_SHEET_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_switch_active_sheet(sheet_name): + global ret + try: + # 获取所有工作表 + sheets = doc.getSheets() + + # 检查工作表是否存在 + if not sheets.hasByName(sheet_name): + # 创建新工作表 + new_sheet = doc.createInstance("com.sun.star.sheet.Spreadsheet") + sheets.insertByName(sheet_name, new_sheet) + + # 获取目标工作表 + sheet = sheets.getByName(sheet_name) + + # 切换到目标工作表 + doc.getCurrentController().setActiveSheet(sheet) + + # 更新当前工作表引用 + sheet = sheet + ret = "Success" + return True + + except Exception as e: + ret = f"Error: {{e}}" + return False +libreoffice_calc_switch_active_sheet(sheet_name={sheet_name}) +print(ret) +""" + + LIBREOFFICE_CALC_GET_ACTIVE_SHEET_DATA_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_get_last_used_row(): + cursor = sheet.createCursor() + cursor.gotoEndOfUsedArea(False) + return cursor.RangeAddress.EndRow + +def libreoffice_calc_get_last_used_column(): + cursor = sheet.createCursor() + cursor.gotoEndOfUsedArea(False) + return cursor.RangeAddress.EndColumn + +def libreoffice_calc_get_active_sheet_data(): + global ret + try: + # 获取使用范围的最后行和列 + last_row = libreoffice_calc_get_last_used_row() + last_col = libreoffice_calc_get_last_used_column() + + # 如果没有数据,返回空结果 + if last_row == -1 or last_col == -1: + ret = json.dumps({{"data": [], "rows": 0, "columns": 0}}, ensure_ascii=False) + return {{"data": [], "rows": 0, "columns": 0}} + + # 获取整个使用范围的数据 + data_range = sheet.getCellRangeByPosition(0, 0, last_col, last_row) + data_array = data_range.getDataArray() + + # 转换为带坐标信息的数据结构 + sheet_data = [] + for row_idx, row in enumerate(data_array): + row_data = [] + for col_idx, cell in enumerate(row): + # 计算Excel风格的列名 (A, B, C, ...) + col_name = chr(ord('A') + col_idx) if col_idx < 26 else f"A{{chr(ord('A') + col_idx - 26)}}" + cell_address = f"{{col_name}}{{row_idx + 1}}" + + # 处理不同类型的单元格值 + if isinstance(cell, (int, float)): + cell_value = cell + elif isinstance(cell, str): + cell_value = cell + else: + cell_value = str(cell) + + row_data.append({{ + "address": cell_address, + "value": cell_value, + "row": row_idx + 1, + "col": col_idx + 1, + "col_name": col_name, + "is_empty": cell_value == "" or cell_value == "--" + }}) + sheet_data.append(row_data) + + result = {{ + "data": sheet_data, + "rows": last_row + 1, + "columns": last_col + 1, + "range": f"A1:{{chr(ord('A') + last_col)}}{{last_row + 1}}" + }} + + ret = json.dumps(result, ensure_ascii=False) + return result + + except Exception as e: + ret = f"Error: {{e}}" + return None +libreoffice_calc_get_active_sheet_data() +print(ret) +""" + + @agent_action + def libreoffice_calc_get_active_sheet_data(self): + return self.LIBREOFFICE_CALC_GET_ACTIVE_SHEET_DATA_CMD.format() + + @agent_action + def libreoffice_calc_switch_active_sheet(self, sheet_name): + + return self.LIBREOFFICE_CALC_SWITCH_ACTIVE_SHEET_CMD.format(sheet_name=repr(sheet_name)) + + LIBREOFFICE_CALC_SET_COLUMN_VALUES_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" +def libreoffice_calc_get_column_index(column_name, sheet=None): + try: + return ord(column_name[0]) - ord("A") + except ValueError: + return None + +def libreoffice_calc_set_column_values(column_name, data, start_index=2): + global ret + column_index = libreoffice_calc_get_column_index(column_name) + if column_index is None: + ret = "Column not found" + return False + for i, value in enumerate(data): + cell = sheet.getCellByPosition(column_index, i + start_index - 1) + if isinstance(value, str) and value.startswith("="): + cell.setFormula(value) # ✅ 关键修复:公式用 setFormula + elif isinstance(value, float) and value.is_integer(): + cell.setNumber(int(value)) + else: + cell.setString(str(value)) + ret = "Success" + return True + +libreoffice_calc_set_column_values(column_name={column_name}, data={data}, start_index={start_index}) +print(ret) +""" + + @agent_action + def libreoffice_calc_set_column_values(self, column_name, data, start_index): + + return self.LIBREOFFICE_CALC_SET_COLUMN_VALUES_CMD.format(column_name=repr(column_name), data=repr(data), + start_index=repr(start_index)) + + LIBREOFFICE_CALC_HIGHLIGHT_RANGE_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_highlight_range(range_str, color=0xFF0000): + global ret + try: + _range = sheet.getCellRangeByName(range_str) + _range.CellBackColor = color + ret = "Success" + return True + except: + ret = "False" + return False +libreoffice_calc_highlight_range(range_str={range_str}, color={color}) +print(ret) +""" + + @agent_action + def libreoffice_calc_highlight_range(self, range_str, color): + """ + highlight the specified range with the specified color + +Args: + range_str (str): Range to highlight, in the format of "A1:B10" + color (str): Color to highlight with, default is '0xFF0000' (red) + +Returns: + bool: True if successful, False otherwise + """ + + return self.LIBREOFFICE_CALC_HIGHLIGHT_RANGE_CMD.format(range_str=repr(range_str), color=repr(color)) + + LIBREOFFICE_CALC_TRANSPOSE_RANGE_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_transpose_range(source_range, target_cell): + global ret + try: + source = sheet.getCellRangeByName(source_range) + target = sheet.getCellRangeByName(target_cell) + + data = source.getDataArray() + # 转置数据 + transposed_data = list(map(list, zip(*data))) + + # 设置转置后的数据 + target_range = sheet.getCellRangeByPosition( + target.CellAddress.Column, + target.CellAddress.Row, + target.CellAddress.Column + len(transposed_data[0]) - 1, + target.CellAddress.Row + len(transposed_data) - 1, + ) + target_range.setDataArray(transposed_data) + ret = "Success" + return True + except Exception as e: + ret = f"Error: {{e}}" + return False +libreoffice_calc_transpose_range(source_range={source_range}, target_cell={target_cell}) +print(ret) +""" + + @agent_action + def libreoffice_calc_transpose_range(self, source_range, target_cell): + """ + Transpose the specified range and paste it to the target cell + +Args: + source_range (str): Range to transpose, in the format of "A1:B10" + target_cell (str): Target cell to paste the transposed data, in the format of "A1" + +Returns: + bool: True if successful, False otherwise + """ + + return self.LIBREOFFICE_CALC_TRANSPOSE_RANGE_CMD.format(source_range=repr(source_range), + target_cell=repr(target_cell)) + + LIBREOFFICE_CALC_EXPORT_TO_CSV_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_export_to_csv(): + global ret + try: + # 获取当前文档的URL + doc_url = doc.getURL() + if not doc_url: + raise ValueError("Document must be saved first") + + # 构造CSV文件路径 + if doc_url.startswith("file://"): + base_path = doc_url[7:] # 移除 'file://' 前缀 + else: + base_path = doc_url + + # 获取基本路径和文件名 + csv_path = os.path.splitext(base_path)[0] + ".csv" + + # 确保路径是绝对路径 + csv_path = os.path.abspath(csv_path) + + # 转换为 LibreOffice URL 格式 + csv_url = uno.systemPathToFileUrl(csv_path) + + # 设置CSV导出选项 + props = ( + PropertyValue(Name="FilterName", Value="Text - txt - csv (StarCalc)"), + PropertyValue( + Name="FilterOptions", Value="44,0,76,0" + ), # 44=comma, 34=quote, 76=UTF-8, 1=first row as header + ) + + # 导出文件 + doc.storeToURL(csv_url, props) + ret = "Success" + return True + + except Exception as e: + ret = f"Error: {{e}}" + return False +libreoffice_calc_export_to_csv() +print(ret) +""" + + @agent_action + def libreoffice_calc_export_to_csv(self): + + return self.LIBREOFFICE_CALC_EXPORT_TO_CSV_CMD.format() + + LIBREOFFICE_CALC_SORT_COLUMN_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" +def libreoffice_calc_get_column_index(column_name, sheet=None): + + try: + return ord(column_name[0]) - ord("A") + except ValueError: + return None +def libreoffice_calc_get_last_used_row(): + + cursor = sheet.createCursor() + cursor.gotoEndOfUsedArea(False) + return cursor.RangeAddress.EndRow + +def libreoffice_calc_get_column_data(column_name): + global ret + column_index = libreoffice_calc_get_column_index(column_name) + if column_index is None: + return "Column not found" + last_row = libreoffice_calc_get_last_used_row() + _range = sheet.getCellRangeByPosition(column_index, 0, column_index, last_row) + # 获取数据数组并展平 + ret = json.dumps([row[0] for row in _range.getDataArray()], ensure_ascii=False) + return [row[0] for row in _range.getDataArray()] + +def libreoffice_calc_get_column_index(column_name, sheet=None): + + try: + return ord(column_name[0]) - ord("A") + except ValueError: + return None +def libreoffice_calc_set_column_values(column_name, data, start_index=2): + global ret + # 获取列的索引 + column_index = libreoffice_calc_get_column_index(column_name) + if column_index is None: + ret = "Column not found" + return False + for i, value in enumerate(data): + cell = sheet.getCellByPosition(column_index, i + start_index - 1) + if type(value) == float and value.is_integer(): + cell.setNumber(int(value)) + else: + cell.setString(str(value)) + ret = "Success" + return True + +def libreoffice_calc_sort_column(column_name, ascending=True, start_index=2): + global ret + try: + column_data = libreoffice_calc_get_column_data(column_name)[start_index - 1 :] + column_data = sorted(column_data, key=lambda x: float(x), reverse=not ascending) + except: + ret = "Error: Invalid column name or data type" + return False + + return libreoffice_calc_set_column_values(column_name, column_data, start_index) +libreoffice_calc_sort_column(column_name={column_name}, ascending={ascending}, start_index={start_index}) +print(ret) +""" + + @agent_action + def libreoffice_calc_sort_column(self, column_name, ascending, start_index): + + return self.LIBREOFFICE_CALC_SORT_COLUMN_CMD.format(column_name=repr(column_name), ascending=repr(ascending), + start_index=repr(start_index)) + + LIBREOFFICE_CALC_SET_VALIDATION_LIST_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" +def libreoffice_calc_get_column_index(column_name, sheet=None): + + try: + return ord(column_name[0]) - ord("A") + except ValueError: + return None +def libreoffice_calc_get_last_used_row(): + + cursor = sheet.createCursor() + cursor.gotoEndOfUsedArea(False) + return cursor.RangeAddress.EndRow +def libreoffice_calc_set_validation_list(column_name, values): + global ret + try: + column_index = libreoffice_calc_get_column_index(column_name) + last_row = libreoffice_calc_get_last_used_row() + cell_range = sheet.getCellRangeByPosition(column_index, 1, column_index, last_row) + + # 获取现有的验证对象 + validation = cell_range.getPropertyValue("Validation") + + # 设置基本验证类型 + validation.Type = uno.Enum("com.sun.star.sheet.ValidationType", "LIST") + validation.Operator = uno.Enum("com.sun.star.sheet.ConditionOperator", "EQUAL") + + # 设置下拉列表 + validation.ShowList = True + # 调试:打印实际的值 + print(f"Debug: Original values = {{{{values}}}}") + # 用双引号包围每个值,这是 LibreOffice 的标准格式 + values_str = ";".join('"' + str(val) + '"' for val in values) + print(f"Debug: values_str = '{{{{values_str}}}}'") + validation.Formula1 = values_str + + # 应用验证设置回单元格范围 + cell_range.setPropertyValue("Validation", validation) + + ret = "Success" + return True + + except Exception as e: + ret = f"Error: {{e}}" + return False +libreoffice_calc_set_validation_list(column_name={column_name}, values={values}) +print(ret) +""" + + @agent_action + def libreoffice_calc_set_validation_list(self, column_name, values): + + return self.LIBREOFFICE_CALC_SET_VALIDATION_LIST_CMD.format(column_name=repr(column_name), values=repr(values)) + + LIBREOFFICE_CALC_HIDE_ROW_DATA_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" +def libreoffice_calc_get_last_used_column(): + + cursor = sheet.createCursor() + cursor.gotoEndOfUsedArea(False) + return cursor.RangeAddress.EndColumn + +def libreoffice_calc_get_last_used_row(): + + cursor = sheet.createCursor() + cursor.gotoEndOfUsedArea(False) + return cursor.RangeAddress.EndRow +def libreoffice_calc_hide_row_data(value="N/A"): + global ret + last_row = libreoffice_calc_get_last_used_row() + last_col = libreoffice_calc_get_last_used_column() + + for row in range(1, last_row + 1): + has_value = False + for col in range(last_col + 1): + cell = sheet.getCellByPosition(col, row) + if cell.getString() == value: + has_value = True + break + row_range = sheet.getRows().getByIndex(row) + row_range.IsVisible = not has_value + + ret = "Success" + return True +libreoffice_calc_hide_row_data(value={value}) +print(ret) +""" + + @agent_action + def libreoffice_calc_hide_row_data(self, value): + + return self.LIBREOFFICE_CALC_HIDE_ROW_DATA_CMD.format(value=repr(value)) + + LIBREOFFICE_CALC_REORDER_COLUMNS_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" +def libreoffice_calc_get_column_index(column_name, sheet=None): + + try: + return ord(column_name[0]) - ord("A") + except ValueError: + return None +def libreoffice_calc_reorder_columns(column_order): + global ret + try: + # 获取新的列索引 + new_indices = [libreoffice_calc_get_column_index(col) for col in column_order] + + # 创建新的列顺序 + for new_index, old_index in enumerate(new_indices): + if new_index != old_index: + sheet.Columns.insertByIndex(new_index, 1) + source = sheet.Columns[old_index + (old_index > new_index)] + target = sheet.Columns[new_index] + target.setDataArray(source.getDataArray()) + sheet.Columns.removeByIndex(old_index + (old_index > new_index), 1) + ret = "Success" + return True + + except Exception as e: + ret = f"Error: {{e}}" + return False +libreoffice_calc_reorder_columns(column_order={column_order}) +print(ret) +""" + + @agent_action + def libreoffice_calc_reorder_columns(self, column_order): + + return self.LIBREOFFICE_CALC_REORDER_COLUMNS_CMD.format(column_order=repr(column_order)) + + LIBREOFFICE_CALC_CREATE_PIVOT_TABLE_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" +def libreoffice_calc_get_column_index(column_name, sheet=None): + + try: + return ord(column_name[0]) - ord("A") + except ValueError: + return None +def libreoffice_calc_create_pivot_table(source_sheet, + table_name, + row_fields=None, + col_fields=None, + value_fields=None, + aggregation_function="sum", + target_cell="A1", + ): + global ret + try: + source = doc.getSheets().getByName(source_sheet) + + # 获取数据范围 + cursor = source.createCursor() + cursor.gotoEndOfUsedArea(False) + end_col = cursor.getRangeAddress().EndColumn + end_row = cursor.getRangeAddress().EndRow + + # 获取完整的数据范围 + source_range = source.getCellRangeByPosition(0, 0, end_col, end_row) + + # 获取数据透视表集合 + dp_tables = sheet.getDataPilotTables() + + # 创建数据透视表描述符 + dp_descriptor = dp_tables.createDataPilotDescriptor() + + # 设置数据源 + dp_descriptor.setSourceRange(source_range.getRangeAddress()) + + # 设置行字段 + if row_fields: + for field in row_fields: + field_index = libreoffice_calc_get_column_index(field) + dimension = dp_descriptor.getDataPilotFields().getByIndex(field_index) + dimension.Orientation = uno.Enum("com.sun.star.sheet.DataPilotFieldOrientation", "ROW") + + # 设置列字段 + if col_fields: + for field in col_fields: + field_index = libreoffice_calc_get_column_index(field) + dimension = dp_descriptor.getDataPilotFields().getByIndex(field_index) + dimension.Orientation = uno.Enum("com.sun.star.sheet.DataPilotFieldOrientation", "COLUMN") + + # 设置数据字段 + for field in value_fields: + field_index = libreoffice_calc_get_column_index(field) + dimension = dp_descriptor.getDataPilotFields().getByIndex(field_index) + dimension.Orientation = uno.Enum("com.sun.star.sheet.DataPilotFieldOrientation", "DATA") + + # 设置聚合函数 + function_map = {{"Count": "COUNT", "Sum": "SUM", "Average": "AVERAGE", "Min": "MIN", "Max": "MAX"}} + + if aggregation_function in function_map: + dimension.Function = uno.Enum( + "com.sun.star.sheet.GeneralFunction", function_map[aggregation_function] + ) + + # 在当前工作表中创建数据透视表 + dp_tables.insertNewByName( + table_name, # 透视表名称 + sheet.getCellRangeByName(target_cell).CellAddress, # 目标位置 + dp_descriptor, # 描述符 + ) + + ret = "Success" + return True + except Exception as e: + ret = f"Error: {{e}}" + return False +libreoffice_calc_create_pivot_table(source_sheet={source_sheet}, table_name={table_name}, row_fields={row_fields}, col_fields={col_fields}, value_fields={value_fields}, aggregation_function={aggregation_function}, target_cell={target_cell}) +print(ret) +""" + + @agent_action + def libreoffice_calc_create_pivot_table(self, source_sheet, table_name, row_fields, col_fields, value_fields, + aggregation_function, target_cell): + + return self.LIBREOFFICE_CALC_CREATE_PIVOT_TABLE_CMD.format(source_sheet=repr(source_sheet), + table_name=repr(table_name), + row_fields=repr(row_fields), + col_fields=repr(col_fields), + value_fields=repr(value_fields), + aggregation_function=repr(aggregation_function), + target_cell=repr(target_cell)) + + LIBREOFFICE_CALC_MERGE_CELLS_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +ret = "" + +def libreoffice_calc_merge_cells(sheet_name, range_str): + global ret + try: + # 通过名称获取指定的工作表 + sheet = doc.getSheets().getByName(sheet_name) + + # 获取单元格范围 + cell_range = sheet.getCellRangeByName(range_str) + + # 检查单元格是否已经合并 + is_merged = cell_range.getIsMerged() + + # 如果单元格范围尚未合并,则进行合并 + if not is_merged: + cell_range.merge(True) + + ret = "Success" + return True + except Exception as e: + ret = f"Error: {{e}}" + return False + +# 调用函数时需要传入 sheet_name 和 range_str 两个参数 +libreoffice_calc_merge_cells(sheet_name={sheet_name}, range_str={range_str}) +print(ret) +""" + + @agent_action + def libreoffice_calc_merge_cells(self, sheet_name, range_str): + """ + Merges a specified range of cells within a specific worksheet. + + Args: + sheet_name (str): The name of the worksheet, e.g., 'Sheet1'. + range_str (str): The cell range to merge, e.g., 'A1:B10'. + + Returns: + str: A formatted command string to be executed. + """ + return self.LIBREOFFICE_CALC_MERGE_CELLS_CMD.format( + sheet_name=repr(sheet_name), + range_str=repr(range_str) + ) + + LIBREOFFICE_CALC_SET_CELL_VALUE_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_set_cell_value(cell, value): + global ret + try: + # 获取单元格对象 + cell_obj = sheet.getCellRangeByName(cell) + + if isinstance(value, str) and value.startswith("="): + # 设置公式 + cell_obj.Formula = value + ret = "Success" + return True + + # 尝试将值转换为数字 + try: + # 尝试转换为整数 + int_value = int(value) + cell_obj.Value = int_value + except ValueError: + try: + # 尝试转换为浮点数 + float_value = float(value) + cell_obj.Value = float_value + except ValueError: + # 如果不是数字,则设置为字符串 + cell_obj.String = value + + ret = "Success" + return True + except Exception as e: + ret = f"Error: {{e}}" + return False +libreoffice_calc_set_cell_value(cell={cell}, value={value}) +print(ret) +""" + + @agent_action + def libreoffice_calc_set_cell_value(self, cell, value): + + return self.LIBREOFFICE_CALC_SET_CELL_VALUE_CMD.format(cell=repr(cell), value=repr(value)) + + LIBREOFFICE_CALC_FORMAT_RANGE_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_format_range(range_str, background_color=None, font_color=None, bold=None, alignment=None): + global ret + try: + # 获取指定范围 + cell_range = sheet.getCellRangeByName(range_str) + + # 设置背景颜色 + if background_color: + # 将十六进制颜色转换为整数 + bg_color_int = int(background_color.replace("#", ""), 16) + cell_range.CellBackColor = bg_color_int + + # 设置字体颜色 + if font_color: + # 将十六进制颜色转换为整数 + font_color_int = int(font_color.replace("#", ""), 16) + cell_range.CharColor = font_color_int + + # 设置粗体 + if bold is not None: + cell_range.CharWeight = 150.0 if bold else 100.0 # 150.0 是粗体,100.0 是正常 + + # 设置对齐方式 + if alignment: + # 设置水平对齐方式 + struct = cell_range.getPropertyValue("HoriJustify") + if alignment == "left": + struct.value = "LEFT" + elif alignment == "center": + struct.value = "CENTER" + elif alignment == "right": + struct.value = "RIGHT" + cell_range.setPropertyValue("HoriJustify", struct) + + ret = "Success" + return True + except Exception as e: + ret = f"Error: {{e}}" + return False +libreoffice_calc_format_range(range_str={range_str}, background_color={background_color}, font_color={font_color}, bold={bold}, alignment={alignment}) +print(ret) +""" + + @agent_action + def libreoffice_calc_format_range(self, range_str, background_color, font_color, bold, alignment): + + return self.LIBREOFFICE_CALC_FORMAT_RANGE_CMD.format(range_str=repr(range_str), + background_color=repr(background_color), + font_color=repr(font_color), bold=repr(bold), + alignment=repr(alignment)) + + LIBREOFFICE_CALC_INSERT_CHART_CMD = """import json +import os +import subprocess +import sys +import uno +import time +from com.sun.star.beans import PropertyValue +from com.sun.star.awt import Rectangle +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_insert_chart( + chart_type="column", + data_ranges=None, + first_row_as_label=True, + first_column_as_label=True, + data_series_in_rows=False, + title=None, + subtitle=None, + x_axis_title=None, + y_axis_title=None, + display_x_grid=False, + display_y_grid=True, + chart_name=None, + chart_position=None, + chart_size=None +): + global ret + + try: + # 生成唯一的图表名称 + if chart_name is None: + import time + timestamp = str(int(time.time() * 1000)) # 使用毫秒时间戳 + chart_name = f"Chart_{{timestamp}}" + + # 图表类型映射 + chart_type_map = {{ + "column": "com.sun.star.chart.ColumnDiagram", + "line": "com.sun.star.chart.LineDiagram" + }} + + if chart_type not in chart_type_map: + ret = f"Error: Unsupported chart type '{{chart_type}}'. Supported: column, line" + return False + + # 处理数据范围 + if isinstance(data_ranges, str): + if "," in data_ranges: + range_list = [r.strip() for r in data_ranges.split(",")] + else: + range_list = [data_ranges] + elif isinstance(data_ranges, list): + range_list = data_ranges + else: + ret = "Error: data_ranges must be string or list" + return False + + # 转换数据范围为CellRangeAddress对象 + cell_ranges = [] + for range_str in range_list: + try: + cell_range = sheet.getCellRangeByName(range_str.replace("$", "")) + cell_ranges.append(cell_range.getRangeAddress()) + except Exception as e: + ret = f"Error processing range '{{range_str}}': {{e}}" + return False + + # 设置图表位置和大小 + if chart_position and chart_size: + rect = Rectangle( + chart_position.get("x", 1000), + chart_position.get("y", 1000), + chart_size.get("width", 10000), + chart_size.get("height", 7000) + ) + elif chart_position: + rect = Rectangle( + chart_position.get("x", 1000), + chart_position.get("y", 1000), + 10000, 7000 + ) + else: + rect = Rectangle(1000, 1000, 10000, 7000) + + # 创建图表 + charts = sheet.Charts + charts.addNewByName(chart_name, rect, tuple(cell_ranges), first_row_as_label, first_column_as_label) + + # 获取图表对象 + chart_obj = charts.getByName(chart_name) + chart_doc = chart_obj.EmbeddedObject + + # 创建并设置图表类型 + diagram = chart_doc.createInstance(chart_type_map[chart_type]) + chart_doc.setDiagram(diagram) + + # 设置数据系列排列方向 + try: + if hasattr(chart_doc, 'setDataSourceLabelsInFirstRow'): + chart_doc.setDataSourceLabelsInFirstRow(not data_series_in_rows) + if hasattr(chart_doc, 'setDataSourceLabelsInFirstColumn'): + chart_doc.setDataSourceLabelsInFirstColumn(data_series_in_rows) + except Exception as e: + print(f"Warning: Failed to set data series orientation: {{e}}") + + # 设置图表标题 + if title: + try: + title_obj = chart_doc.getTitle() + if not title_obj: + title_obj = chart_doc.createInstance("com.sun.star.chart.Title") + chart_doc.setTitleObject(title_obj) + title_obj.String = title + except Exception as e: + print(f"Warning: Failed to set title: {{e}}") + + # 设置副标题 + if subtitle: + try: + subtitle_obj = chart_doc.getSubTitle() + if not subtitle_obj: + subtitle_obj = chart_doc.createInstance("com.sun.star.chart.Title") + chart_doc.setSubTitleObject(subtitle_obj) + subtitle_obj.String = subtitle + except Exception as e: + print(f"Warning: Failed to set subtitle: {{e}}") + + # 设置坐标轴标题 + if x_axis_title or y_axis_title: + try: + if hasattr(diagram, 'XAxisTitle') and x_axis_title: + diagram.HasXAxisTitle = True + x_title = diagram.XAxisTitle + x_title.String = x_axis_title + + if hasattr(diagram, 'YAxisTitle') and y_axis_title: + diagram.HasYAxisTitle = True + y_title = diagram.YAxisTitle + y_title.String = y_axis_title + except Exception as e: + print(f"Warning: Failed to set axis titles: {{e}}") + + # 设置网格线 + try: + if hasattr(diagram, 'HasXAxisGrid'): + diagram.HasXAxisGrid = display_x_grid + if hasattr(diagram, 'HasYAxisGrid'): + diagram.HasYAxisGrid = display_y_grid + except Exception as e: + print(f"Warning: Failed to set grid lines: {{e}}") + + ret = "Chart created successfully" + return True + + except Exception as e: + ret = f"Error creating chart: {{e}}" + return False +libreoffice_calc_insert_chart(chart_type={chart_type}, data_ranges={data_ranges}, first_row_as_label={first_row_as_label}, first_column_as_label={first_column_as_label}, data_series_in_rows={data_series_in_rows}, title={title}, subtitle={subtitle}, x_axis_title={x_axis_title}, y_axis_title={y_axis_title}, display_x_grid={display_x_grid}, display_y_grid={display_y_grid}, chart_name={chart_name}, chart_position={chart_position}, chart_size={chart_size}) +print(ret)""" + + @agent_action + def libreoffice_calc_insert_chart(self, chart_type="column", data_ranges=None, first_row_as_label=True, + first_column_as_label=True, data_series_in_rows=False, title=None, + subtitle=None, x_axis_title=None, y_axis_title=None, display_x_grid=False, + display_y_grid=True, chart_name=None, chart_position=None, chart_size=None): + return self.LIBREOFFICE_CALC_INSERT_CHART_CMD.format(chart_type=repr(chart_type), data_ranges=repr(data_ranges), + first_row_as_label=repr(first_row_as_label), + first_column_as_label=repr(first_column_as_label), + data_series_in_rows=repr(data_series_in_rows), + title=repr(title), subtitle=repr(subtitle), + x_axis_title=repr(x_axis_title), + y_axis_title=repr(y_axis_title), + display_x_grid=repr(display_x_grid), + display_y_grid=repr(display_y_grid), + chart_name=repr(chart_name), + chart_position=repr(chart_position), + chart_size=repr(chart_size)) + + LIBREOFFICE_CALC_FREEZE_PANES_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_freeze_panes(rows=0, columns=0): + global ret + try: + # 获取当前视图 + view = doc.getCurrentController() + + # 设置冻结窗格 + view.freezeAtPosition(columns, rows) + + ret = "Success" + return True + except Exception as e: + ret = f"Error: {{e}}" + return False +libreoffice_calc_freeze_panes(rows={rows}, columns={columns}) +print(ret) +""" + + @agent_action + def libreoffice_calc_freeze_panes(self, rows, columns): + + return self.LIBREOFFICE_CALC_FREEZE_PANES_CMD.format(rows=repr(rows), columns=repr(columns)) + + LIBREOFFICE_CALC_RENAME_SHEET_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_rename_sheet(old_name, new_name): + global ret + try: + # 获取所有工作表 + sheets = doc.getSheets() + + # 检查原工作表是否存在 + if not sheets.hasByName(old_name): + return False + + # 检查新名称是否已存在 + if sheets.hasByName(new_name): + return False + + # 获取要重命名的工作表 + sheet = sheets.getByName(old_name) + + # 重命名工作表 + sheet.setName(new_name) + + ret = "Success" + return True + + except Exception as e: + ret = f"Error: {{e}}" + return False +libreoffice_calc_rename_sheet(old_name={old_name}, new_name={new_name}) +print(ret) +""" + + @agent_action + def libreoffice_calc_rename_sheet(self, old_name, new_name): + + return self.LIBREOFFICE_CALC_RENAME_SHEET_CMD.format(old_name=repr(old_name), new_name=repr(new_name)) + + LIBREOFFICE_CALC_COPY_SHEET_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_copy_sheet(source_sheet, new_sheet_name=None): + global ret + try: + # 获取所有工作表 + sheets = doc.getSheets() + + # 检查源工作表是否存在 + if not sheets.hasByName(source_sheet): + return None + + # 如果没有提供新名称,则生成一个 + if not new_sheet_name: + # 生成类似 "Sheet1 (2)" 的名称 + base_name = source_sheet + counter = 1 + new_sheet_name = f"{{base_name}} ({{counter}})" + + # 确保名称不重复 + while sheets.hasByName(new_sheet_name): + counter += 1 + new_sheet_name = f"{{base_name}} ({{counter}})" + + # 检查新名称是否已存在 + if sheets.hasByName(new_sheet_name): + return None # 名称已存在,无法创建 + + # 获取源工作表的索引 + source_index = -1 + for i in range(sheets.getCount()): + if sheets.getByIndex(i).getName() == source_sheet: + source_index = i + break + + if source_index == -1: + return None + + # 复制工作表 + sheets.copyByName(source_sheet, new_sheet_name, source_index + 1) + + ret = f"New sheet created: {{new_sheet_name}}" + return new_sheet_name + + except Exception as e: + ret = f"Error: {{e}}" + return None +libreoffice_calc_copy_sheet(source_sheet={source_sheet}, new_sheet_name={new_sheet_name}) +print(ret) +""" + + @agent_action + def libreoffice_calc_copy_sheet(self, source_sheet, new_sheet_name): + + return self.LIBREOFFICE_CALC_COPY_SHEET_CMD.format(source_sheet=repr(source_sheet), + new_sheet_name=repr(new_sheet_name)) + + LIBREOFFICE_CALC_REORDER_SHEETS_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_reorder_sheets(sheet_name, position): + global ret + try: + # 获取所有工作表 + sheets = doc.getSheets() + + # 检查工作表是否存在 + if not sheets.hasByName(sheet_name): + return False + + # 获取工作表总数 + sheet_count = sheets.getCount() + + # 检查位置是否有效 + if position < 0 or position >= sheet_count: + return False + + # 获取要移动的工作表 + sheet = sheets.getByName(sheet_name) + + # 获取工作表当前索引 + current_index = -1 + for i in range(sheet_count): + if sheets.getByIndex(i).Name == sheet_name: + current_index = i + break + + if current_index == -1: + return False + + # 移动工作表到指定位置 + sheets.moveByName(sheet_name, position) + + ret = "Success" + return True + + except Exception as e: + ret = f"Error: {{e}}" + return False +libreoffice_calc_reorder_sheets(sheet_name={sheet_name}, position={position}) +print(ret) +""" + + @agent_action + def libreoffice_calc_reorder_sheets(self, sheet_name, position): + + return self.LIBREOFFICE_CALC_REORDER_SHEETS_CMD.format(sheet_name=repr(sheet_name), position=repr(position)) + + LIBREOFFICE_CALC_SET_CHART_LEGEND_POSITION_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_set_chart_legend_position(position): + global ret + try: + # 获取当前工作表中的所有图表 + charts = sheet.getCharts() + if charts.getCount() == 0: + return False + + # 获取第一个图表(假设我们要修改的是第一个图表) + chart = charts.getByIndex(0) + chart_obj = chart.getEmbeddedObject() + + # 获取图表的图例 + diagram = chart_obj.getDiagram() + legend = chart_obj.getLegend() + + # 根据指定的位置设置图例位置 + if position == "none": + # 如果选择"none",则隐藏图例 + chart_obj.HasLegend = False + else: + # 确保图例可见 + chart_obj.HasLegend = True + + import inspect + + print(inspect.getmembers(legend)) + + # 设置图例位置 + if position == "top": + pos = uno.Enum("com.sun.star.chart.ChartLegendPosition", "TOP") + elif position == "bottom": + pos = uno.Enum("com.sun.star.chart.ChartLegendPosition", "BOTTOM") + elif position == "left": + pos = uno.Enum("com.sun.star.chart.ChartLegendPosition", "LEFT") + elif position == "right": + pos = uno.Enum("com.sun.star.chart.ChartLegendPosition", "RIGHT") + + legend.Alignment = pos + + ret = "Success" + return True + except Exception: + ret = "Error" + return False +libreoffice_calc_set_chart_legend_position(position={position}) +print(ret) +""" + + @agent_action + def libreoffice_calc_set_chart_legend_position(self, position): + + return self.LIBREOFFICE_CALC_SET_CHART_LEGEND_POSITION_CMD.format(position=repr(position)) + + LIBREOFFICE_CALC_SET_NUMBER_FORMAT_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_set_number_format(range_str, format_type, decimal_places=None): + global ret + try: + # 获取单元格范围 + cell_range = sheet.getCellRangeByName(range_str) + + # 获取数字格式化服务 + number_formats = doc.NumberFormats + locale = doc.CharLocale + + # 根据格式类型设置格式字符串 + format_string = "" + + if format_type == "general": + format_string = "General" + elif format_type == "number": + if decimal_places is not None: + format_string = f"0{{('.' + '0' * decimal_places) if decimal_places > 0 else ''}}" + else: + format_string = "0" + elif format_type == "currency": + if decimal_places is not None: + format_string = f"[$¥-804]#,##0{{('.' + '0' * decimal_places) if decimal_places > 0 else ''}}" + else: + format_string = "[$¥-804]#,##0.00" + elif format_type == "accounting": + if decimal_places is not None: + format_string = f"_-[$¥-804]* #,##0{{('.' + '0' * decimal_places) if decimal_places > 0 else ''}}_-;-[$¥-804]* #,##0{{('.' + '0' * decimal_places) if decimal_places > 0 else ''}}_-;_-[$¥-804]* \"-\"_-;_-@_-" + else: + format_string = '_-[$¥-804]* #,##0.00_-;-[$¥-804]* #,##0.00_-;_-[$¥-804]* "-"??_-;_-@_-' + elif format_type == "date": + format_string = "YYYY/MM/DD" + elif format_type == "time": + format_string = "HH:MM:SS" + elif format_type == "percentage": + if decimal_places is not None: + format_string = f"0{{('.' + '0' * decimal_places) if decimal_places > 0 else ''}}%" + else: + format_string = "0.00%" + elif format_type == "fraction": + format_string = "# ?/?" + elif format_type == "scientific": + if decimal_places is not None: + format_string = f"0{{('.' + '0' * decimal_places) if decimal_places > 0 else ''}}E+00" + else: + format_string = "0.00E+00" + elif format_type == "text": + format_string = "@" + + # 获取格式键 + format_key = number_formats.queryKey(format_string, locale, True) + + # 如果格式不存在,则添加 + if format_key == -1: + format_key = number_formats.addNew(format_string, locale) + + # 应用格式 + cell_range.NumberFormat = format_key + + ret = "Success" + return True + except Exception as e: + ret = f"Error: {{e}}" + return False +libreoffice_calc_set_number_format(range_str={range_str}, format_type={format_type}, decimal_places={decimal_places}) +print(ret) +""" + + @agent_action + def libreoffice_calc_set_number_format(self, range_str, format_type, decimal_places): + + return self.LIBREOFFICE_CALC_SET_NUMBER_FORMAT_CMD.format(range_str=repr(range_str), + format_type=repr(format_type), + decimal_places=repr(decimal_places)) + + LIBREOFFICE_CALC_ADJUST_COLUMN_WIDTH_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" +def libreoffice_calc_column_name_to_index(column_name): + + column_name = column_name.upper() + result = 0 + for char in column_name: + result = result * 26 + (ord(char) - ord("A") + 1) + return result - 1 +def libreoffice_calc_adjust_column_width(columns, width=None, autofit=False): + global ret + try: + # 解析列范围 + col_range = columns.split(":") + start_col = libreoffice_calc_column_name_to_index(col_range[0]) + + if len(col_range) > 1: + end_col = libreoffice_calc_column_name_to_index(col_range[1]) + else: + end_col = start_col + + # 获取列对象 + columns_obj = sheet.getColumns() + + # 遍历指定的列范围 + for col_idx in range(start_col, end_col + 1): + column = columns_obj.getByIndex(col_idx) + + if autofit: + # 自动调整列宽 + column.OptimalWidth = True + elif width is not None: + # 设置指定宽度(转换为1/100毫米) + # 大约一个字符宽度为256 (1/100 mm) + column.Width = int(width * 256) + + ret = "Success" + return True + + except Exception as e: + ret = f"Error: {{e}}" + return False +libreoffice_calc_adjust_column_width(columns={columns}, width={width}, autofit={autofit}) +print(ret) +""" + + @agent_action + def libreoffice_calc_adjust_column_width(self, columns, width, autofit): + + return self.LIBREOFFICE_CALC_ADJUST_COLUMN_WIDTH_CMD.format(columns=repr(columns), width=repr(width), + autofit=repr(autofit)) + + LIBREOFFICE_CALC_ADJUST_ROW_HEIGHT_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_adjust_row_height(rows, height=None, autofit=False): + global ret + try: + # 解析行范围 + row_range = rows.split(":") + start_row = int(row_range[0]) + end_row = int(row_range[1]) if len(row_range) > 1 else start_row + + # 获取行对象 + for row_index in range(start_row, end_row + 1): + row = sheet.getRows().getByIndex(row_index - 1) # 索引从0开始 + + if autofit: + # 自动调整行高以适应内容 + row.OptimalHeight = True + elif height is not None: + # 设置指定高度(将点转换为1/100毫米,LibreOffice使用的单位) + # 1点 ≈ 35.28 1/100毫米 + row.Height = int(height * 35.28) + row.OptimalHeight = False + + ret = "Success" + return True + except Exception as e: + ret = f"Error: {{e}}" + return False +libreoffice_calc_adjust_row_height(rows={rows}, height={height}, autofit={autofit}) +print(ret) +""" + + @agent_action + def libreoffice_calc_adjust_row_height(self, rows, height, autofit): + + return self.LIBREOFFICE_CALC_ADJUST_ROW_HEIGHT_CMD.format(rows=repr(rows), height=repr(height), + autofit=repr(autofit)) + + LIBREOFFICE_CALC_EXPORT_TO_PDF_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_export_to_pdf(file_path=None, sheets=None, open_after_export=False): + global ret + try: + # 如果未指定文件路径,则使用当前文档路径并更改扩展名为.pdf + if not file_path: + if doc.hasLocation(): + url = doc.getLocation() + file_path = uno.fileUrlToSystemPath(url) + file_path = os.path.splitext(file_path)[0] + ".pdf" + else: + # 如果文档尚未保存,则在用户桌面创建临时文件 + desktop_path = os.path.join(os.path.expanduser("~"), "Desktop") + file_path = os.path.join(desktop_path, "LibreOffice_Export.pdf") + + # 确保文件路径是系统路径,然后转换为URL + pdf_url = uno.systemPathToFileUrl(os.path.abspath(file_path)) + + # 创建导出属性 + export_props = [] + + # 设置过滤器名称 + export_props.append(PropertyValue(Name="FilterName", Value="calc_pdf_Export")) + + # 如果指定了特定工作表,则只导出这些工作表 + if sheets and isinstance(sheets, list) and len(sheets) > 0: + # 获取所有工作表 + all_sheets = doc.getSheets() + selection = [] + + # 查找指定的工作表 + for sheet_name in sheets: + if all_sheets.hasByName(sheet_name): + sheet = all_sheets.getByName(sheet_name) + selection.append(sheet) + + # 如果找到了指定的工作表,则设置导出选择 + if selection: + export_props.append(PropertyValue(Name="Selection", Value=tuple(selection))) + + # 导出PDF + doc.storeToURL(pdf_url, tuple(export_props)) + + # 如果需要,导出后打开PDF + if open_after_export: + if sys.platform.startswith("darwin"): # macOS + subprocess.call(("open", file_path)) + elif os.name == "nt": # Windows + os.startfile(file_path) + elif os.name == "posix": # Linux + subprocess.call(("xdg-open", file_path)) + + ret = "Success" + return True + + except Exception as e: + ret = f"Error: {{e}}" + return False +libreoffice_calc_export_to_pdf(file_path={file_path}, sheets={sheets}, open_after_export={open_after_export}) +print(ret) +""" + + @agent_action + def libreoffice_calc_export_to_pdf(self, file_path, sheets, open_after_export): + + return self.LIBREOFFICE_CALC_EXPORT_TO_PDF_CMD.format(file_path=repr(file_path), sheets=repr(sheets), + open_after_export=repr(open_after_export)) + + LIBREOFFICE_CALC_SET_ZOOM_LEVEL_CMD = """import json +import os +import subprocess +import sys +import uno +from com.sun.star.beans import PropertyValue +localContext = uno.getComponentContext() +resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) +ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") +desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx) +doc = desktop.getCurrentComponent() +sheet = doc.CurrentController.ActiveSheet +ret = "" + +def libreoffice_calc_set_zoom_level(zoom_percentage): + global ret + try: + # 获取当前控制器 + controller = doc.getCurrentController() + + # 设置缩放值 + # 确保缩放值在合理范围内 + if zoom_percentage < 10: + zoom_percentage = 10 + elif zoom_percentage > 400: + zoom_percentage = 400 + + # 应用缩放值 + controller.ZoomValue = zoom_percentage + ret = "Success" + return True + + except Exception as e: + ret = f"Error: {{e}}" + return False + +libreoffice_calc_set_zoom_level(zoom_percentage={zoom_percentage}) +print(ret) +""" + + @agent_action + def libreoffice_calc_set_zoom_level(self, zoom_percentage): + + return self.LIBREOFFICE_CALC_SET_ZOOM_LEVEL_CMD.format(zoom_percentage=repr(zoom_percentage)) + + +# ACI that supports the worker-only mode: done() and fail() become task scoped instead +class OSWorldWorkerOnlyACI(OSWorldACI): + @agent_action + def done( + self, + ): + """End the current task with a success. Use this when you believe the entire task has been fully completed.""" + return """DONE""" + + @agent_action + def fail(self): + """End the current task with a failure. Use this when you believe the entire task is impossible to complete.""" + return """FAIL""" diff --git a/mm_agents/aworldguiagent/prompt.py b/mm_agents/aworldguiagent/prompt.py new file mode 100644 index 0000000..5f8add3 --- /dev/null +++ b/mm_agents/aworldguiagent/prompt.py @@ -0,0 +1,947 @@ +""" +This code is adapted from AgentS2 (https://github.com/simular-ai/Agent-S) +with modifications to suit specific requirements. +""" +GENERATOR_SYS_PROMPT = """You are an expert in graphical user interfaces and Python code. You are responsible for executing the task: `TASK_DESCRIPTION`. +You are working in Ubuntu. +You are provided with: +1. A screenshot of the current time step. +2. The history of your previous interactions with the UI. +3. Access to the following class and methods to interact with the UI: +class Agent: + + def click(self, element_description: str, num_clicks: int = 1, button_type: str = 'left', hold_keys: List = []): + '''Click on the element + Args: + element_description:str, a detailed descriptions of which element to click on. This description should be at least a full sentence. + num_clicks:int, number of times to click the element + button_type:str, which mouse button to press can be "left", "middle", or "right" + hold_keys:List, list of keys to hold while clicking + ''' + + def done(self, return_value: Union[Dict, str, List, Tuple, int, float, bool, NoneType] = None): + '''End the current task with a success and the required return value''' + + def drag_and_drop(self, starting_description: str, ending_description: str, hold_keys: List = []): + '''Drag from the starting description to the ending description + Args: + starting_description:str, a very detailed description of where to start the drag action. This description should be at least a full sentence. + ending_description:str, a very detailed description of where to end the drag action. This description should be at least a full sentence. + hold_keys:List list of keys to hold while dragging + ''' + + def fail(self): + '''End the current task with a failure, and replan the whole task.''' + + def hold_and_press(self, hold_keys: List, press_keys: List): + '''Hold a list of keys and press a list of keys + Args: + hold_keys:List, list of keys to hold + press_keys:List, list of keys to press in a sequence + ''' + + def hotkey(self, keys: List): + '''Press a hotkey combination + Args: + keys:List the keys to press in combination in a list format (e.g. ['ctrl', 'c']) + ''' + + def open(self, app_or_filename: str): + '''Open any application or file with name app_or_filename. Use this action to open applications or files on the desktop, do not open manually. + Args: + app_or_filename:str, the name of the application or filename to open + ''' + + def save_to_knowledge(self, text: List[str]): + '''Save facts, elements, texts, etc. to a long-term knowledge bank for reuse during this task. Can be used for copy-pasting text, saving elements, etc. + Args: + text:List[str] the text to save to the knowledge + ''' + + def scroll(self, element_description: str, clicks: int, shift: bool = False): + '''Scroll the element in the specified direction + Args: + element_description:str, a very detailed description of which element to enter scroll in. This description should be at least a full sentence. + clicks:int, the number of clicks to scroll can be positive (up) or negative (down). + shift:bool, whether to use shift+scroll for horizontal scrolling + ''' + + def set_cell_values(self, cell_values: Dict[str, Any], app_name: str, sheet_name: str): + '''Use this to set individual cell values in a spreadsheet. For example, setting A2 to "hello" would be done by passing {"A2": "hello"} as cell_values. The sheet must be opened before this command can be used. + Args: + cell_values: Dict[str, Any], A dictionary of cell values to set in the spreadsheet. The keys are the cell coordinates in the format "A1", "B2", etc. + Supported value types include: float, int, string, bool, formulas. + app_name: str, The name of the spreadsheet application. For example, "Some_sheet.xlsx". + sheet_name: str, The name of the sheet in the spreadsheet. For example, "Sheet1". + ''' + + def switch_applications(self, app_code): + '''Switch to a different application that is already open + Args: + app_code:str the code name of the application to switch to from the provided list of open applications + ''' + + def type(self, element_description: str, text: str = '', overwrite: bool = False, enter: bool = False): + '''Type text into a specific element + Args: + element_description:str, a detailed description of which element to enter text in. This description should be at least a full sentence. + text:str, the text to type + overwrite:bool, Assign it to True if the text should overwrite the existing text, otherwise assign it to False. Using this argument clears all text in an element. + enter:bool, Assign it to True if the enter key should be pressed after typing the text, otherwise assign it to False. + ''' + + def wait(self, time: float): + '''Wait for a specified amount of time + Args: + time:float the amount of time to wait in seconds + ''' + + def code_launch_vscode(self, path): + '''Launches Visual Studio Code with the specified file path or directory. +在存在的窗口中打开一个文件或目录。 + +Args: + path (str): 文件路径或目录。''' + +def code_compare_files(self, file1, file2): + '''Compares two files in VSCode. +在VSCode中比较两个文件。 + +Args: + file1 (str): 第一个文件的路径。 + file2 (str): 第二个文件的路径。''' + +def code_add_folder(self, folder): + '''Adds a folder to the last active window in VSCode. +向VSCode的最后一个活动窗口添加文件夹。 + +Args: + folder (str): 文件夹路径。''' + +def code_goto_file(self, file_path, line=1, character=1): + '''Opens a file at a specific line and character position. +在特定行和字符的位置打开文件。 + +Args: + file_path (str): 文件路径。 + line (int): 行号。 + character (int): 字符位置。''' + +def code_perform_merge(self, path1, path2, base, result): + '''Perform a three-way merge. +执行三方合并。 + +Args: + path1 (str): 第一版本文件路径。 + path2 (str): 第二版本文件路径。 + base (str): 基础版本文件路径。 + result (str): 结果文件的保存路径。''' + +def code_remove_folder(self, folder): + '''Removes a folder from the last active window in VSCode. +在VSCode的最后一个活动窗口中移除文件夹。 + +Args: + folder (str): 文件夹路径。''' + +def code_install_extension(self, extension_id, pre_release=False): + '''Installs an extension or updates it in VSCode. +安装或更新VSCode中的扩展。 + +Args: + extension_id (str): 扩展的标识符。 + pre_release (bool): 是否安装预发布版本。''' + +def code_uninstall_extension(self, extension_id): + '''Uninstalls an extension from VSCode. +从VSCode中卸载扩展。 + +Args: + extension_id (str): 扩展的标识符。''' + +def code_list_extensions(self, show_versions=False, category=None): + '''Lists installed extensions in VSCode. +列出VSCode中安装的扩展。 + +Args: + show_versions (bool): 是否显示扩展的版本。 + category (str): 按类别筛选扩展。''' + +def code_update_extensions(self): + '''Updates all installed extensions in VSCode to the latest version. +更新VSCode中所有安装的扩展到最新版本。''' + +def code_disable_extension(self, extension_id): + '''Disables a specific extension for the next instance of VSCode. +禁用在下一个VSCode窗口中的指定扩展。 + +Args: + extension_id (str): 扩展的标识符。''' + +def code_toggle_sync(self, state): + '''Toggles synchronization on or off in VSCode. +在VSCode中开启或关闭同步。 + +Args: + state (str): 'on' 或 'off' 表示开启或关闭。''' + + +def libreoffice_calc_save(self): + '''Save the current workbook to its current location + +Returns: + bool: True if save successful, False otherwise''' + +def libreoffice_calc_get_workbook_info(self): + '''Get workbook information + +Args: + None + +Returns: + dict: Workbook information, including file path, file name, sheets and active sheet''' + +def libreoffice_calc_get_column_data(self, column_name): + '''Get data from the specified column + +Args: + column_name (str): Name of the column to read + +Returns: + list: List of values in the specified column''' + +def libreoffice_calc_set_column_as_text(self, column_name): + +''' +Set the specified column format as text type. +This will convert all numeric values in the column to text format and apply text formatting. + +Args: + column_name (str): The column name to format as text (e.g., 'A', 'B', 'C') + +Returns: + str: Success message or error description + +Example: + "Successfully set column A as text format" +''' + +def libreoffice_calc_get_active_sheet_data(self): + +''' +Get all data from the currently active sheet with detailed coordinate information. +Returns data with cell addresses, values, row/column info, and empty cell indicators. + +Returns: + dict: Complete sheet data with detailed cell information + +Example: + { + "data": [ + [ + {"address": "A1", "value": "", "row": 1, "col": 1, "col_name": "A", "is_empty": true}, + {"address": "B1", "value": "Age", "row": 1, "col": 2, "col_name": "B", "is_empty": false} + ], + [ + {"address": "A2", "value": "Ryan", "row": 2, "col": 1, "col_name": "A", "is_empty": false}, + {"address": "B2", "value": 5.0, "row": 2, "col": 2, "col_name": "B", "is_empty": false} + ], + [ + {"address": "A3", "value": "Jack", "row": 3, "col": 1, "col_name": "A", "is_empty": false}, + {"address": "B3", "value": 6.0, "row": 3, "col": 2, "col_name": "B", "is_empty": false} + ] + ], + "rows": 3, + "columns": 2, + "range": "A1:B3" + } +''' + +def libreoffice_calc_switch_active_sheet(self, sheet_name): + '''Switch to the specified sheet and make it active, create if not exist + +Args: + sheet_name (str): Name of the sheet to switch to or create + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_calc_set_column_values(self, column_name, data, start_index=2): + '''Set data to the specified column + +Args: + column_name (str): Name of the column to write + data (list): List of values to write to the column + start_index (int): The index of the first row to write to, default is 2 (skip the first row) + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_calc_highlight_range(self, range_str, color=0xFF0000): + '''highlight the specified range with the specified color + +Args: + range_str (str): Range to highlight, in the format of "A1:B10" + color (str): Color to highlight with, default is '0xFF0000' (red) + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_calc_transpose_range(self, source_range, target_cell): + '''Transpose the specified range and paste it to the target cell + +Args: + source_range (str): Range to transpose, in the format of "A1:B10" + target_cell (str): Target cell to paste the transposed data, in the format of "A1" + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_calc_export_to_csv(self): + '''Export the current document to a CSV file + +Args: + None + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_calc_sort_column(self, column_name, ascending=True, start_index=2): + '''Sorts the data in the specified column in ascending or descending order + +Args: + column_name (str): The name of the column to sort (e.g. 'A') or the title + ascending (bool): Whether to sort in ascending order (default True) + start_index (int): The index of the first row to sort, default is 1 + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_calc_set_validation_list(self, column_name, values): + '''Set a validation list for the specified column + +Args: + column_name (str): The name of the column to set the validation list for + values (list): The list of values to use for the validation list + +Returns: + None''' + +def libreoffice_calc_hide_row_data(self, value="N/A"): + '''Hide rows that contain the specified value + +Args: + value (str): The value to hide rows for, default is 'N/A' + +Returns: + None''' + +def libreoffice_calc_reorder_columns(self, column_order): + '''Reorder the columns in the sheet according to the specified order + +Args: + column_order (list): A list of column names in the desired order + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_calc_create_pivot_table(self, + source_sheet, + table_name, + row_fields=None, + col_fields=None, + value_fields=None, + aggregation_function="sum", + target_cell="A1", + ): + '''Create a pivot table in the active worksheet based on data from the active sheet.''' + +def libreoffice_calc_merge_cells(sheet_name, range_str): + '''Merges a specified range of cells within a specific worksheet. + + This function connects to a running LibreOffice Calc instance, + selects a worksheet by its name, and merges the cells defined + by the given range string. + + Args: + sheet_name (str): The name of the worksheet where the cells will be + merged, e.g., 'Sheet1' or 'Q4_Report'. + range_str (str): The cell range to merge, specified in A1 notation, + e.g., 'A1:B10'. + + Returns: + bool: True if the cells were successfully merged, False if an + error occurred. + ''' + +def libreoffice_calc_set_cell_value(self, cell, value): + '''Set a value to a specific cell in the active worksheet. + +Args: + cell (str): Cell reference (e.g., 'A1') + value (str): Value to set in the cell + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_calc_format_range(self, range_str, background_color=None, font_color=None, bold=None, alignment=None): + '''Apply formatting to the specified range in the active worksheet + +Args: + range_str (str): Range to format, in the format of 'A1:B10' + background_color (str, optional): Background color in hex format (e.g., '#0000ff') + font_color (str, optional): Font color in hex format (e.g., '#ffffff') + bold (bool, optional): Whether to make the text bold + italic (bool, optional): Whether to make the text italic + alignment (str, optional): Text alignment (left, center, right) + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_calc_freeze_panes(self, rows=0, columns=0): + '''冻结活动工作表中的行和/或列 + +Args: + rows (int): 从顶部开始冻结的行数 + columns (int): 从左侧开始冻结的列数 + +Returns: + bool: 成功返回True,失败返回False''' + +def libreoffice_calc_rename_sheet(self, old_name, new_name): + '''重命名工作表 + +Args: + old_name (str): 要重命名的工作表的当前名称 + new_name (str): 工作表的新名称 + +Returns: + bool: 成功返回True,失败返回False''' + +def libreoffice_calc_copy_sheet(self, source_sheet, new_sheet_name=None): + '''创建工作簿中现有工作表的副本 + +Args: + source_sheet (str): 要复制的工作表名称 + new_sheet_name (str, optional): 新工作表副本的名称,如果不提供则自动生成 + +Returns: + str: 新创建的工作表名称,如果失败则返回None''' + +def libreoffice_calc_reorder_sheets(self, sheet_name, position): + '''重新排序工作表在工作簿中的位置 + +Args: + sheet_name (str): 要移动的工作表名称 + position (int): 要移动到的位置(基于0的索引) + +Returns: + bool: 成功返回True,失败返回False''' + +def libreoffice_calc_set_chart_legend_position(self, position): + '''Set the position of the legend in a chart in the active worksheet. + +Args: + position (str): Position of the legend ('top', 'bottom', 'left', 'right', 'none') + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_calc_set_number_format(self, range_str, format_type, decimal_places=None): + '''Apply a specific number format to a range of cells in the active worksheet. + +Args: + range_str (str): Range to format, in the format of 'A1:B10' + format_type (str): Type of number format to apply + decimal_places (int, optional): Number of decimal places to display + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_calc_adjust_column_width(self, columns, width=None, autofit=False): + '''调整活动工作表中指定列的宽度 + +Args: + columns (str): 要调整的列范围,例如 'A:C' 表示从A列到C列 + width (float, optional): 要设置的宽度(以字符为单位) + autofit (bool, optional): 是否自动调整列宽以适应内容 + +Returns: + bool: 成功返回True,失败返回False''' + +def libreoffice_calc_adjust_row_height(self, rows, height=None, autofit=False): + '''调整活动工作表中指定行的高度 + +Args: + rows (str): 要调整的行范围,例如 '1:10' 表示第1行到第10行 + height (float, optional): 要设置的高度(以点为单位) + autofit (bool, optional): 是否自动调整行高以适应内容 + +Returns: + bool: 操作成功返回True,否则返回False''' + +def libreoffice_calc_export_to_pdf(self, file_path=None, sheets=None, open_after_export=False): + '''将当前文档或指定工作表导出为PDF文件 + +Args: + file_path (str, optional): PDF文件保存路径,如果不指定则使用当前文档路径 + sheets (list, optional): 要包含在PDF中的工作表名称列表,如果不指定则包含所有工作表 + open_after_export (bool, optional): 导出后是否打开PDF文件 + +Returns: + bool: 成功返回True,失败返回False''' + +def libreoffice_calc_set_zoom_level(self, zoom_percentage): + '''调整当前工作表的缩放级别,使单元格看起来更大或更小 + +Args: + zoom_percentage (int): 缩放级别的百分比(例如,75表示75%,100表示正常大小,150表示放大)。 + 有效范围通常为10-400。 + +Returns: + bool: 成功返回True,失败返回False''' + + +def libreoffice_impress_save(self): + '''保存文档到当前位置''' + +def libreoffice_impress_go_to_slide(self, slide_index): + '''Navigates to a specific slide in the presentation based on its index. + +Args: + slide_index (int): The index of the slide to navigate to (1-based indexing) + +Returns: + bool: True if navigation was successful, False otherwise''' + +def libreoffice_impress_get_slide_count(self): + '''Gets the total number of slides in the current presentation. +:return: The total number of slides as an integer''' + +def libreoffice_impress_duplicate_slide(self, slide_index): + '''Creates a duplicate of a specific slide and places it at the end of the presentation. + +:param slide_index: The index of the slide to duplicate (1-based indexing) +:return: True if successful, False otherwise''' + +def libreoffice_impress_set_slide_font(self, slide_index, font_name): + '''Sets the font style for all text elements in a specific slide, including the title. + +Args: + slide_index (int): The index of the slide to modify (1-based indexing) + font_name (str): The name of the font to apply (e.g., 'Arial', 'Times New Roman', 'Calibri') + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_impress_write_text(self, content, page_index, box_index, bold=False, italic=False, size=None, append=False): + '''Writes text to a specific textbox on a slide + +:param content: The text content to add +:param page_index: The index of the slide (1-based indexing) +:param box_index: The index of the textbox to modify (0-based indexing) +:param bold: Whether to make the text bold, default is False +:param italic: Whether to make the text italic, default is False +:param size: The size of the text. If None, uses the box's current font size. +:param append: Whether to append the text, default is False. If you want to observe some formats(like a bullet at the beginning) or keep the original text, you should set up it. +:return: True if successful, False otherwise''' + +def libreoffice_impress_set_style(self, slide_index, box_index, bold=None, italic=None, underline=None): + '''Sets the style properties for the specified textbox on a slide. + +:param slide_index: The index of the slide to modify (1-based indexing) +:param box_index: The index of the textbox to modify (0-based indexing) +:param bold: Whether to make the text bold +:param italic: Whether to make the text italic +:param underline: Whether to underline the text +:return: True if successful, False otherwise''' + +def libreoffice_impress_configure_auto_save(self, enabled, interval_minutes): + '''Enables or disables auto-save functionality for the current document and sets the auto-save interval. + +:param enabled: Whether to enable (True) or disable (False) auto-save +:param interval_minutes: The interval in minutes between auto-saves (minimum 1 minute) +:return: True if successful, False otherwise''' + +def libreoffice_impress_set_background_color(self, slide_index, box_index, color): + '''Sets the background color for the specified textbox on a slide. + +Args: + slide_index (int): The index of the slide containing the textbox (1-based indexing) + box_index (int): The index of the textbox to modify (0-based indexing) + color (str): The color to apply to the textbox (e.g., 'red', 'green', 'blue', 'yellow', or hex color code) + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_impress_set_text_color(self, slide_index, box_index, color): + '''Sets the text color for the specified textbox on a slide. + +Args: + slide_index (int): The index of the slide to modify (1-based indexing) + box_index (int): The index of the textbox to modify (0-based indexing) + color (str): The color to apply to the text (e.g., 'red', 'green', 'blue', 'black', or hex color code) + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_impress_delete_content(self, slide_index, box_index): + '''Deletes the specified textbox from a slide. + +:param slide_index: The index of the slide to modify (1-based indexing) +:param box_index: The index of the textbox to modify (0-based indexing) +:return: True if successful, False otherwise''' + +def libreoffice_impress_set_slide_orientation(self, orientation): + '''Changes the orientation of slides in the presentation between portrait (upright) and landscape (sideways). + +:param orientation: The desired orientation for the slides ('portrait' or 'landscape') +:return: True if successful, False otherwise''' + +def libreoffice_impress_position_box(self, slide_index, box_index, position): + '''Positions a textbox or image on a slide at a specific location or predefined position. + +:param slide_index: The index of the slide containing the box (1-based indexing) +:param box_index: The index of the box to position (0-based indexing) +:param position: Predefined position on the slide (left, right, center, top, bottom, etc.) +:return: True if successful, False otherwise''' + +def libreoffice_impress_insert_file(self, file_path, slide_index=None, position=None, size=None, autoplay=False): + '''Inserts a video file into the current or specified slide in the presentation. + +Args: + file_path (str): The full path to the video file to be inserted + slide_index (int, optional): The index of the slide to insert the video into (1-based indexing). + If not provided, inserts into the current slide. + position (dict, optional): The position coordinates for the video as percentages of slide dimensions + {'x': float, 'y': float} + size (dict, optional): The size dimensions for the video as percentages of slide dimensions + {'width': float, 'height': float} + autoplay (bool, optional): Whether the video should automatically play when the slide is shown + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_impress_set_slide_background(self, slide_index=None, color=None, image_path=None): + '''Sets the background color or image for a specific slide or all slides. + +Args: + slide_index (int, optional): The index of the slide to modify (1-based indexing). + If not provided, applies to all slides. + color (str, optional): The background color to apply (e.g., 'red', 'green', 'blue', or hex color code) + image_path (str, optional): Path to an image file to use as background. If provided, overrides color. + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_impress_save_as(self, file_path, overwrite=False): + '''Saves the current document to a specified location with a given filename. + +:param file_path: The full path where the file should be saved, including the filename and extension +:param overwrite: Whether to overwrite the file if it already exists (default: False) +:return: True if successful, False otherwise''' + +def libreoffice_impress_insert_image(self, slide_index, image_path, width=None, height=None, position=None): + '''Inserts an image to a specific slide in the presentation. + +Args: + slide_index (int): The index of the slide to add the image to (1-based indexing) + image_path (str): The full path to the image file to be added + width (float, optional): The width of the image in centimeters + height (float, optional): The height of the image in centimeters + position (dict, optional): The position coordinates for the image as percentages + { + 'x': float, # The x-coordinate as a percentage of slide width + 'y': float # The y-coordinate as a percentage of slide height + } + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_impress_configure_display_settings(self, use_presenter_view=None, primary_monitor_only=None, monitor_for_presentation=None + ): + '''Configures the display settings for LibreOffice Impress presentations. + +Args: + use_presenter_view (bool, optional): Whether to use presenter view. Set to false to disable presenter view. + primary_monitor_only (bool, optional): Whether to use only the primary monitor for the presentation. + monitor_for_presentation (int, optional): Specify which monitor to use (1 for primary, 2 for secondary, etc.) + +Returns: + bool: True if settings were successfully applied, False otherwise''' + +def libreoffice_impress_set_text_strikethrough(self, slide_index, box_index, line_numbers, apply): + '''Applies or removes strike-through formatting to specific text content in a slide. + +Args: + slide_index (int): The index of the slide containing the text (1-based indexing) + box_index (int): The index of the textbox containing the text (0-based indexing) + line_numbers (list): The line numbers to apply strike-through formatting to (1-based indexing) + apply (bool): Whether to apply (true) or remove (false) strike-through formatting + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_impress_set_textbox_alignment(self, slide_index, box_index, alignment): + '''Sets the text alignment for the specified textbox on a slide. + +:param slide_index: The index of the slide to modify (1-based indexing) +:param box_index: The index of the textbox to modify (0-based indexing) +:param alignment: The text alignment to apply ('left', 'center', 'right', or 'justify') +:return: True if successful, False otherwise''' + +def libreoffice_impress_set_slide_number_color(self, color): + '''Sets the color of the slide number in the presentation. + +Args: + color (str): The color to apply to slide numbers (e.g., 'red', 'green', 'blue', 'black', or hex color code) + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_impress_export_to_image(self, file_path, format, slide_index=None): + '''Exports the current presentation or a specific slide to an image file format. + +Args: + file_path (str): The full path where the image file should be saved, including the filename and extension + format (str): The image format to export to (e.g., 'png', 'jpeg', 'gif') + slide_index (int, optional): The index of the specific slide to export (1-based indexing). + If not provided, exports the entire presentation as a series of images. + +Returns: + bool: True if export was successful, False otherwise''' + + +def libreoffice_writer_save(self): + '''保存文档到当前位置''' + +def libreoffice_writer_write_text(self, text, bold=False, italic=False, size=None): + '''写入文本''' + +def libreoffice_writer_set_color(self, pattern, color, paragraph_indices=None): + '''Changes the color of matched text in the document for specified paragraphs. + +Args: + pattern (str): Regular expression pattern to match text + color (int): Hex color code (e.g., 0x000000 for black) + paragraph_indices (list, optional): List of paragraph indices to modify (0-based). + If None, applies to all paragraphs.''' + +def libreoffice_writer_find_and_replace(self, pattern, replacement, paragraph_indices=None): + '''Finds all occurrences of a specified text pattern and replaces them with another text in the document. + +Args: + pattern (str): The pattern to match in the document, should be a regular expression + replacement (str): The text to replace the found text with + paragraph_indices (list, optional): Indices of paragraphs to modify (0-based indexing) + +Returns: + str: Success message with number of replacements made''' + +def libreoffice_writer_set_font(self, font_name, paragraph_indices=None): + '''Changes the font of text in the document or specified paragraphs. + +Args: + font_name (str): The name of the font to apply (e.g., 'Times New Roman', 'Arial', 'Calibri') + paragraph_indices (list, optional): Indices of paragraphs to modify (0-based indexing). + If not provided, applies to all paragraphs.''' + +def libreoffice_writer_set_line_spacing(self, spacing_value, paragraph_indices=None): + '''Sets the line spacing for specified paragraphs in the document. + +Args: + spacing_value (float): The line spacing value to apply (1.0 for single spacing, 2.0 for double spacing, etc.) + paragraph_indices (list, optional): Indices of paragraphs to modify (0-based indexing). + If not provided, applies to all paragraphs.''' + +def libreoffice_writer_remove_highlighting(self, paragraph_indices=None): + '''Removes ALL highlighting from text in the document for specified paragraphs. + +Args: + paragraph_indices (list, optional): Indices of paragraphs to modify (0-based indexing). + If not provided, applies to all paragraphs. + +Returns: + str: Success message or error message''' + +def libreoffice_writer_find_highlighted_text(self, highlight_color): + '''Finds all text in the document that has a specific highlight color applied to it. + +Args: + highlight_color (str): The highlight color to search for. Can be a color name (e.g., 'yellow', 'green') or hex code. + +Returns: + list: A list of strings containing all text segments with the specified highlight color.''' + +def libreoffice_writer_insert_formula_at_cursor(self, formula): + '''Inserts a formula at the current cursor position in the document. + +Args: + formula (str): The formula to insert at the current cursor position. + +Returns: + bool: True if successful, False otherwise''' + +def libreoffice_writer_insert_image_at_cursor(self, image_path, width=None, height=None): + '''Inserts an image at the current cursor position in the document. + +Args: + image_path (str): Full path to the image file to insert + width (int, optional): Width to display the image in pixels + height (int, optional): Height to display the image in pixels + +Returns: + str: Success message or error message''' + +def libreoffice_writer_set_strikethrough(self, pattern, paragraph_indices=None): + '''Sets the strikethrough formatting for text matching the specified pattern in the document. + +Args: + pattern (str): The regular expression pattern to match in the document + paragraph_indices (list, optional): Indices of paragraphs to modify (0-based indexing). + If not provided, applies to all paragraphs. + +Returns: + str: Success message or error information''' + +def libreoffice_writer_set_font_size(self, font_size, pattern, paragraph_indices=None): + '''Changes the font size of specified text in the document. + +Args: + font_size (float): The font size to apply (in points). + pattern (str): The pattern to match in the document, should be a regular expression. + paragraph_indices (list, optional): Indices of paragraphs to modify (0-based indexing). + If not provided, applies to all paragraphs. + +Returns: + str: Result message indicating success or failure.''' + +def libreoffice_writer_export_to_pdf(self, output_path=None, output_filename=None, include_comments=False, quality="standard"): + '''Exports the current document to PDF format. + +Args: + output_path (str, optional): The full path where the PDF should be saved. + If not provided, uses the same location as the original document. + output_filename (str, optional): The filename to use for the PDF. + If not provided, uses the original document's filename with .pdf extension. + include_comments (bool, optional): Whether to include comments in the exported PDF. + Defaults to False. + quality (str, optional): The quality of the PDF export ('standard', 'high', 'print'). + Defaults to 'standard'. + +Returns: + str: Path to the exported PDF file or error message''' + +def libreoffice_writer_set_paragraph_alignment(self, alignment, paragraph_indices=None): + '''Sets the text alignment for specified paragraphs in the document. + +Args: + alignment (str): The alignment to apply ('left', 'center', 'right', 'justify'). + paragraph_indices (list, optional): Indices of paragraphs to modify (0-based indexing). + If not provided, applies to all paragraphs. + +Returns: + str: Success message or error message''' + +def libreoffice_writer_capitalize_words(self, paragraph_indices=None): + '''Capitalizes the first letter of each word for specified paragraphs in the document. + +Args: + paragraph_indices (list, optional): Indices of paragraphs to modify (0-based indexing). + If not provided, applies to all paragraphs. + +Returns: + str: Success message or error message''' + +def libreoffice_writer_set_default_font(self, font_name, font_size=None): + '''Sets the default font for new text in the document without changing existing text. + +Args: + font_name (str): The name of the font to set as default (e.g., 'Times New Roman', 'Arial', 'Calibri') + font_size (float, optional): The default font size in points. + +Returns: + str: Success message or error message''' + +def libreoffice_writer_add_page_numbers(self, position, start_number=1, format=None): + '''Adds page numbers to the document at the specified position. + +Args: + position (str): Position of the page numbers ('bottom_left', 'bottom_center', 'bottom_right', + 'top_left', 'top_center', 'top_right') + start_number (int +def libreoffice_writer_add_page_numbers(self, position, start_number=1, format=None): + '''Adds page numbers to the document at the specified position. + +Args: + position (str): Position of the page numbers ('bottom_left', 'bottom_center', 'bottom_right', + 'top_left', 'top_center', 'top_right') + start_number (int, optional): The starting page number. Defaults to 1. + format (str, optional): Format of the page numbers (e.g., '1', 'Page 1', '1 of N'). + Defaults to simple number format. + +Returns: + str: Success message or error message''', optional): The starting page number. Defaults to 1. + format (str, optional): Format of the page numbers (e.g., '1', 'Page 1', '1 of N'). + Defaults to simple number format. + +Returns: + str: Success message or error message''' + +def libreoffice_writer_insert_page_break(self, position="at_cursor"): + '''Inserts a page break at the specified position. + +Args: + position (str): Where to insert the page break: 'at_cursor' for current cursor position, + 'end_of_document' for end of document. Defaults to 'at_cursor'.''' + +Your response should be formatted like this: +(Previous action verification) +Carefully analyze based on the screenshot if the previous action was successful. If the previous action was not successful, provide a reason for the failure. + +(Screenshot Analysis) +Closely examine and describe the current state of the desktop along with the currently open applications. + +(Next Action) +Based on the current screenshot and the history of your previous interaction with the UI, decide on the next action in natural language to accomplish the given task. + +(Grounded Action) +Translate the next action into code using the provided API methods. Format the code like this: +```python +agent.click("The menu button at the top right of the window", 1, "left") +``` +Note for the code: +1. Only perform one action at a time. +2. Do not put anything other than python code in the block. You can only use one function call at a time. Do not put more than one function call in the block. +3. You must use only the available methods provided above to interact with the UI, do not invent new methods. +4. Only return one code block every time. There must be a single line of code in the code block. +5. Do not do anything other than the exact specified task. Return with `agent.done()` immediately after the subtask is completed or `agent.fail()` if it cannot be completed. +6. Whenever possible, your grounded action should use hot-keys with the agent.hotkey() action instead of clicking or dragging. +7. My computer's password is 'osworld-public-evaluation', feel free to use it when you need sudo rights. +8. Before performing any calculations on elements in a table or inserting charts, always use libreoffice_calc_get_column_data or libreoffice_calc_get_active_sheet_data to obtain accurate column coordinates and element values from the table, ensuring precise execution of subsequent calculations or chart insertions. +9. Generate agent.fail() as your grounded action if you get exhaustively stuck on the task and believe it is impossible. +10. Generate agent.done() as your grounded action when your believe the task is fully complete. +11. Do not use the "command" + "tab" hotkey on MacOS. +""" + + +REFLECTION_SYS_PROMPT = """ +You are an expert computer use agent designed to reflect on the trajectory of a task and provide feedback on what has happened so far. +You have access to the Task Description and the Current Trajectory of another computer agent. The Current Trajectory is a sequence of a desktop image, chain-of-thought reasoning, and a desktop action for each time step. The last image is the screen's display after the last action. +Your task is to generate a reflection. Your generated reflection must fall under one of the cases listed below: + +**Your judgment must be based solely on a critical comparison between the agent's stated plan/reasoning and the visual evidence presented in the screenshot history.** Do not take the agent's claims of success at face value. **If there is no visual proof in the screenshot, the action did not happen.** + +Case 1. The trajectory is not going according to plan. This occurs when there is a mismatch between the intended action and the visual outcome, when the agent hallucinates information, or when it is stuck. You must trigger Case 1 if you detect any of the following: +Failed Action: The previous action did not produce its expected visual change on the screen (e.g., a window failed to open, text was not pasted). +Unsupported Conclusion (Hallucination): The agent makes a claim or states a result (like a number or a fact) that is not visibly supported by the current or any previous screenshot. This is a critical failure. +Repetitive Cycle: The agent is repeating actions without making meaningful progress. +Case 2. The trajectory is going according to plan. In this case, simply tell the agent to continue proceeding as planned. DO NOT encourage a specific action in particular. +Case 3. You believe the current task has been completed. In this case, tell the agent that the task has been successfully completed. + +To be successful, you must follow the rules below: +- **Your output MUST be based on one of the case options above**. +- DO NOT suggest any specific future plans or actions. Your only goal is to provide a reflection, not an actual plan or action. +- Any response that falls under Case 1 should explain why the trajectory is not going according to plan. You should especially lookout for cycles of actions that are continually repeated with no progress. +- Any response that falls under Case 2 should be concise, since you just need to affirm the agent to continue with the current trajectory. +""" diff --git a/mm_agents/aworldguiagent/utils.py b/mm_agents/aworldguiagent/utils.py new file mode 100644 index 0000000..8f7f8c9 --- /dev/null +++ b/mm_agents/aworldguiagent/utils.py @@ -0,0 +1,194 @@ +""" +This code is adapted from AgentS2 (https://github.com/simular-ai/Agent-S) +with modifications to suit specific requirements. +""" +import re +import base64 +from aworld.core.common import Observation, ActionModel +from aworld.models.model_response import ModelResponse +from aworld.core.agent.base import AgentResult +from aworld.memory.main import InMemoryMemoryStore + +def encode_image(image_content): + # if image_content is a path to an image file, check type of the image_content to verify + if isinstance(image_content, str): + with open(image_content, "rb") as image_file: + return base64.b64encode(image_file.read()).decode("utf-8") + else: + return base64.b64encode(image_content).decode("utf-8") + + +def extract_first_agent_function(code_string): + # Regular expression pattern to match 'agent' functions with any arguments, including nested parentheses + pattern = r'agent\.[a-zA-Z_]+\((?:[^()\'"]|\'[^\']*\'|"[^"]*")*\)' + + # Find all matches in the string + matches = re.findall(pattern, code_string) + + # Return the first match if found, otherwise return None + return matches[0] if matches else None + + +def parse_single_code_from_string(input_string): + input_string = input_string.strip() + if input_string.strip() in ["WAIT", "DONE", "FAIL"]: + return input_string.strip() + + # This regular expression will match both ```code``` and ```python code``` + # and capture the `code` part. It uses a non-greedy match for the content inside. + pattern = r"```(?:\w+\s+)?(.*?)```" + # Find all non-overlapping matches in the string + matches = re.findall(pattern, input_string, re.DOTALL) + + # The regex above captures the content inside the triple backticks. + # The `re.DOTALL` flag allows the dot `.` to match newline characters as well, + # so the code inside backticks can span multiple lines. + + # matches now contains all the captured code snippets + + codes = [] + + for match in matches: + match = match.strip() + commands = [ + "WAIT", + "DONE", + "FAIL", + ] # fixme: updates this part when we have more commands + + if match in commands: + codes.append(match.strip()) + elif match.split("\n")[-1] in commands: + if len(match.split("\n")) > 1: + codes.append("\n".join(match.split("\n")[:-1])) + codes.append(match.split("\n")[-1]) + else: + codes.append(match) + + if len(codes) <= 0: + return "fail" + return codes[0] + + +def sanitize_code(code): + # This pattern captures the outermost double-quoted text + if "\n" in code: + pattern = r'(".*?")' + # Find all matches in the text + matches = re.findall(pattern, code, flags=re.DOTALL) + if matches: + # Replace the first occurrence only + first_match = matches[0] + code = code.replace(first_match, f'"""{first_match[1:-1]}"""', 1) + return code + +def prune_image_messages(memory_store: InMemoryMemoryStore, max_trajectory_length: int): + """ + 检查 memory_store 中的消息,并仅保留最新的 max_trajectory_length 个包含图片的消息。 + 对于更早的包含图片的消息,会从其 content 中移除图片部分。 + + Args: + memory_store (InMemoryMemoryStore): 内存存储的对象实例。 + max_trajectory_length (int): 希望保留的含图片消息的最大数量。 + """ + # 步骤 1: 使用 memory_store 的 get_all 方法获取所有消息 + all_items = memory_store.get_all() + + # 步骤 2: 筛选出所有包含图片内容的消息 + image_messages = [] + for item in all_items: + if isinstance(item.content, list): + if any(isinstance(part, dict) and part.get('type') == 'image_url' for part in item.content): + image_messages.append(item) + + # 步骤 3: 检查包含图片的消息数量是否超过限制 + if len(image_messages) <= max_trajectory_length: + print("Number of image messages does not exceed the limit. No pruning needed.") + return + + # 步骤 4: 确定需要移除图片的旧消息 + # 由于 get_all() 返回的列表是按添加顺序排列的,所以列表前面的项就是最旧的 + num_to_prune = len(image_messages) - max_trajectory_length + messages_to_prune = image_messages[:num_to_prune] + + print(f"Found {len(image_messages)} image messages. Pruning the oldest {num_to_prune}.") + + # 步骤 5: 遍历需要修剪的消息,更新其 content,并使用 store 的 update 方法保存 + for item_to_prune in messages_to_prune: + + # 创建一个新的 content 列表,仅包含非图片部分 + new_content = [ + part for part in item_to_prune.content + if not (isinstance(part, dict) and part.get('type') == 'image_url') + ] + + # 可选:如果 new_content 中只剩下一个文本元素,可以将其简化为字符串 + if len(new_content) == 1 and new_content[0].get('type') == 'text': + final_content = new_content[0].get('text', '') + else: + final_content = new_content + + # 更新消息对象的 content 属性 + item_to_prune.content = final_content + + # 使用 memory_store 的 update 方法将更改持久化到 store 中 + memory_store.update(item_to_prune) + + print(f"Pruned image from message with ID: {item_to_prune.id}") + +def reps_action_result(resp: ModelResponse) -> AgentResult: + try: + full_response = resp.content + # Extract thoughts section + thoughts_match = re.search( + r"(.*?)", full_response, re.DOTALL + ) + thoughts = thoughts_match.group(1).strip() + # Extract answer section + answer_match = re.search(r"(.*?)", full_response, re.DOTALL) + answer = answer_match.group(1).strip() + action = ActionModel(action_name=answer, policy_info=thoughts) + return AgentResult(actions=[action], current_state=None) + except Exception as e: + action = ActionModel(action_name=resp.content, policy_info="") + return AgentResult(actions=[action], current_state=None) + +def parse_single_code_from_string(input_string): + input_string = input_string.strip() + if input_string.strip() in ["WAIT", "DONE", "FAIL"]: + return input_string.strip() + + # This regular expression will match both ```code``` and ```python code``` + # and capture the `code` part. It uses a non-greedy match for the content inside. + pattern = r"```(?:\w+\s+)?(.*?)```" + # Find all non-overlapping matches in the string + matches = re.findall(pattern, input_string, re.DOTALL) + + # The regex above captures the content inside the triple backticks. + # The `re.DOTALL` flag allows the dot `.` to match newline characters as well, + # so the code inside backticks can span multiple lines. + + # matches now contains all the captured code snippets + + codes = [] + + for match in matches: + match = match.strip() + commands = [ + "WAIT", + "DONE", + "FAIL", + ] # fixme: updates this part when we have more commands + + if match in commands: + codes.append(match.strip()) + elif match.split("\n")[-1] in commands: + if len(match.split("\n")) > 1: + codes.append("\n".join(match.split("\n")[:-1])) + codes.append(match.split("\n")[-1]) + else: + codes.append(match) + + if len(codes) <= 0: + return "fail" + return codes[0] \ No newline at end of file diff --git a/mm_agents/aworldguiagent/workflow.py b/mm_agents/aworldguiagent/workflow.py new file mode 100644 index 0000000..d4adf80 --- /dev/null +++ b/mm_agents/aworldguiagent/workflow.py @@ -0,0 +1,230 @@ +""" +This code is adapted from AgentS2 (https://github.com/simular-ai/Agent-S) +with modifications to suit specific requirements. +""" +import logging +import textwrap +from typing import Dict, List, Tuple + +from aworld.config.conf import AgentConfig +from aworld.agents.llm_agent import Agent +from aworld.core.common import Observation + +from aworld.core.task import Task +from aworld.core.context.base import Context +from aworld.core.event.base import Message +from aworld.models.llm import get_llm_model +from aworld.utils.common import sync_exec + +from mm_agents.aworldguiagent.grounding import ACI +from mm_agents.aworldguiagent.prompt import GENERATOR_SYS_PROMPT, REFLECTION_SYS_PROMPT +from mm_agents.aworldguiagent.utils import encode_image, extract_first_agent_function, parse_single_code_from_string, sanitize_code +from mm_agents.aworldguiagent.utils import prune_image_messages, reps_action_result + +logger = logging.getLogger("desktopenv.agent") + + +class Worker: + def __init__( + self, + engine_params: Dict, + grounding_agent: ACI, + platform: str = "ubuntu", + max_trajectory_length: int = 16, + enable_reflection: bool = True, + ): + """ + Worker receives the main task and generates actions, without the need of hierarchical planning + Args: + engine_params: Dict + Parameters for the multimodal engine + grounding_agent: Agent + The grounding agent to use + platform: str + OS platform the agent runs on (darwin, linux, windows) + max_trajectory_length: int + The amount of images turns to keep + enable_reflection: bool + Whether to enable reflection + """ + # super().__init__(engine_params, platform) + + self.grounding_agent = grounding_agent + self.max_trajectory_length = max_trajectory_length + self.enable_reflection = enable_reflection + self.use_thinking = engine_params.get("model", "") in [ + "claude-3-7-sonnet-20250219" + ] + + self.generator_agent_config = AgentConfig( + llm_provider=engine_params.get("engine_type", "openai"), + llm_model_name=engine_params.get("model", "openai/o3",), + llm_temperature=engine_params.get("temperature", 1.0), + llm_base_url=engine_params.get("base_url", "https://openrouter.ai/api/v1"), + llm_api_key=engine_params.get("api_key", ""), + ) + + self.reset() + + def reset(self): + + self.generator_agent = Agent( + name="generator_agent", + conf=self.generator_agent_config, + system_prompt=GENERATOR_SYS_PROMPT, + resp_parse_func=reps_action_result + ) + + self.reflection_agent = Agent( + name="reflection_agent", + conf=self.generator_agent_config, + system_prompt=REFLECTION_SYS_PROMPT, + resp_parse_func=reps_action_result + ) + + self.turn_count = 0 + self.worker_history = [] + self.reflections = [] + self.cost_this_turn = 0 + self.screenshot_inputs = [] + + self.dummy_task = Task() + self.dummy_context = Context() + self.dummy_context.set_task(self.dummy_task) + self.dummy_message = Message(headers={'context': self.dummy_context}) + + self.planning_model = get_llm_model(self.generator_agent_config) + + self.first_done = False + self.first_image = None + + def generate_next_action( + self, + instruction: str, + obs: Dict, + ) -> Tuple[Dict, List]: + """ + Predict the next action(s) based on the current observation. + """ + agent = self.grounding_agent + generator_message = ( + "" + if self.turn_count > 0 + else "The initial screen is provided. No action has been taken yet." + ) + + # Load the task into the system prompt + if self.turn_count == 0: + self.generator_agent.system_prompt = self.generator_agent.system_prompt.replace( + "TASK_DESCRIPTION", instruction) + + # Get the per-step reflection + reflection = None + reflection_thoughts = None + if self.enable_reflection: + # Load the initial message + if self.turn_count == 0: + text_content = textwrap.dedent( + f""" + Task Description: {instruction} + Current Trajectory below: + """ + ) + updated_sys_prompt = ( + self.reflection_agent.system_prompt + "\n" + text_content + ) + self.reflection_agent.system_prompt = updated_sys_prompt + + image_content = [ + { + "type": "text", + "text": f"The initial screen is provided. No action has been taken yet." + }, + { + "type": "image_url", + "image_url": { + "url": "data:image/png;base64," + encode_image(obs["screenshot"]) + } + } + ] + self.reflection_agent._init_context(context=self.dummy_context) + + sync_exec( + self.reflection_agent._add_human_input_to_memory, + image_content, + self.dummy_context, + "message" + ) + + # Load the latest action + else: + + image = "data:image/png;base64," + encode_image(obs["screenshot"]) + reflection_message = self.worker_history[-1] + "\n" + f"Here is function execute result: {obs['action_response']}.\n" + + reflection_observation = Observation(content=reflection_message, image=image) + + self.reflection_agent._init_context(context=self.dummy_context) + reflection_actions = self.reflection_agent.policy(reflection_observation, message=self.dummy_message) + + reflection = reflection_actions[0].action_name + reflection_thoughts = reflection_actions[0].policy_info + + self.reflections.append(reflection) + + generator_message += f"Here is your function execute result: {obs['action_response']}.\n" + + generator_message += f"REFLECTION: You may use this reflection on the previous action and overall trajectory:\n{reflection}\n" + logger.info("REFLECTION: %s", reflection) + + if self.first_done: + pass + + else: + # Add finalized message to conversation + generator_message += f"\nCurrent Text Buffer = [{','.join(agent.notes)}]\n" + + image = "data:image/png;base64," + encode_image(obs["screenshot"]) + generator_observation = Observation(content=generator_message, image=image) + + self.generator_agent._init_context(context=self.dummy_context) + generator_actions = self.generator_agent.policy(generator_observation, message=self.dummy_message) + + plan = generator_actions[0].action_name + plan_thoughts = generator_actions[0].policy_info + + prune_image_messages(self.generator_agent.memory.memory_store, 16) + prune_image_messages(self.reflection_agent.memory.memory_store, 16) + + self.worker_history.append(plan) + + logger.info("FULL PLAN:\n %s", plan) + + # self.generator_agent.add_message(plan, role="assistant") + # Use the grounding agent to convert agent_action("desc") into agent_action([x, y]) + + try: + agent.assign_coordinates(plan, obs) + plan_code = parse_single_code_from_string(plan.split("Grounded Action")[-1]) + plan_code = sanitize_code(plan_code) + plan_code = extract_first_agent_function(plan_code) + exec_code = eval(plan_code) + + except Exception as e: + logger.error("Error in parsing plan code: %s", e) + plan_code = "agent.wait(1.0)" + exec_code = eval(plan_code) + + executor_info = { + "full_plan": plan, + "executor_plan": plan, + "plan_thoughts": plan_thoughts, + "plan_code": plan_code, + "reflection": reflection, + "reflection_thoughts": reflection_thoughts, + } + self.turn_count += 1 + + self.screenshot_inputs.append(obs["screenshot"]) + + return executor_info, [exec_code] \ No newline at end of file diff --git a/run_multienv_aworldguiagent.py b/run_multienv_aworldguiagent.py new file mode 100644 index 0000000..ac0e945 --- /dev/null +++ b/run_multienv_aworldguiagent.py @@ -0,0 +1,740 @@ +from __future__ import annotations +import argparse +import datetime +import json +import logging +import os +import sys +import signal +import time +from typing import List, Dict, Any, Optional +import math +from tqdm import tqdm +from multiprocessing import Process, Manager +from multiprocessing import current_process +import lib_run_single +from desktop_env.desktop_env import DesktopEnv, _fix_pyautogui_less_than_bug +from mm_agents.aworldguiagent.agent import AworldGUIAgent +from mm_agents.aworldguiagent.grounding import OSWorldACI + +MAX_RETRIES = 5 # Maximum retries for environment setup + +# Global variables for signal handling +active_environments = [] +processes = [] +is_terminating = False + +# import wandb + +# load the environment variables from .env file +if os.path.exists(".env"): + from dotenv import load_dotenv + + load_dotenv() + + +# Logger Configs {{{ # +def config() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Run end-to-end evaluation on the benchmark" + ) + + # environment config + parser.add_argument("--path_to_vm", type=str, default=None) + parser.add_argument( + "--headless", action="store_true", help="Run in headless machine" + ) + parser.add_argument( + "--action_space", type=str, default="pyautogui", help="Action type" + ) + parser.add_argument( + "--observation_type", + choices=["screenshot", "a11y_tree", "screenshot_a11y_tree", "som"], + default="screenshot", + help="Observation type", + ) + parser.add_argument("--sleep_after_execution", type=float, default=0.0) + parser.add_argument("--max_steps", type=int, default=15) + + # agent config + parser.add_argument( + "--test_config_base_dir", type=str, default="evaluation_examples" + ) + + # lm config + parser.add_argument("--model", type=str, default="o3") + + # example config + parser.add_argument("--domain", type=str, default="all") + parser.add_argument( + "--test_all_meta_path", type=str, default="evaluation_examples/test_all.json" + ) + + # logging related + parser.add_argument("--result_dir", type=str, default="./results") + parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to run in parallel") + parser.add_argument("--log_level", type=str, choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], + default='INFO', help="Set the logging level") + # aws config + parser.add_argument( + "--region", type=str, default="us-east-1", help="AWS region for the VM" + ) + parser.add_argument( + "--provider_name", type=str, default="aws", choices=["aws", "virtualbox", "vmware", "docker", "azure"], + help="Provider name" + ) + parser.add_argument( + "--client_password", type=str, default="", help="Client password" + ) + parser.add_argument( + "--screen_width", type=int, default=1920, help="Screen width" + ) + parser.add_argument( + "--screen_height", type=int, default=1080, help="Screen height" + ) + + # agent S2 config + + parser.add_argument("--model_provider", type=str, default="openai") + parser.add_argument( + "--model_url", + type=str, + default="", + help="The URL of the main generation model API.", + ) + parser.add_argument( + "--model_api_key", + type=str, + default="", + help="The API key of the main generation model.", + ) + parser.add_argument("--model_temperature", type=float, default=None, + help="Temperature to fix the generation model at (e.g. o3 can only be run with 1.0)") + + parser.add_argument("--ground_provider", type=str, required=True, help="The provider for the grounding model") + parser.add_argument("--ground_url", type=str, required=True, help="The URL of the grounding model") + parser.add_argument( + "--ground_api_key", + type=str, + default="", + help="The API key of the grounding model.", + ) + parser.add_argument( + "--ground_model", type=str, required=True, help="The model name for the grounding model" + ) + parser.add_argument( + "--grounding_width", + type=int, + required=True, + help="Width of screenshot image after processor rescaling", + ) + parser.add_argument( + "--grounding_height", + type=int, + required=True, + help="Height of screenshot image after processor rescaling", + ) + + args = parser.parse_args() + return args + + +args = config() # Get command line arguments first + +logger = logging.getLogger() +log_level = getattr(logging, args.log_level.upper()) +logger.setLevel(log_level) + +datetime_str: str = datetime.datetime.now().strftime("%Y%m%d@%H%M%S") + +file_handler = logging.FileHandler( + os.path.join("logs", "normal-{:}.log".format(datetime_str)), encoding="utf-8" +) +debug_handler = logging.FileHandler( + os.path.join("logs", "debug-{:}.log".format(datetime_str)), encoding="utf-8" +) +stdout_handler = logging.StreamHandler(sys.stdout) + +file_handler.setLevel(logging.INFO) +debug_handler.setLevel(logging.DEBUG) +stdout_handler.setLevel(log_level) + +formatter = logging.Formatter( + fmt="\x1b[1;33m[%(asctime)s \x1b[31m%(levelname)s \x1b[32m%(module)s/%(lineno)d-%(processName)s\x1b[1;33m] \x1b[0m%(message)s" +) +file_handler.setFormatter(formatter) +debug_handler.setFormatter(formatter) +stdout_handler.setFormatter(formatter) + +stdout_handler.addFilter(logging.Filter("desktopenv")) + +logger.addHandler(file_handler) +logger.addHandler(debug_handler) +logger.addHandler(stdout_handler) +# }}} Logger Configs # + +logger = logging.getLogger("desktopenv.experiment") + + +class CustomDesktopEnv(DesktopEnv): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + logger.info("CustomDesktopEnv class initialized.") + + def reset(self, task_config: Optional[Dict[str, Any]] = None, seed=None, options=None) -> Dict[str, Any]: + + # Reset to certain task in OSWorld + logger.info("Resetting environment...") + logger.info("Switching task...") + logger.info("Setting counters...") + self._traj_no += 1 + self._step_no = 0 + self.action_history.clear() + + for attempt in range(MAX_RETRIES): + # Only revert to snapshot if environment has been used (step/setup) + # This optimization is especially important for cloud providers like AWS + # where unnecessary snapshot operations are costly and time-consuming + + if task_config is not None: + # Only consider task proxy requirement if proxy is enabled at system level + task_use_proxy = task_config.get("proxy", False) and self.enable_proxy + if not self.enable_proxy and task_config.get("proxy", False): + logger.info( + "Task requires proxy but proxy is disabled at system level, ignoring proxy requirement.") + + if task_use_proxy != self.current_use_proxy: + # keep because get_info_from_website depend on this + self.current_use_proxy = task_use_proxy + + if self.is_environment_used: + logger.info("Environment has been used, reverting to snapshot {}...".format(self.snapshot_name)) + self._revert_to_snapshot() + logger.info("Starting emulator...") + self._start_emulator() + logger.info("Emulator started.") + # Reset the usage flag after reverting + self.is_environment_used = False + else: + logger.info("Environment is clean, skipping snapshot revert (provider: {}).".format(self.provider_name)) + + if task_config is not None: + if task_config.get("proxy", False) and self.enable_proxy: + # If using proxy and proxy is enabled, set up the proxy configuration + self.setup_controller._proxy_setup(self.client_password) + self._set_task_info(task_config) + self.setup_controller.reset_cache_dir(self.cache_dir) + logger.info("Clearing browser cache and browsing data...") + try: + self.setup_controller._delete_all_browsing_data_chromium_setup() + logger.info("Browser cache cleared successfully") + except Exception as e: + logger.warning(f"Failed to clear browser cache: {e}") + logger.info("Setting up environment...") + success = self.setup_controller.setup(self.config, + task_config.get("proxy", False) and self.enable_proxy) + if success: + # Mark environment as used when setup is successfully executed + if self.config: # Only mark as used if there were actual setup operations + self.is_environment_used = True + break + else: + logger.error( + "Environment setup failed, retrying (%d/%d)...", + attempt + 1, + MAX_RETRIES, + ) + time.sleep(5) + else: + break + + logger.info("Environment setup complete.") + + # start soffice service for office tools + self.setup_controller._launch_setup( + 'soffice --headless --accept="socket,host=localhost,port=2002;urp;" --norestore --nologo --nodefault', shell=True) + time.sleep(5) + + observation = self._get_obs() + return observation + + def step(self, action, pause=2): + self._step_no += 1 + self.action_history.append(action) + + # Mark environment as used when step is called + self.is_environment_used = True + + reward = 0 # todo: Define reward calculation for each example + done = False # todo: Define episode termination condition for each example + response = None + info = {} + logger.info(f"Step {self._step_no} in trajectory {self._traj_no} with action: {action}") + # handle the special actions + if action in ['WAIT', 'FAIL', 'DONE'] or ( + type(action) == dict and action['action_type'] in ['WAIT', 'FAIL', 'DONE']): + if action == 'WAIT': + time.sleep(pause) + elif action == 'FAIL': + done = True + info = {"fail": True} + elif action == 'DONE': + done = True + info = {"done": True} + + if self.action_space == "computer_13": + # the set of all possible actions defined in the action representation + self.controller.execute_action(action) + elif self.action_space == "pyautogui" or self.action_space == "claude_computer_use": + if action in ['WAIT', 'FAIL', 'DONE']: + self.controller.execute_action(action) + else: + # the set of all possible python commands insides `pyautogui` + if type(action) == str: + # Fix PyAutoGUI '<' character bug before execution + fixed_command = _fix_pyautogui_less_than_bug(action) + response = self.controller.execute_python_command(fixed_command) + + elif type(action) == dict: + # Fix PyAutoGUI '<' character bug before execution + fixed_command = _fix_pyautogui_less_than_bug(action['command']) + response = self.controller.execute_python_command(fixed_command) + + time.sleep(pause) + observation = self._get_obs() + observation["action_response"] = response + return observation, reward, done, info + + +def distribute_tasks(test_all_meta: dict) -> List[tuple]: + all_tasks = [] + for domain, examples in test_all_meta.items(): + for example_id in examples: + all_tasks.append((domain, example_id)) + return all_tasks + + +def process_signal_handler(signum, frame, env_idx): + """Signal handler for child processes to gracefully shut down their environments.""" + logger.info(f"Process {env_idx + 1} received signal {signum}. Shutting down...") + + # Get the active_environments from the caller's frame + local_vars = frame.f_locals + active_environments = local_vars.get('active_environments', []) + + # Close environment in the current process context + for env in active_environments: + if env is not None: + try: + logger.info(f"Process {env_idx + 1} closing environment...") + env.close() + logger.info(f"Process {env_idx + 1} environment closed successfully") + except Exception as e: + logger.error(f"Process {env_idx + 1} error closing environment: {e}") + + logger.info(f"Process {env_idx + 1} shutdown complete. Exiting.") + sys.exit(0) + + +def run_env_tasks(task_queue: Queue, args: argparse.Namespace, shared_scores: list): + active_environments = [] + env = None + try: + from desktop_env.providers.aws.manager import IMAGE_ID_MAP + REGION = args.region + screen_size = (args.screen_width, args.screen_height) + ami_id = IMAGE_ID_MAP[REGION].get(screen_size, IMAGE_ID_MAP[REGION][(1920, 1080)]) + env = CustomDesktopEnv( + path_to_vm=args.path_to_vm, + action_space=args.action_space, + provider_name=args.provider_name, + region=REGION, + # snapshot_name=ami_id, + screen_size=screen_size, + headless=args.headless, + os_type="Ubuntu", + require_a11y_tree=args.observation_type in ["a11y_tree", "screenshot_a11y_tree", "som"], + enable_proxy=False, + client_password=args.client_password + ) + active_environments.append(env) + + # AgentS2 configuration + engine_params = { + "engine_type": args.model_provider, + "model": args.model, + "base_url": getattr(args, 'model_url', ''), + "api_key": getattr(args, 'model_api_key', ''), + "temperature": getattr(args, 'model_temperature', None), + } + + + engine_params_for_grounding = { + "engine_type": args.ground_provider, + "model": args.ground_model, + "base_url": getattr(args, 'ground_url', ''), + "api_key": getattr(args, 'ground_api_key', ''), + "grounding_width": args.grounding_width, + "grounding_height": args.grounding_height, + } + + # Create grounding agent + grounding_agent = OSWorldACI( + platform="linux", + engine_params_for_generation=engine_params, + engine_params_for_grounding=engine_params_for_grounding, + width=args.screen_width, + height=args.screen_height, + ) + + # Create AgentS2 worker + agent = AworldGUIAgent( + engine_params, + grounding_agent, + platform="linux", + ) + + logger.info(f"Process {current_process().name} started.") + while True: + try: + item = task_queue.get(timeout=5) + except Exception: + break + domain, example_id = item + try: + config_file = os.path.join( + args.test_config_base_dir, f"examples/{domain}/{example_id}.json" + ) + with open(config_file, "r", encoding="utf-8") as f: + example = json.load(f) + logger.info(f"[{current_process().name}][Domain]: {domain}") + logger.info(f"[{current_process().name}][Example ID]: {example_id}") + logger.info(f"[{current_process().name}][Instruction]: {example['instruction']}") + example_result_dir = os.path.join( + args.result_dir, + args.action_space, + args.observation_type, + args.model, + domain, + example_id, + ) + os.makedirs(example_result_dir, exist_ok=True) + try: + lib_run_single.run_single_example( + agent, + env, + example, + args.max_steps, + example["instruction"], + args, + example_result_dir, + shared_scores, + ) + except Exception as e: + import traceback + logger.error(f"Exception in {current_process().name} {domain}/{example_id}: {e}") + logger.error(traceback.format_exc()) + try: + env.controller.end_recording( + os.path.join(example_result_dir, "recording.mp4") + ) + except Exception as rec_e: + logger.error(f"Failed to end recording: {rec_e}") + with open(os.path.join(example_result_dir, "traj.jsonl"), "a") as f: + f.write( + json.dumps( + {"Error": f"{domain}/{example_id} - {e}"} + ) + ) + f.write("\n") + except Exception as e: + logger.error(f"Task-level error in {current_process().name}: {e}") + import traceback + logger.error(traceback.format_exc()) + except Exception as e: + logger.error(f"Process-level error in {current_process().name}: {e}") + import traceback + logger.error(traceback.format_exc()) + finally: + logger.info(f"{current_process().name} cleaning up environment...") + try: + if env: + env.close() + logger.info(f"{current_process().name} environment closed successfully") + except Exception as e: + logger.error(f"{current_process().name} error during environment cleanup: {e}") + + +def signal_handler(signum, frame): + """Handle termination signals (SIGINT, SIGTERM) to gracefully shutdown environments.""" + global is_terminating, active_environments, processes + + # Avoid duplicate handling + if is_terminating: + return + + is_terminating = True + logger.info(f"Received signal {signum}. Gracefully shutting down...") + + # Close all registered environments in the main process + for env in active_environments: + try: + logger.info(f"Closing environment...") + env.close() + logger.info(f"Environment closed successfully") + except Exception as e: + logger.error(f"Error closing environment: {e}") + + # Send termination signal to all child processes first + for p in processes: + if p.is_alive(): + try: + logger.info(f"Sending termination signal to process {p.name}...") + p.terminate() + except Exception as e: + logger.error(f"Error sending termination signal to process: {e}") + + # Allow a short time for processes to handle their own cleanup + time.sleep(1) + + # Forcefully terminate any processes that didn't exit + for p in processes: + if p.is_alive(): + try: + logger.info(f"Forcefully terminating process {p.name}...") + import signal as sig + os.kill(p.pid, sig.SIGKILL) + except Exception as e: + logger.error(f"Error forcefully terminating process: {e}") + + logger.info("Shutdown complete. Exiting.") + sys.exit(0) + + +def test(args: argparse.Namespace, test_all_meta: dict) -> None: + global processes + logger.info("Args: %s", args) + all_tasks = distribute_tasks(test_all_meta) + logger.info(f"Total tasks: {len(all_tasks)}") + with Manager() as manager: + shared_scores = manager.list() + task_queue = manager.Queue() + for item in all_tasks: + task_queue.put(item) + num_envs = args.num_envs + processes = [] + for i in range(num_envs): + p = Process( + target=run_env_tasks, + args=(task_queue, args, shared_scores), + name=f"EnvProcess-{i + 1}" + ) + p.daemon = True + p.start() + processes.append(p) + logger.info(f"Started process {p.name} with PID {p.pid}") + try: + while True: + alive_count = 0 + for idx, p in enumerate(processes): + if not p.is_alive(): + logger.warning(f"Process {p.name} died, restarting...") + new_p = Process( + target=run_env_tasks, + args=(task_queue, args, shared_scores), + name=f"EnvProcess-Restart-{idx + 1}" + ) + new_p.daemon = True + new_p.start() + processes[idx] = new_p + logger.info(f"Restarted process {new_p.name} with PID {new_p.pid}") + else: + alive_count += 1 + if task_queue.empty(): + logger.info("All tasks finished.") + break + if alive_count == 0: + logger.error("All processes died, exiting.") + break + time.sleep(5) + for p in processes: + p.join() + except KeyboardInterrupt: + logger.info("Main process received KeyboardInterrupt. Initiating graceful shutdown...") + raise + except Exception as e: + logger.error(f"Unexpected error while waiting for processes: {e}", exc_info=True) + for p in processes: + if p.is_alive(): + try: + logger.info(f"Terminating process {p.name} due to error...") + p.terminate() + except Exception as term_e: + logger.error(f"Error terminating process {p.name}: {term_e}") + raise + scores = list(shared_scores) + logger.info(f"Average score: {sum(scores) / len(scores) if scores else 0}") + + +def get_unfinished( + action_space, use_model, observation_type, result_dir, total_file_json +): + target_dir = os.path.join(result_dir, action_space, observation_type, use_model) + + if not os.path.exists(target_dir): + return total_file_json + + finished = {} + for domain in os.listdir(target_dir): + finished[domain] = [] + domain_path = os.path.join(target_dir, domain) + if os.path.isdir(domain_path): + for example_id in os.listdir(domain_path): + if example_id == "onboard": + continue + example_path = os.path.join(domain_path, example_id) + if os.path.isdir(example_path): + if "result.txt" not in os.listdir(example_path): + # empty all files under example_id + for file in os.listdir(example_path): + os.remove(os.path.join(example_path, file)) + else: + finished[domain].append(example_id) + + if not finished: + return total_file_json + + for domain, examples in finished.items(): + if domain in total_file_json: + total_file_json[domain] = [ + x for x in total_file_json[domain] if x not in examples + ] + + return total_file_json + + +def get_result(action_space, use_model, observation_type, result_dir, total_file_json): + target_dir = os.path.join(result_dir, action_space, observation_type, use_model) + if not os.path.exists(target_dir): + print("New experiment, no result yet.") + return None + + all_result = [] + + for domain in os.listdir(target_dir): + domain_path = os.path.join(target_dir, domain) + if os.path.isdir(domain_path): + for example_id in os.listdir(domain_path): + example_path = os.path.join(domain_path, example_id) + if os.path.isdir(example_path): + if "result.txt" in os.listdir(example_path): + # empty all files under example_id + try: + all_result.append( + float( + open( + os.path.join(example_path, "result.txt"), "r" + ).read() + ) + ) + except: + all_result.append(0.0) + + if not all_result: + print("New experiment, no result yet.") + return None + else: + print("Current Success Rate:", sum(all_result) / len(all_result) * 100, "%") + return all_result + + +if __name__ == "__main__": + ####### The complete version of the list of examples ####### + os.environ["TOKENIZERS_PARALLELISM"] = "false" + + # Register signal handlers for graceful termination + signal.signal(signal.SIGINT, signal_handler) # Handle Ctrl+C + signal.signal(signal.SIGTERM, signal_handler) # Handle termination signal + + try: + args = config() + + # save args to json in result_dir/action_space/observation_type/model/args.json + path_to_args = os.path.join( + args.result_dir, + args.action_space, + args.observation_type, + args.model, + "args.json", + ) + os.makedirs(os.path.dirname(path_to_args), exist_ok=True) + with open(path_to_args, "w", encoding="utf-8") as f: + json.dump(vars(args), f, indent=4) + + with open(args.test_all_meta_path, "r", encoding="utf-8") as f: + test_all_meta = json.load(f) + + if args.domain != "all": + test_all_meta = {args.domain: test_all_meta[args.domain]} + + test_file_list = get_unfinished( + args.action_space, + args.model, + args.observation_type, + args.result_dir, + test_all_meta, + ) + left_info = "" + for domain in test_file_list: + left_info += f"{domain}: {len(test_file_list[domain])}\n" + logger.info(f"Left tasks:\n{left_info}") + + get_result( + args.action_space, + args.model, + args.observation_type, + args.result_dir, + test_all_meta, + ) + test(args, test_file_list) + except KeyboardInterrupt: + logger.info("Main process received KeyboardInterrupt.") + # Signal handler will take care of cleanup + except Exception as e: + logger.error(f"Unexpected error in main process: {e}", exc_info=True) + # Also trigger cleanup for unhandled exceptions + signal_handler(signal.SIGTERM, None) + finally: + # Final cleanup in case any environments or processes remain + logger.info("Main process final cleanup...") + for env in active_environments: + if env is not None: + try: + logger.info(f"Closing environment in final cleanup...") + env.close() + logger.info(f"Environment closed successfully in final cleanup") + except Exception as e: + logger.error(f"Error during final environment cleanup: {e}") + + # First try gentle termination + for p in processes: + if p is not None and p.is_alive(): + try: + logger.info(f"Terminating process {p.name}...") + p.terminate() + except Exception as e: + logger.error(f"Error terminating process: {e}") + + # Wait a moment for processes to terminate + time.sleep(1) + + # Then force kill if needed + for p in processes: + if p is not None and p.is_alive(): + try: + logger.info(f"Force killing process {p.name}...") + os.kill(p.pid, signal.SIGKILL) + logger.info(f"Process {p.name} force killed") + except Exception as e: + logger.error(f"Error force killing process: {e}")