Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# personal config
config.yaml
memory.yaml

# Python-generated files
__pycache__/
Expand Down
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
12 changes: 6 additions & 6 deletions src/config/user_config_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
5 changes: 5 additions & 0 deletions src/core/model_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
119 changes: 119 additions & 0 deletions src/experts/memory.py
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions src/experts/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .game import GameAgent
from .lights import LightingAgent
from .weather import WeatherAgent
from .memory import MemoryAgent


class OrchestratorSignature(dspy.Signature):
Expand All @@ -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
Expand Down Expand Up @@ -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
11 changes: 10 additions & 1 deletion src/main.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading