From 8f1fc23ce32aaab2f4af961c7b5a4c7d56808951 Mon Sep 17 00:00:00 2001 From: Jarno Ensio Hakulinen Date: Mon, 6 May 2024 07:34:03 -0700 Subject: [PATCH] refactor config file operations --- gui/main_window.py | 7 +- .../azure/ai/assistant/_version.py | 2 +- .../management/assistant_config_manager.py | 121 ++++++++---------- .../management/base_assistant_client.py | 8 ++ .../management/function_config_manager.py | 38 ++++-- 5 files changed, 90 insertions(+), 86 deletions(-) diff --git a/gui/main_window.py b/gui/main_window.py index 6bb47d7..2c95181 100644 --- a/gui/main_window.py +++ b/gui/main_window.py @@ -51,8 +51,8 @@ def __init__(self): QTimer.singleShot(100, lambda: self.deferred_init()) def initialize_singletons(self): - self.function_config_manager = FunctionConfigManager.get_instance() - self.assistant_config_manager = AssistantConfigManager.get_instance() + self.function_config_manager = FunctionConfigManager.get_instance('config') + self.assistant_config_manager = AssistantConfigManager.get_instance('config') self.task_manager = TaskManager.get_instance(self) self.assistant_client_manager = AssistantClientManager.get_instance() @@ -229,6 +229,9 @@ def set_active_ai_client_type(self, ai_client_type : AIClientType): if self.conversation_thread_clients[self.active_ai_client_type] is not None: self.conversation_thread_clients[self.active_ai_client_type].save_conversation_threads() + # Save assistant configurations when switching AI client types + self.assistant_config_manager.save_configs() + self.conversation_view.conversationView.clear() self.active_ai_client_type = ai_client_type client = None diff --git a/sdk/azure-ai-assistant/azure/ai/assistant/_version.py b/sdk/azure-ai-assistant/azure/ai/assistant/_version.py index baa31e9..57ace2c 100644 --- a/sdk/azure-ai-assistant/azure/ai/assistant/_version.py +++ b/sdk/azure-ai-assistant/azure/ai/assistant/_version.py @@ -6,4 +6,4 @@ # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -VERSION = "0.3.2a1" +VERSION = "0.3.3a1" diff --git a/sdk/azure-ai-assistant/azure/ai/assistant/management/assistant_config_manager.py b/sdk/azure-ai-assistant/azure/ai/assistant/management/assistant_config_manager.py index cb1d0fc..ba1c753 100644 --- a/sdk/azure-ai-assistant/azure/ai/assistant/management/assistant_config_manager.py +++ b/sdk/azure-ai-assistant/azure/ai/assistant/management/assistant_config_manager.py @@ -24,28 +24,37 @@ class AssistantConfigManager: """ A class to manage the creation, updating, deletion, and loading of assistant configurations from local files. - :param config_folder: The folder path for storing configuration files. Optional, defaults to 'config'. + :param config_folder: The folder path for storing configuration files. Optional, defaults to config folder in the user's home directory. :type config_folder: str """ def __init__( self, - config_folder : str ='config' + config_folder : Optional[str] = None ) -> None: - self._config_folder = config_folder + if config_folder is None: + self._config_folder = self._default_config_path() + else: + self._config_folder = config_folder self._last_modified_assistant_name = None self._configs: dict[str, AssistantConfig] = {} # Load all assistant configurations under the config folder self.load_configs() + @staticmethod + def _default_config_path() -> str: + home = os.path.expanduser("~") + return os.path.join(home, ".config", 'azure-ai-assistant') + @classmethod def get_instance( cls, - config_folder : str ='config' + config_folder : Optional[str] = None + ) -> 'AssistantConfigManager': """ Gets the singleton instance of the AssistantConfigManager object. - :param config_folder: The folder path for storing configuration files. Optional, defaults to 'config'. + :param config_folder: The folder path for storing configuration files. Optional, defaults to config folder in the user's home directory. :type config_folder: str :return: The singleton instance of the AssistantConfigManager object. @@ -61,8 +70,8 @@ def update_config( config_json : str ) -> str: """ - Updates an existing assistant local configuration. - + Updates an existing assistant local configuration in memory. + :param name: The name of the configuration to update. :type name: str :param config_json: The JSON string containing the updated configuration data. @@ -75,8 +84,11 @@ def update_config( logger.info(f"Updating assistant configuration for '{name}' with data: {config_json}") new_config_data = json.loads(config_json) self._validate_config(new_config_data) - name = self._save_config(name, new_config_data) + + # Update the configuration in memory without saving to a file + self._configs[name] = AssistantConfig(new_config_data) self._last_modified_assistant_name = name + return name except json.JSONDecodeError as e: raise InvalidJSONError(f"Invalid JSON format: {e}") @@ -136,9 +148,6 @@ def get_config( logger.warning(f"No configuration found for '{name}'") return None - # ensure the configurations are up-to-date - self._load_config(name) - # Return the AssistantConfig object for the given name return self._configs.get(name, None) @@ -209,13 +218,19 @@ def _set_last_modified_assistant(self): self._last_modified_assistant_name = latest_assistant_name - def save_configs(self) -> None: + def save_configs( + self, + config_folder: Optional[str] = None + ) -> None: """ Saves all assistant local configurations to json files. + + :param config_folder: The folder path where the configuration files should be saved. Optional, defaults to the config folder. + :type config_folder: str """ # Save all assistant configurations to files for assistant_name, assistant_config in self._configs.items(): - self._save_config(assistant_name, assistant_config._get_config_data()) + self.save_config(assistant_name, config_folder or self._config_folder) def get_last_modified_assistant(self) -> str: """ @@ -296,71 +311,39 @@ def _validate_config(self, config_data): if 'tool_resources' in config_data and config_data.get('tool_resources') is not None and not isinstance(config_data['tool_resources'], dict): raise ConfigError("Assistant 'tool_resources' must be a dictionary in the configuration") - def _save_config(self, assistant_name, config_data): - # Check if the assistant name and configuration data are provided - if not assistant_name: - raise ConfigError("Assistant name is required") - - if not config_data: - raise ConfigError("Assistant configuration data is required") - - logger.info(f"Checking for updates in assistant configuration for '{assistant_name}'") - - # Handle possible change in assistant name within the configuration data - if 'name' in config_data and config_data['name'] != assistant_name: - logger.info(f"Assistant name changed from '{assistant_name}' to \"{config_data['name']}\"") - - # Construct path for potentially existing old configuration files - old_json_config_path = os.path.join(self._config_folder, f"{assistant_name}_assistant_config.json") - old_yaml_config_path = os.path.join(self._config_folder, f"{assistant_name}_assistant_config.yaml") - old_yml_config_path = os.path.join(self._config_folder, f"{assistant_name}_assistant_config.yml") - - # Attempt to delete old configuration files if they exist - for old_path in [old_json_config_path, old_yaml_config_path, old_yml_config_path]: - if os.path.exists(old_path): - try: - os.remove(old_path) - logger.info(f"Removed outdated configuration file: {old_path}") - except Exception as e: - logger.error(f"Error deleting outdated file: {e}") - - # Update the assistant name to the new name from the configuration data - assistant_name = config_data['name'] + def save_config( + self, + name: str, + folder_path : Optional[str] = None + ) -> None: + """ + Saves the specified assistant configuration to a file in the given directory. - # Define the new YAML file path for saving the configuration - config_filename = f"{assistant_name}_assistant_config.yaml" - config_path = os.path.join(self._config_folder, config_filename) + :param name: The name of the assistant configuration to save. + :type name: str + :param folder_path: The directory path where the configuration file should be saved. Optional, defaults to the config folder. + :type folder_path: str + """ + if name not in self._configs: + raise ConfigError(f"No configuration found for '{name}'") - # Update in-memory configuration - self._configs[assistant_name] = AssistantConfig(config_data) - - logger.info(f"Saving updated configuration for '{assistant_name}' in YAML format") + config_data = self._configs[name]._get_config_data() # assuming AssistantConfig has a method to get its data + config_filename = f"{name}_assistant_config.yaml" + folder_path = folder_path or self._config_folder + config_path = os.path.join(folder_path, config_filename) - # Ensure the configuration directory exists - if not os.path.exists(self._config_folder): - try: - os.makedirs(self._config_folder) - except Exception as e: - logger.error(f"Error creating config directory: {e}") - raise ConfigError(f"Error creating config directory: {e}") + # Ensure the directory exists + if not os.path.exists(folder_path): + os.makedirs(folder_path) # Save the configuration data in YAML format try: with open(config_path, 'w') as file: yaml.dump(config_data, file, sort_keys=False) + logger.info(f"Configuration for '{name}' saved successfully at '{config_path}'") except Exception as e: - logger.error(f"Error writing to YAML file: {e}") - raise ConfigError(f"Error writing to YAML file: {e}") - - # Delete the corresponding JSON file if it exists - json_config_path = config_path.replace('.yaml', '.json') - if os.path.exists(json_config_path): - try: - os.remove(json_config_path) - logger.info(f"Removed outdated JSON configuration for '{assistant_name}'") - except Exception as e: - logger.error(f"Error deleting outdated JSON file: {e}") - return assistant_name + logger.error(f"Error saving configuration file at '{config_path}': {e}") + raise ConfigError(f"Error saving configuration file: {e}") @property def configs(self) -> dict: diff --git a/sdk/azure-ai-assistant/azure/ai/assistant/management/base_assistant_client.py b/sdk/azure-ai-assistant/azure/ai/assistant/management/base_assistant_client.py index 1693b4d..fd3679f 100644 --- a/sdk/azure-ai-assistant/azure/ai/assistant/management/base_assistant_client.py +++ b/sdk/azure-ai-assistant/azure/ai/assistant/management/base_assistant_client.py @@ -233,6 +233,14 @@ def _replace_file_references_with_content(self, assistant_config: AssistantConfi file_references = assistant_config.file_references try: + # Log the current working directory + cwd = os.getcwd() + logger.info(f"Current working directory: {cwd}") + + # Optionally, list files in the current directory + files_in_cwd = os.listdir(cwd) + logger.debug(f"Files in the current directory: {files_in_cwd}") + # Regular expression to find all placeholders in the format {file_reference:X} pattern = re.compile(r'\{file_reference:(\d+)\}') diff --git a/sdk/azure-ai-assistant/azure/ai/assistant/management/function_config_manager.py b/sdk/azure-ai-assistant/azure/ai/assistant/management/function_config_manager.py index bcd48d9..3f9de90 100644 --- a/sdk/azure-ai-assistant/azure/ai/assistant/management/function_config_manager.py +++ b/sdk/azure-ai-assistant/azure/ai/assistant/management/function_config_manager.py @@ -1,13 +1,15 @@ # Copyright (c) Microsoft. All rights reserved. # Licensed under the MIT license. See LICENSE.md file in the project root for full license information. -import json -import os, ast, re, sys -from pathlib import Path from azure.ai.assistant.management.function_config import FunctionConfig from azure.ai.assistant.management.exceptions import EngineError from azure.ai.assistant.management.logger_module import logger +import json +import os, ast, re, sys +from pathlib import Path +from typing import Optional + # Template for a function spec function_spec_template = { "type": "function", @@ -39,16 +41,24 @@ class FunctionConfigManager: """ def __init__( self, - config_directory : str = 'config' + config_folder : Optional[str] = None ) -> None: - self._config_directory = config_directory + if config_folder is None: + self._config_folder = self._default_config_path() + else: + self._config_folder = config_folder self.load_function_configs() self.load_function_error_specs() + @staticmethod + def _default_config_path() -> str: + home = os.path.expanduser("~") + return os.path.join(home, ".config", 'azure-ai-assistant') + @classmethod def get_instance( cls, - config_directory : str = 'config' + config_directory : Optional[str] = None ) -> 'FunctionConfigManager': """ Get the singleton instance of FunctionConfigManager. @@ -67,13 +77,13 @@ def load_function_configs(self) -> None: """ Loads function specifications from the config directory. """ - logger.info(f"Loading function specifications from {self._config_directory}") + logger.info(f"Loading function specifications from {self._config_folder}") # Clear the existing configs self._function_configs = {} # Scan the directory for JSON files - for file in Path(self._config_directory).glob("*_function_specs.json"): + for file in Path(self._config_folder).glob("*_function_specs.json"): self._load_function_spec(file) def _load_function_spec(self, file_path): @@ -98,13 +108,13 @@ def load_function_error_specs(self) -> None: """ Loads function error specifications from the config directory. """ - logger.info(f"Loading function error specifications from {self._config_directory}") + logger.info(f"Loading function error specifications from {self._config_folder}") # Clear the existing configs self._function_error_specs = {} # Load the error specs from function_error_specs.json - file_path = Path(self._config_directory) / "function_error_specs.json" + file_path = Path(self._config_folder) / "function_error_specs.json" logger.info(f"Loading function error specs from {file_path}") try: with open(file_path, 'r') as file: @@ -154,7 +164,7 @@ def save_function_error_specs(self, function_error_specs : dict) -> bool: """ try: # Define path for error specs - file_path = Path(self._config_directory) / "function_error_specs.json" + file_path = Path(self._config_folder) / "function_error_specs.json" # Write the error specs to the file with open(file_path, 'w') as file: @@ -280,8 +290,8 @@ def save_function_spec( """ try: # Define paths for system and user specs - system_file_path = Path(self._config_directory) / "system_function_specs.json" - user_file_path = Path(self._config_directory) / "user_function_specs.json" + system_file_path = Path(self._config_folder) / "system_function_specs.json" + user_file_path = Path(self._config_folder) / "user_function_specs.json" new_spec_dict = json.loads(new_spec) @@ -324,7 +334,7 @@ def delete_user_function(self, function_name : str) -> bool: """ try: # Define path for user specs - user_file_path = Path(self._config_directory) / "user_function_specs.json" + user_file_path = Path(self._config_folder) / "user_function_specs.json" # Delete the function from user specs if not self._delete_function_spec(function_name, user_file_path):