From a0761435c5d4662f477687837e62e7277f9535ee Mon Sep 17 00:00:00 2001 From: Jake Pullen Date: Sun, 26 Oct 2025 08:28:38 +0000 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=E2=9C=A8=20long=20term=20memory=20?= =?UTF-8?q?started?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + src/experts/memory.py | 114 ++++++++++++++++++++++++++++++++++++ src/experts/orchestrator.py | 36 +++++++++++- src/merlin.py | 4 +- 4 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 src/experts/memory.py diff --git a/.gitignore b/.gitignore index b59d38d..5e39818 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # personal config config.yaml +memory.yaml +memory.yaml # Python-generated files __pycache__/ diff --git a/src/experts/memory.py b/src/experts/memory.py new file mode 100644 index 0000000..d4b3dc4 --- /dev/null +++ b/src/experts/memory.py @@ -0,0 +1,114 @@ +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 = '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 = '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 \ No newline at end of file diff --git a/src/experts/orchestrator.py b/src/experts/orchestrator.py index 06f1c83..7a74781 100644 --- a/src/experts/orchestrator.py +++ b/src/experts/orchestrator.py @@ -2,6 +2,7 @@ from experts.weather import WeatherAgent from experts.game import GameAgent from experts.lights import LightingAgent +from experts.memory import MemoryAgent from config import Config class OrchestratorSignature(dspy.Signature): @@ -20,6 +21,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, @@ -27,7 +29,6 @@ def __init__(self): max_iters=10 ) - def consult_weather_expert(self,question: str) -> str: """Use this expert when the user asks about weather, temperature, climate, atmospheric conditions, forecasts, or anything related to meteorology. This expert provides detailed weather analysis.""" @@ -60,3 +61,36 @@ def consult_lighting_expert(self,command: str) -> str: )): 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. + + This expert can: + - Add new memories (key-value pairs) to long-term storage + - Update existing memories with new information + - Retrieve specific memories by key + - List all available memory keys + - Remove unwanted memories + + The memory system uses a YAML file called 'memory.yaml' for persistent storage. + + When you need to store information: + - Identify a meaningful key that describes what you're storing + - Store relevant data that might be useful later in the conversation or task + - Use this tool whenever you encounter information that should persist beyond the current interaction + + Examples of when to use this: + - Remembering user preferences or settings + - Storing important facts or data that will be referenced later + - Keeping track of conversation context + - Saving results from previous operations for future reference + + Use this expert freely and proactively - it's designed to help you maintain a persistent knowledge base. + """ + with dspy.context(lm=dspy.LM( + model = f"{Config.Model.PROVIDER}/{Config.Model.EXPERT_MODEL_NAME}", + api_base=f"http://{Config.Model.HOST_ADDRESS}:{Config.Model.HOST_PORT}/v1/", + api_key=Config.Model.HOST_API_KEY, + )): + result = MemoryAgent().memory_agent(command=command) + return result.answer diff --git a/src/merlin.py b/src/merlin.py index 84772a2..bca24dc 100644 --- a/src/merlin.py +++ b/src/merlin.py @@ -30,8 +30,8 @@ def main(): ) console.print(welcome_text) - console.print('[bold cyan]What Can I do you ask?[/bold cyan]') - console.print(intro_text) + # console.print('[bold cyan]What Can I do you ask?[/bold cyan]') + # console.print(intro_text) # Step 1: Head over to src/config.py to Configure DSPy with your LLM default_model = dspy.LM( From 8a571eae3820e02d170853098e90ab918fe57b5c Mon Sep 17 00:00:00 2001 From: Jake Pullen Date: Sun, 26 Oct 2025 15:38:21 +0000 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=F0=9F=94=8D=20Working=20memory=20m?= =?UTF-8?q?anagement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - src/experts/orchestrator.py | 22 +-------------------- src/merlin.py | 38 ++++++++++++++++++++++--------------- 3 files changed, 24 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index 5e39818..6fe2b51 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ # personal config config.yaml memory.yaml -memory.yaml # Python-generated files __pycache__/ diff --git a/src/experts/orchestrator.py b/src/experts/orchestrator.py index 7a74781..245f547 100644 --- a/src/experts/orchestrator.py +++ b/src/experts/orchestrator.py @@ -64,27 +64,7 @@ def consult_lighting_expert(self,command: str) -> str: 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. - - This expert can: - - Add new memories (key-value pairs) to long-term storage - - Update existing memories with new information - - Retrieve specific memories by key - - List all available memory keys - - Remove unwanted memories - - The memory system uses a YAML file called 'memory.yaml' for persistent storage. - - When you need to store information: - - Identify a meaningful key that describes what you're storing - - Store relevant data that might be useful later in the conversation or task - - Use this tool whenever you encounter information that should persist beyond the current interaction - - Examples of when to use this: - - Remembering user preferences or settings - - Storing important facts or data that will be referenced later - - Keeping track of conversation context - - Saving results from previous operations for future reference - + 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=dspy.LM( diff --git a/src/merlin.py b/src/merlin.py index bca24dc..4a0daa0 100644 --- a/src/merlin.py +++ b/src/merlin.py @@ -7,7 +7,8 @@ from rich.panel import Panel from rich.text import Text from rich.markdown import Markdown - +import os +import yaml from experts.orchestrator import TheOracle from config import Config @@ -15,6 +16,7 @@ # Initialize Rich console - Core to all interface console = Console() + def main(): logger.debug('main application started') welcome_text = Text("🤖🧙‍♂️ Hi There! My name is Merlin 🧙‍♂️🤖", style="bold cyan") @@ -43,7 +45,14 @@ def main(): # Initialize history for context history = dspy.History(messages=[]) - + + if os.path.exists('memory.yaml'): + with open('memory.yaml', 'r') as file: + long_term_memory = yaml.safe_load(file) or {} + else: + long_term_memory = {} + + history.messages.append({"memory": long_term_memory}) try: while True: # Styled user input @@ -84,16 +93,6 @@ def main(): # Update history history.messages.append({"question": question, **result}) - # Display AI response in a beautiful panel - ai_response = Panel( - Markdown(answer), - title="🤖🧙‍♂️ Merlin Response", - border_style="cyan", - padding=(1, 2) - ) - console.print(ai_response) - console.print() - # Check if any expert agents were called expert_calls = [] if hasattr(result, 'trajectory') and isinstance(result.trajectory, dict): @@ -102,10 +101,19 @@ def main(): expert_calls.append(str(value)) if expert_calls: - console.print(f"[dim]🔀 Routed to: {', '.join(set(expert_calls))}[/dim]") + info_text = f"🔀 Routed to: {', '.join(set(expert_calls))}" else: - console.print("[dim]💬 Handled directly by orchestrator[/dim]") - console.print() + info_text = "💬 Handled directly by orchestrator" + + # Display AI response in a beautiful panel + ai_response = Panel( + Markdown(answer), + title="🤖🧙‍♂️ Merlin Response", + border_style="cyan", + padding=(1, 2), + subtitle=info_text + ) + console.print(ai_response) except KeyboardInterrupt: console.print("\n[bold cyan]🤖🧙‍♂️ Goodbye!🧙‍♂️🤖[/bold cyan]") From 4c8ca51d6e8c13caf720522d19d84fd69ae0f7ba Mon Sep 17 00:00:00 2001 From: Jake Pullen Date: Sun, 26 Oct 2025 15:50:35 +0000 Subject: [PATCH 3/6] =?UTF-8?q?docs:=20=F0=9F=93=9C=20tidy=20up=20ready=20?= =?UTF-8?q?for=20merlin=201.2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 ++++++++++------ pyproject.toml | 2 +- src/merlin.py | 4 ++-- uv.lock | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e018d34..49ac9d8 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: @@ -51,11 +59,7 @@ Merlin's long-term goal is to become a comprehensive AI assistant that can: 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 178028b..d053785 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "Merlin" -version = "1.1.0" +version = "1.2.0" description = "Your personal AI assistant" readme = "README.md" requires-python = ">=3.14" diff --git a/src/merlin.py b/src/merlin.py index 4a0daa0..ce8bd02 100644 --- a/src/merlin.py +++ b/src/merlin.py @@ -25,6 +25,7 @@ def main(): ☀️ Weather Expert - Meteorological analysis 🎲 Games Expert - Dice, coins, and random fun 💡 Lighting Expert - The power to control your Phillips Hue Lights! + 🧠 Memory Expert - Long term memory management Commands: • Type 'history' to see conversation history • Type 'quit' or 'exit' to leave.""", @@ -32,8 +33,7 @@ def main(): ) console.print(welcome_text) - # console.print('[bold cyan]What Can I do you ask?[/bold cyan]') - # console.print(intro_text) + console.print(intro_text) # Step 1: Head over to src/config.py to Configure DSPy with your LLM default_model = dspy.LM( diff --git a/uv.lock b/uv.lock index 58d4d83..9fc3bd0 100644 --- a/uv.lock +++ b/uv.lock @@ -724,7 +724,7 @@ wheels = [ [[package]] name = "merlin" -version = "1.1.0" +version = "1.2.0" source = { virtual = "." } dependencies = [ { name = "dspy" }, From 9c8a163a61c2d8656f10b8fb21e709c914cc8cd8 Mon Sep 17 00:00:00 2001 From: Jake Pullen Date: Sun, 26 Oct 2025 19:20:47 +0000 Subject: [PATCH 4/6] Updated memory expert file path --- src/experts/memory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/experts/memory.py b/src/experts/memory.py index d4b3dc4..9f434fa 100644 --- a/src/experts/memory.py +++ b/src/experts/memory.py @@ -18,7 +18,7 @@ class MemoryExpertSignature(dspy.Signature): class MemoryAgent(dspy.Module): """ A Lighting Agent that has access to Light based tools""" def __init__(self): - self.memory_file_path = 'memory.yaml' + self.memory_file_path = 'src/config/memory.yaml' self.tools = [ self.add_or_update_memory, self.load_memory, @@ -95,7 +95,7 @@ def load_all_memories(self) -> Dict[str, Any]: return memory_content def _load_memory_file(self) -> Dict[str, Any]: - memory_file_path = 'memory.yaml' + 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: From 9de029bcdcd211ff0742a951b0d19d43899ed673 Mon Sep 17 00:00:00 2001 From: Alex Dimmock Date: Sun, 26 Oct 2025 19:21:14 +0000 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=E2=9C=A8=20Add=20memory=20expert?= =?UTF-8?q?=20model=20creation=20and=20integrate=20into=20orchestrator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/model_factory.py | 5 +++++ src/experts/memory.py | 41 +++++++++++++++++++++---------------- src/experts/orchestrator.py | 8 ++------ 3 files changed, 30 insertions(+), 24 deletions(-) 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 index d4b3dc4..601d1e9 100644 --- a/src/experts/memory.py +++ b/src/experts/memory.py @@ -4,21 +4,27 @@ import dspy -class MemoryExpertSignature(dspy.Signature): +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") + + 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""" + """A Lighting Agent that has access to Light based tools""" + def __init__(self): - self.memory_file_path = 'memory.yaml' + self.memory_file_path = "memory.yaml" self.tools = [ self.add_or_update_memory, self.load_memory, @@ -27,8 +33,7 @@ def __init__(self): self.load_all_memories, ] self.memory_agent = dspy.ReAct( - signature=MemoryExpertSignature, - tools=self.tools + signature=MemoryExpertSignature, tools=self.tools ) def add_or_update_memory(self, key: str, value: Any) -> bool: @@ -64,7 +69,7 @@ def load_memory(self, key: str) -> Optional[Any]: 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 """ @@ -75,10 +80,10 @@ def list_memory_keys(self) -> Optional[List[str]]: 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 """ @@ -88,27 +93,27 @@ def remove_memory(self, key: str) -> bool: 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 = 'memory.yaml' + memory_file_path = "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, "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: + 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 \ No newline at end of file + return False diff --git a/src/experts/orchestrator.py b/src/experts/orchestrator.py index 9944186..9bd875d 100644 --- a/src/experts/orchestrator.py +++ b/src/experts/orchestrator.py @@ -65,15 +65,11 @@ def consult_lighting_expert(self, command: str) -> str: result = LightingAgent().lighting_agent(command=command) return result.answer - def consult_memory_expert(self,command: str) -> str: + 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=dspy.LM( - model = f"{Config.Model.PROVIDER}/{Config.Model.EXPERT_MODEL_NAME}", - api_base=f"http://{Config.Model.HOST_ADDRESS}:{Config.Model.HOST_PORT}/v1/", - api_key=Config.Model.HOST_API_KEY, - )): + with dspy.context(lm=ModelFactory.create_memory_model()): result = MemoryAgent().memory_agent(command=command) return result.answer From 304f97acfbf93800339c94ed2fa0fadf5c11bf76 Mon Sep 17 00:00:00 2001 From: Jake Pullen Date: Sun, 26 Oct 2025 19:40:22 +0000 Subject: [PATCH 6/6] fixes --- src/config/user_config_template.py | 12 ++++++------ src/experts/orchestrator.py | 1 + src/main.py | 11 ++++++++++- 3 files changed, 17 insertions(+), 7 deletions(-) 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/experts/orchestrator.py b/src/experts/orchestrator.py index 9bd875d..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): 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