diff --git a/pyproject.toml b/pyproject.toml index 34bb056..f35f23d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "Merlin" -version = "1.2.0" +version = "1.2.1" description = "Merlin - Your AI Assistant with multi-agent architecture" readme = "README.md" requires-python = ">=3.14" diff --git a/src/config/app_config.py b/src/config/app_config.py index b4ab816..2b28373 100644 --- a/src/config/app_config.py +++ b/src/config/app_config.py @@ -1,7 +1,8 @@ from pathlib import Path -import yaml from types import SimpleNamespace +import yaml + class Config: """Main Config Class for application-level configuration.""" @@ -183,13 +184,22 @@ class Weather: @classmethod # Load from YAML def load_yaml(cls, file_path="src/config/config.yaml"): + """Load configuration from YAML file. + + Args: + file_path (str): Path to the YAML configuration file. + + Returns: + AppConfig: Configuration instance with loaded settings. + + """ yaml_file = Path(file_path) if not yaml_file.exists(): default_dict = {"DEBUG": True} with open(yaml_file, "w") as f: yaml.dump(default_dict, f) - with open(yaml_file, "r") as f: + with open(yaml_file) as f: config_data = yaml.safe_load(f) # Populate lights and rooms diff --git a/src/config/user_config_template.py b/src/config/user_config_template.py index 4db0ca8..095c402 100644 --- a/src/config/user_config_template.py +++ b/src/config/user_config_template.py @@ -15,12 +15,12 @@ class Model: # Uncomment and modify as needed # Base model overrides (affects all agents unless specifically overridden) - PROVIDER = 'lm_studio' - MODEL_NAME = 'openai/gpt-oss-20b' - HOST_ADDRESS = '127.0.0.1' - HOST_PORT = '1234' - HOST_API_KEY = 'your-personal-key' - HOST_API_PATH = 'v1' + PROVIDER = "lm_studio" + MODEL_NAME = "openai/gpt-oss-20b" + HOST_ADDRESS = "127.0.0.1" + HOST_PORT = "1234" + HOST_API_KEY = "your-personal-key" + HOST_API_PATH = "v1" # Orchestrator personal config ORCHESTRATOR = { diff --git a/src/experts/memory.py b/src/experts/memory.py index 4b0ebd7..1358d70 100644 --- a/src/experts/memory.py +++ b/src/experts/memory.py @@ -1,30 +1,36 @@ import os -import yaml -from typing import Any, Optional, List, Dict +from typing import Any + import dspy +import yaml class MemoryExpertSignature(dspy.Signature): - """You are the long term memory expert responsible for managing memories that persist across sessions - Important, your keys should always be lower case and snake_case to ensure consistency. + """You are the long term memory expert responsible for managing memories. + + Important, your keys should always be lower case and snake_case to + ensure consistency. Your role: - memory management, adding, removing and updating memories - returning memory content to the requestor - - Confirm whether tasks were completed successfully or failed""" + - Confirm whether tasks were completed successfully or failed + """ command: str = dspy.InputField( desc="A natural language command describing what you should do with the memory" ) answer: str = dspy.OutputField( - desc="A confirmation message indicating success or failure of the requested task" + desc="A confirmation message indicating success or failure of the " + "requested task" ) class MemoryAgent(dspy.Module): - """A Lighting Agent that has access to Light based tools""" + """A Memory Agent that has access to memory-based tools.""" def __init__(self): - self.memory_file_path = 'src/config/memory.yaml' + """Initialize the Memory Agent with configuration and tools.""" + self.memory_file_path = "src/config/memory.yaml" self.tools = [ self.add_or_update_memory, self.load_memory, @@ -37,8 +43,7 @@ def __init__(self): ) def add_or_update_memory(self, key: str, value: Any) -> bool: - """ - Save or update a memory to long term storage + """Save or update a memory to long term storage. Args: key (str): The key to store/update @@ -46,46 +51,47 @@ def add_or_update_memory(self, key: str, value: Any) -> bool: Returns: bool: True if successful, False if failed + """ memory_content = self._load_memory_file() memory_content[key] = value succsess = self._save_memory_file(memory_content) return succsess - def load_memory(self, key: str) -> Optional[Any]: - """ - Load a memory value from long term storage by key + def load_memory(self, key: str) -> Any | None: + """Load a memory value from long term storage by key. Args: key (str): The key to retrieve Returns: Any: The stored value if found, None if key doesn't exist + """ memory_content = self._load_memory_file() return memory_content.get(key) - def list_memory_keys(self) -> Optional[List[str]]: - """ - List all keys from long term storage + def list_memory_keys(self) -> list[str] | None: + """List all keys from long term storage. Returns: List[str]: List of all keys if successful, None if failed + """ memory_content = self._load_memory_file() return list(memory_content.keys()) def remove_memory(self, key: str) -> bool: - """ - Remove a key-value pair from long term storage + """Remove a key-value pair from long term storage. Args: key (str): The key to remove Returns: bool: True if successful, False if failed + """ memory_content = self._load_memory_file() # Remove the key if it exists @@ -94,16 +100,16 @@ def remove_memory(self, key: str) -> bool: succsess = self._save_memory_file(memory_content) return succsess - def load_all_memories(self) -> Dict[str, Any]: - "shows all things stored in long term memory" + def load_all_memories(self) -> dict[str, Any]: + """Return all things stored in long term memory.""" memory_content = self._load_memory_file() return memory_content - def _load_memory_file(self) -> Dict[str, Any]: - memory_file_path = 'src/config/memory.yaml' + def _load_memory_file(self) -> dict[str, Any]: + memory_file_path = "src/config/memory.yaml" # Load existing memory content if os.path.exists(memory_file_path): - with open(memory_file_path, "r") as file: + with open(memory_file_path) as file: memory_content = yaml.safe_load(file) or {} else: memory_content = {} diff --git a/src/experts/orchestrator.py b/src/experts/orchestrator.py index 88e3d0b..09d03dc 100644 --- a/src/experts/orchestrator.py +++ b/src/experts/orchestrator.py @@ -4,8 +4,8 @@ from .game import GameAgent from .lights import LightingAgent -from .weather import WeatherAgent from .memory import MemoryAgent +from .weather import WeatherAgent class OrchestratorSignature(dspy.Signature): @@ -67,9 +67,13 @@ def consult_lighting_expert(self, command: str) -> str: return result.answer def consult_memory_expert(self, command: str) -> str: - """Use this expert when you want to save or retrieve any information that should be stored for future use. - Use this expert whenever you encounter information that should persist beyond the current interaction - Use this expert freely and proactively - it's designed to help you maintain a persistent knowledge base. + """Use this expert when you want to save or retrieve information. + + Use this expert when you want to save or retrieve any information + that should be stored for future use. Use this expert whenever you + encounter information that should persist beyond the current + interaction. Use this expert freely and proactively - it's designed + to help you maintain a persistent knowledge base. """ with dspy.context(lm=ModelFactory.create_memory_model()): result = MemoryAgent().memory_agent(command=command) diff --git a/src/main.py b/src/main.py index 4507066..f9ac8b5 100644 --- a/src/main.py +++ b/src/main.py @@ -160,7 +160,7 @@ def setup_logging(): # Create a file handler with rotation every 5MB file_handler = RotatingFileHandler( - "merlin.log", maxBytes=5*1024*1024, backupCount=3 + "merlin.log", maxBytes=5 * 1024 * 1024, backupCount=3 ) file_handler.setLevel(logging.DEBUG) diff --git a/src/setup/lights/hue_bridge_set_up.py b/src/setup/lights/hue_bridge_set_up.py index e31ada9..49295e6 100644 --- a/src/setup/lights/hue_bridge_set_up.py +++ b/src/setup/lights/hue_bridge_set_up.py @@ -10,16 +10,14 @@ def is_bridge_is_set_up() -> bool: - """Check if the bridge is already set up. + """Check if Hue bridge is properly configured. Returns False if not set up Returns True if set up """ load_dotenv() bridge_user = os.getenv("HUE_CLIENTKEY") - if not bridge_user: - return False - return True + return bool(bridge_user) def get_hue_bridge_ip() -> str: @@ -35,7 +33,9 @@ def get_hue_bridge_ip() -> str: def wait_for_hue_bridge_link_button( bridge_ip, max_attempts=30, interval=2, save_to_env=True ): - """Wait for user to press the Hue bridge link button and return the success response. + """Wait for user to press the Hue bridge link button. + + Return the success response. Args: bridge_ip (str): IP address of the Hue bridge @@ -82,7 +82,8 @@ def wait_for_hue_bridge_link_button( # Check for error response (link button not pressed) elif "error" in first_item and first_item["error"]["type"] == 101: print( - f"Link button not pressed yet. Attempt {attempt + 1}/{max_attempts}" + f"Link button not pressed yet. " + f"Attempt {attempt + 1}/{max_attempts}" ) time.sleep(interval) continue @@ -114,6 +115,7 @@ def save_credentials_to_env(credentials, home_ip_address): Args: credentials (dict): Dictionary containing 'username' and 'clientkey' + home_ip_address (str): IP address of the Hue bridge """ # Create or update .env file diff --git a/src/setup/lights/hue_config_setup.py b/src/setup/lights/hue_config_setup.py index 5b7b2cb..57e028b 100644 --- a/src/setup/lights/hue_config_setup.py +++ b/src/setup/lights/hue_config_setup.py @@ -10,12 +10,16 @@ load_dotenv() -class hue_lights: +class HueLights: + """Class for managing Hue lights configuration and setup.""" + def __init__(self): + """Initialize HueLights with environment configuration.""" self.get_secret_info() self.base_url = f"https://{self.HUE_BRIDGE_IP}/clip/v2/resource" def get_secret_info(self): + """Load Hue bridge credentials from environment variables.""" self.HUE_USERNAME = os.getenv("HUE_USERNAME") # app_key self.HUE_CLIENTKEY = os.getenv("HUE_CLIENTKEY") self.HUE_BRIDGE_IP = os.getenv("HUE_BRIDGE_IP") @@ -126,46 +130,41 @@ def get_room_dictionary(self): return {} -def update_config(config_file_path, lights_dict=None, rooms_dict=None): - """Load config.yaml file, update/add lights and/or rooms content, and save it back. - - Args: - config_file_path (str): Path to the config.yaml file - lights_dict (dict, optional): Dictionary of light_name: light_id pairs to update/add - rooms_dict (dict, optional): Dictionary of room_name: room_id pairs to update/add - - """ - # Load existing config file +def _load_config_file(config_file_path): + """Load configuration from YAML file or create empty config.""" if os.path.exists(config_file_path): with open(config_file_path) as file: - config_content = yaml.safe_load(file) or {} + return yaml.safe_load(file) or {} else: - # If file doesn't exist, create empty config - config_content = {} + return {} - # Update or add lights content if provided - if lights_dict is not None: - if "lights" not in config_content: - config_content["lights"] = {} - # Add/update each light - for light_name, light_id in lights_dict.items(): - config_content["lights"][light_name] = {"id": light_id} +def _update_lights_config(config_content, lights_dict): + """Update lights section in config content.""" + if "lights" not in config_content: + config_content["lights"] = {} - # Update or add rooms content if provided - if rooms_dict is not None: - if "rooms" not in config_content: - config_content["rooms"] = {} + for light_name, light_id in lights_dict.items(): + config_content["lights"][light_name] = {"id": light_id} - # Add/update each room - for room_name, room_id in rooms_dict.items(): - config_content["rooms"][room_name] = {"id": room_id} - # Save back to file +def _update_rooms_config(config_content, rooms_dict): + """Update rooms section in config content.""" + if "rooms" not in config_content: + config_content["rooms"] = {} + + for room_name, room_id in rooms_dict.items(): + config_content["rooms"][room_name] = {"id": room_id} + + +def _save_config_file(config_file_path, config_content): + """Save configuration content to YAML file.""" with open(config_file_path, "w") as file: yaml.dump(config_content, file, default_flow_style=False, indent=2) - # Print update messages + +def _print_update_summary(config_file_path, lights_dict, rooms_dict): + """Print summary of configuration updates.""" updates = [] if lights_dict is not None: updates.append("lights") @@ -180,8 +179,38 @@ def update_config(config_file_path, lights_dict=None, rooms_dict=None): print(f"No updates made to config file: {config_file_path}") +def update_config(config_file_path, lights_dict=None, rooms_dict=None): + """Load config.yaml file, update/add lights and/or rooms content, and save it back. + + Args: + config_file_path (str): Path to the config.yaml file + lights_dict (dict, optional): Dictionary of light_name: light_id pairs + to update/add + rooms_dict (dict, optional): Dictionary of room_name: room_id pairs + to update/add + + """ + # Load existing config file + config_content = _load_config_file(config_file_path) + + # Update or add lights content if provided + if lights_dict is not None: + _update_lights_config(config_content, lights_dict) + + # Update or add rooms content if provided + if rooms_dict is not None: + _update_rooms_config(config_content, rooms_dict) + + # Save back to file + _save_config_file(config_file_path, config_content) + + # Print update messages + _print_update_summary(config_file_path, lights_dict, rooms_dict) + + def main(): - hue_wizard = hue_lights() + """Set up Hue lights configuration.""" + hue_wizard = HueLights() lights_dict = hue_wizard.get_light_dictionary() rooms_dict = hue_wizard.get_room_dictionary() diff --git a/uv.lock b/uv.lock index 44e7597..215f9db 100644 --- a/uv.lock +++ b/uv.lock @@ -724,7 +724,7 @@ wheels = [ [[package]] name = "merlin" -version = "1.2.0" +version = "1.2.1" source = { editable = "." } dependencies = [ { name = "dspy" },