How Nemori's Configuration System Works: A Complete Guide to MemoryConfig

Nemori manages its configuration through a single, central MemoryConfig dataclass that is validated once and shared thread‑safely across the entire library stack.

The nemori-ai/nemori repository implements a unified configuration system designed to balance simplicity for new users with fine‑grained control for advanced deployments. At its heart lies the MemoryConfig object, which aggregates every tunable setting—from LLM model names to storage paths and feature flags—and propagates these values to all subsystems.

The Central Configuration Object: MemoryConfig

Defining Configuration in src/config.py

The configuration schema lives in src/config.py as a Python dataclass. This design choice provides type safety, automatic attribute handling, and clear default values.

from dataclasses import dataclass, field
from typing import Optional
import os

@dataclass
class MemoryConfig:
    llm_model: str = "gpt-4o"
    embedding_model: str = "text-embedding-3-large"
    buffer_size_max: int = 10
    storage_path: str = "./memory_store"
    vector_index_backend: str = "chroma"
    enable_semantic_memory: bool = True
    enable_episode_merging: bool = True
    # ... additional fields

Default values are declared directly in the class definition, allowing users to instantiate a working system with MemoryConfig() and no arguments.

Environment Variable Integration

Sensitive credentials and deployment‑specific values are injected via environment variables at construction time. The dataclass uses default_factory to read from os.getenv only when the field is not explicitly provided:

@dataclass
class MemoryConfig:
    openai_api_key: Optional[str] = field(
        default_factory=lambda: os.getenv("OPENAI_API_KEY")
    )
    # Additional environment-driven fields follow the same pattern

This approach keeps secrets out of source code while maintaining the ability to override them programmatically when needed.

Validation and Error Handling

Validation logic resides in the __post_init__ method, ensuring that any MemoryConfig instance is internally consistent before it reaches downstream components:

def __post_init__(self):
    if not self.openai_api_key:
        raise ValueError("OPENAI_API_KEY must be set via environment variable or passed explicitly")
    if self.buffer_size_max < 1:
        raise ValueError("buffer_size_max must be positive")
    # Additional consistency checks...

Clear ValueError messages guide users to fix configuration issues immediately at startup.

Injecting Configuration Throughout the System

Core Memory System Initialization

The MemorySystem class in src/core/memory_system.py acts as the central engine. It accepts an optional MemoryConfig parameter and falls back to a default instance if none is provided:

class MemorySystem:
    def __init__(self, config: Optional[MemoryConfig] = None):
        self.config = config or MemoryConfig()
        # Subsequent initialization uses self.config...

This pattern ensures that every subsystem receives the same configuration object, maintaining consistency across the library.

Provider Factory Pattern

The DefaultProviders factory in src/services/providers.py translates configuration values into concrete implementations. It inspects the MemoryConfig to decide which storage backends and index types to instantiate:

class DefaultProviders:
    def __init__(self, config: MemoryConfig):
        self.config = config
        
        # Storage backend selection

        if config.storage_path:
            self.storage = FileSystemStorage(config.storage_path)
        else:
            self.storage = InMemoryStorage()
            
        # Vector index selection

        if config.vector_index_backend == "chroma":
            self.vector_index = ChromaIndex(config)
        else:
            self.vector_index = MemoryVectorIndex()
            
        # Feature flags control optional components

        if config.enable_semantic_memory:
            self.semantic_generator = SemanticGenerator(config)

This centralized factory ensures that feature flags like enable_semantic_memory or enable_batch_segmentation consistently enable or disable components across the stack.

Public API Facade

The user-facing NemoriMemory class in src/api/facade.py exposes the configuration system to end users. It accepts a MemoryConfig object and passes it to the internal MemorySystem:

class NemoriMemory:
    def __init__(self, config: Optional[MemoryConfig] = None):
        self._system = MemorySystem(config=config)
        
    @classmethod
    def from_env(cls):
        """Convenience constructor that creates config from environment variables."""
        return cls(config=MemoryConfig())

Users can instantiate the system in three ways: providing a custom config object, loading from a file via MemoryConfig.from_dict(), or using the from_env() convenience method.

Runtime Configuration Flexibility

Dynamic Component Loading

Because the same MemoryConfig instance is shared across components, any subsystem can read flags at runtime to adjust behavior. For example, the semantic generator checks self.config.enable_semantic_memory before executing operations.

The MemorySystem also supports dynamic reloading of user-specific indices based on configuration flags. The method load_user_data_and_indices_for_method in src/core/memory_system.py (lines 72-104) inspects the configuration to determine whether to load vector indices, BM25 lexical indices, or hybrid combinations.

Configuration Persistence for Experiments

For reproducible research and evaluation, Nemori supports serializing configurations to JSON or YAML. The MemoryConfig.from_dict() class method deserializes dictionaries into valid configuration objects.

Evaluation scripts such as scripts/init_workspace.py, evaluation/longmemeval/search.py, and evaluation/locomo/search.py demonstrate this pattern in production. They load JSON configuration files, deserialize them with MemoryConfig.from_dict(), and pass the resulting objects to the library. This ensures that experimental parameters are version-controlled and reproducible across different environments.

Practical Configuration Examples

Basic Environment-Driven Setup

from nemori import NemoriMemory

# Automatically reads OPENAI_API_KEY from environment

# Uses all default values for models and storage

memory = NemoriMemory()

Programmatic Configuration Override

from nemori import MemoryConfig, NemoriMemory

cfg = MemoryConfig(
    llm_model="gpt-4o-mini",
    embedding_model="text-embedding-3-small",
    buffer_size_max=30,          # Larger buffer before flushing to storage

    enable_episode_merging=False # Disable automatic episode consolidation

)

memory = NemoriMemory(config=cfg)

Loading from JSON Configuration File

import json
from pathlib import Path
from nemori import MemoryConfig, NemoriMemory

config_path = Path("experiments/config.json")
config_dict = json.loads(config_path.read_text())
cfg = MemoryConfig.from_dict(config_dict)

memory = NemoriMemory(config=cfg)

Explicit Environment Constructor

from nemori import NemoriMemory

# Explicitly indicates configuration comes from environment variables

memory = NemoriMemory.from_env()

Custom Provider Accessing Configuration

from nemori.config import MemoryConfig

class CustomStorageProvider:
    def __init__(self, config: MemoryConfig):
        # Access configuration values directly

        self.persistence_path = config.storage_path
        self.vector_backend = config.vector_index_backend
        
        # Check feature flags

        if config.enable_semantic_memory:
            self.init_semantic_layer()

Summary

  • Centralized Configuration: Nemori uses a single MemoryConfig dataclass in src/config.py as the immutable source of truth for all library settings.

  • Environment Integration: Sensitive values like OPENAI_API_KEY are automatically read from environment variables via default_factory lambdas, keeping credentials out of source code.

  • Validation at Construction: The __post_init__ method ensures configuration consistency immediately, raising clear ValueError messages for missing API keys or invalid parameters.

  • Dependency Injection: The MemorySystem class in src/core/memory_system.py accepts the configuration object and propagates it to all subsystems, while DefaultProviders in src/services/providers.py uses it to select concrete implementations based on feature flags.

  • Flexible Instantiation: Users can configure Nemori programmatically, load settings from JSON/YAML files via MemoryConfig.from_dict(), or use the NemoriMemory.from_env() convenience method.

Frequently Asked Questions

How does Nemori handle sensitive configuration like API keys?

Nemori reads sensitive values from environment variables at configuration construction time. In src/config.py, the MemoryConfig dataclass uses field(default_factory=lambda: os.getenv("OPENAI_API_KEY")) to inject the OpenAI API key without hardcoding it. If the variable is missing, the __post_init__ validation raises a ValueError with a clear message indicating the required environment variable.

Can I change Nemori's configuration after initializing the memory system?

No, the MemoryConfig object is designed to be immutable once constructed. The MemorySystem in src/core/memory_system.py stores a reference to the configuration instance during initialization and shares it across all providers. While you cannot modify the configuration object itself after creation, you can create a new MemoryConfig instance with different parameters and initialize a separate NemoriMemory object if you need different behavior.

What is the difference between using NemoriMemory() and NemoriMemory.from_env()?

Both methods ultimately create a memory system configured via environment variables, but from_env() is an explicit convenience constructor. When you call NemoriMemory(), the underlying MemorySystem creates a default MemoryConfig() which automatically reads OPENAI_API_KEY from the environment. The from_env() classmethod in src/api/facade.py makes this behavior explicit in your code, signaling to other developers that the configuration is entirely environment-driven rather than programmatically specified.

How do I switch between different storage backends in Nemori?

You control the storage backend via the MemoryConfig dataclass parameters before initializing the system. In src/services/providers.py, the DefaultProviders factory inspects config.storage_path and config.vector_index_backend to decide between filesystem versus in-memory storage, and Chroma versus memory vector indices. To use a specific backend, set the appropriate fields when creating your configuration object, such as MemoryConfig(storage_path="./data", vector_index_backend="chroma"), then pass this config to NemoriMemory(config=cfg).

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →