Skip to content

Commit

Permalink
refactor config file operations
Browse files Browse the repository at this point in the history
  • Loading branch information
jhakulin committed May 6, 2024
1 parent 3cd4a39 commit 8f1fc23
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 86 deletions.
7 changes: 5 additions & 2 deletions gui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion sdk/azure-ai-assistant/azure/ai/assistant/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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}")
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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+)\}')

Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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.
Expand All @@ -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):
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit 8f1fc23

Please sign in to comment.