diff --git a/.gitignore b/.gitignore index 971aea2..4e75e8a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # personal config config.yaml +memory.yaml # Python-generated files __pycache__/ diff --git a/README.md b/README.md index a13a8c8..876a699 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@ Merlin is a powerful, multi-agent AI assistant designed to help you with various - **Multi-Agent Architecture**: Merlin uses specialized agents for different tasks - **Weather Expert**: Get detailed meteorological analysis and forecasts - **Games Expert**: Roll dice, flip coins, and enjoy random fun activities -- **Home Automation Expert**: Built with future expansion in mind for home automation capabilities +- **Lighting Expert**: Set up and connect to your phillips hue bridge to control your lights +- **Memory Expert**: Manages Merlins long term memory in a memory.yaml file, so you the user can easily see whats going on. + ## Current Capabilities @@ -26,6 +28,12 @@ Merlin is a powerful, multi-agent AI assistant designed to help you with various - Sync all your lights and rooms ready to command - Control brightness and power on / of rooms and lights +### Memory Expert +- Manage long term memory for Merlin +- Add / edit / remove items from a yaml file that gets loaded on start up and also on command +- can always access memory and show whats there. +- stored in memory.yaml so you the use has easy access to the long term memory + ## Future Vision Merlin's long-term goal is to become a comprehensive AI assistant that can: @@ -142,11 +150,7 @@ Settings are merged in this order (later overrides earlier): Merlin uses [DSPy](dspy.ai) an AI Framework with: - An orchestrator (`TheOracle`) that routes questions to appropriate experts -- Specialized agents for specific domains (weather, games, lights) +- Specialized agents for specific domains (weather, games, lights & memory) - Rich UI with `rich` library for beautiful terminal output -## Development Roadmap -1. **Current**: Basic multi-agent system with weather, games and lighting capabilities -1. **Next**: long term memory -1. **Future**: Full software management and computer assistance capabilities diff --git a/pyproject.toml b/pyproject.toml index 0f58960..34bb056 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "Merlin" -version = "1.1.0" +version = "1.2.0" description = "Merlin - Your AI Assistant with multi-agent architecture" readme = "README.md" requires-python = ">=3.14" diff --git a/src/config/user_config_template.py b/src/config/user_config_template.py index a29804a..4db0ca8 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 = 'openai_chat' - # MODEL_NAME = 'gpt-4' - # HOST_ADDRESS = '192.168.1.100' - # HOST_PORT = '11434' - # HOST_API_KEY = 'your-personal-key' - # HOST_API_PATH = 'v1' # For OpenAI-compatible APIs, e.g., '/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/core/model_factory.py b/src/core/model_factory.py index ff4e858..ecbc0e0 100644 --- a/src/core/model_factory.py +++ b/src/core/model_factory.py @@ -59,3 +59,8 @@ def create_games_model() -> dspy.LM: def create_lighting_model() -> dspy.LM: """Create lighting expert model.""" return ModelFactory.create_dspy_model("expert", "lighting") + + @staticmethod + def create_memory_model() -> dspy.LM: + """Create memory expert model.""" + return ModelFactory.create_dspy_model("expert", "memory") diff --git a/src/experts/memory.py b/src/experts/memory.py new file mode 100644 index 0000000..4b0ebd7 --- /dev/null +++ b/src/experts/memory.py @@ -0,0 +1,119 @@ +import os +import yaml +from typing import Any, Optional, List, Dict +import dspy + + +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. + Your role: + - memory management, adding, removing and updating memories + - returning memory content to the requestor + - 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" + ) + + +class MemoryAgent(dspy.Module): + """A Lighting Agent that has access to Light based tools""" + + def __init__(self): + self.memory_file_path = 'src/config/memory.yaml' + self.tools = [ + self.add_or_update_memory, + self.load_memory, + self.list_memory_keys, + self.remove_memory, + self.load_all_memories, + ] + self.memory_agent = dspy.ReAct( + signature=MemoryExpertSignature, tools=self.tools + ) + + def add_or_update_memory(self, key: str, value: Any) -> bool: + """ + Save or update a memory to long term storage + + Args: + key (str): The key to store/update + value (any): The value to store + + 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 + + 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 + + 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 + + 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 + if key in memory_content: + del memory_content[key] + 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" + memory_content = self._load_memory_file() + return memory_content + + 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: + memory_content = yaml.safe_load(file) or {} + else: + memory_content = {} + return memory_content + + def _save_memory_file(self, memory_content): + try: + with open(self.memory_file_path, "w") as file: + yaml.dump(memory_content, file, default_flow_style=False, indent=2) + return True + except Exception as e: + print(f"Error: {e}") + return False diff --git a/src/experts/orchestrator.py b/src/experts/orchestrator.py index 881c61d..88e3d0b 100644 --- a/src/experts/orchestrator.py +++ b/src/experts/orchestrator.py @@ -5,6 +5,7 @@ from .game import GameAgent from .lights import LightingAgent from .weather import WeatherAgent +from .memory import MemoryAgent class OrchestratorSignature(dspy.Signature): @@ -30,6 +31,7 @@ def __init__(self): self.consult_games_expert, self.consult_weather_expert, self.consult_lighting_expert, + self.consult_memory_expert, ] self.oracle = dspy.ReAct( signature=OrchestratorSignature, tools=self.tools, max_iters=10 @@ -63,3 +65,12 @@ def consult_lighting_expert(self, command: str) -> str: with dspy.context(lm=ModelFactory.create_lighting_model()): result = LightingAgent().lighting_agent(command=command) 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. + """ + with dspy.context(lm=ModelFactory.create_memory_model()): + result = MemoryAgent().memory_agent(command=command) + return result.answer diff --git a/src/main.py b/src/main.py index 07654e8..4507066 100644 --- a/src/main.py +++ b/src/main.py @@ -1,6 +1,7 @@ """Main application entry point for Merlin AI Assistant.""" import logging +from logging.handlers import RotatingFileHandler import dspy from dspy.utils.callback import BaseCallback @@ -155,7 +156,13 @@ def setup_logging(): # Create a console handler console_handler = logging.StreamHandler() - console_handler.setLevel(logging.DEBUG) + console_handler.setLevel(logging.INFO) + + # Create a file handler with rotation every 5MB + file_handler = RotatingFileHandler( + "merlin.log", maxBytes=5*1024*1024, backupCount=3 + ) + file_handler.setLevel(logging.DEBUG) # Create a formatter formatter = logging.Formatter( @@ -164,9 +171,11 @@ def setup_logging(): # Set the formatter for the handler console_handler.setFormatter(formatter) + file_handler.setFormatter(formatter) # Add the handler to the logger logger.addHandler(console_handler) + logger.addHandler(file_handler) return logger diff --git a/uv.lock b/uv.lock index 27624f2..44e7597 100644 --- a/uv.lock +++ b/uv.lock @@ -724,7 +724,7 @@ wheels = [ [[package]] name = "merlin" -version = "1.1.0" +version = "1.2.0" source = { editable = "." } dependencies = [ { name = "dspy" },