From 5ce68909393480f156098f0f41be8b653f7166c2 Mon Sep 17 00:00:00 2001 From: Teingi Date: Fri, 28 Nov 2025 14:40:25 +0800 Subject: [PATCH 1/2] fixed: EMBEDDING_PROVIDER=mock --- docs/guides/ebbinghaus_automatic_usage.md | 221 ++++++++++++++++++ examples/agent_memory.py | 2 +- examples/basic_usage.py | 4 +- examples/intelligent_memory_demo.py | 2 +- src/powermem/core/async_memory.py | 7 +- src/powermem/core/memory.py | 7 +- .../integrations/embeddings/factory.py | 33 ++- src/powermem/integrations/embeddings/mock.py | 22 +- 8 files changed, 285 insertions(+), 13 deletions(-) create mode 100644 docs/guides/ebbinghaus_automatic_usage.md diff --git a/docs/guides/ebbinghaus_automatic_usage.md b/docs/guides/ebbinghaus_automatic_usage.md new file mode 100644 index 0000000..83db42e --- /dev/null +++ b/docs/guides/ebbinghaus_automatic_usage.md @@ -0,0 +1,221 @@ +# Ebbinghaus Forgetting Curve - Automatic Usage Explained + +## Overview + +This document clarifies how the Ebbinghaus forgetting curve is automatically applied in PowerMem's intelligent memory system, addressing common confusion about whether manual calculation is required. + +## Key Answer: It's Automatic! + +**The Ebbinghaus forgetting curve is automatically applied to search results when intelligent memory is enabled.** You do NOT need to manually calculate decay or sort results yourself. + +## How It Works Automatically + +### 1. Configuration Setup + +When you configure your `.env` file with: + +```env +MEMORY_DECAY_ENABLED=true +MEMORY_DECAY_ALGORITHM=ebbinghaus +MEMORY_DECAY_BASE_RETENTION=1.0 +MEMORY_DECAY_FORGETTING_RATE=0.1 +MEMORY_DECAY_REINFORCEMENT_FACTOR=0.3 +``` + +**Note:** These `MEMORY_DECAY_*` variables are legacy names. The current implementation uses `INTELLIGENT_MEMORY_*` variables. However, both work if properly mapped in your configuration. + +The recommended `.env` configuration is: + +```env +INTELLIGENT_MEMORY_ENABLED=true +INTELLIGENT_MEMORY_DECAY_RATE=0.1 +INTELLIGENT_MEMORY_INITIAL_RETENTION=1.0 +INTELLIGENT_MEMORY_REINFORCEMENT_FACTOR=0.3 +``` + +### 2. Automatic Application Flow + +When you call `memory.search()`, the following happens automatically: + +``` +1. User calls: memory.search(query="...") + ↓ +2. Memory.search() performs vector similarity search + ↓ +3. If intelligent_memory.enabled == True: + ↓ +4. IntelligenceManager.process_search_results() is called + ↓ +5. IntelligentMemoryManager.process_search_results() applies: + - Calculates relevance_score for each result + - Calculates decay_factor using Ebbinghaus formula + - Computes final_score = relevance_score × decay_factor + - Re-sorts results by final_score (descending) + ↓ +6. Returns automatically sorted results +``` + +### 3. Code Implementation + +The automatic decay is implemented in `IntelligentMemoryManager.process_search_results()`: + +```python +def process_search_results(self, results: List[Dict[str, Any]], query: str): + processed_results = [] + for result in results: + # Calculate relevance score + relevance_score = self.ebbinghaus_algorithm.calculate_relevance(result, query) + + # Apply decay based on age (AUTOMATIC) + decay_factor = self.ebbinghaus_algorithm.calculate_decay( + result.get("created_at", datetime.utcnow()) + ) + + # Combine scores + processed_result = result.copy() + processed_result["relevance_score"] = relevance_score + processed_result["decay_factor"] = decay_factor + processed_result["final_score"] = relevance_score * decay_factor + + processed_results.append(processed_result) + + # AUTOMATICALLY sort by final_score + processed_results.sort(key=lambda x: x["final_score"], reverse=True) + + return processed_results +``` + +This is called automatically in `Memory.search()`: + +```python +def search(self, query: str, ...): + # ... perform vector search ... + + # AUTOMATIC processing if intelligent memory enabled + if self.intelligence.enabled: + processed_results = self.intelligence.process_search_results(results, query) + else: + processed_results = results + + return {"results": processed_results} +``` + +## What You DON'T Need to Do + +❌ **You do NOT need to:** +- Manually calculate decay factors after search +- Manually sort results by retention scores +- Implement your own forgetting curve logic +- Call decay calculation functions yourself + +✅ **You just need to:** +- Enable intelligent memory in config +- Call `memory.search()` normally +- Results are automatically ranked by combined score (similarity × retention) + +## When Manual Calculation is Shown in Documentation + +The documentation examples showing manual calculation (like in `scenario_8_ebbinghaus_forgetting_curve.md`) are for: + +1. **Educational purposes**: Understanding how the curve works +2. **Custom use cases**: When you want to build your own ranking logic +3. **Analysis**: When you want to analyze retention scores separately +4. **Visualization**: Creating charts of the forgetting curve + +These are **optional** and **not required** for normal usage. + +## Verification: Is It Working? + +To verify that automatic decay is working: + +### Method 1: Check Configuration + +```python +from powermem import Memory, auto_config + +config = auto_config() +memory = Memory(config=config) + +# Check if intelligent memory is enabled +print(f"Intelligent memory enabled: {memory.intelligence.enabled}") +print(f"Intelligent memory manager: {memory.intelligence.intelligent_memory_manager is not None}") +``` + +### Method 2: Inspect Search Results + +```python +results = memory.search(query="your query", user_id="user1") + +# Check if results have decay_factor and final_score +for result in results.get("results", [])[:3]: + print(f"Score: {result.get('score')}") + print(f"Decay factor: {result.get('decay_factor', 'N/A')}") + print(f"Final score: {result.get('final_score', 'N/A')}") + print("---") +``` + +If you see `decay_factor` and `final_score` in results, automatic decay is working! + +### Method 3: Compare Old vs New Memories + +```python +# Add an old memory (simulated) +old_memory = memory.add( + messages="This is an old memory", + user_id="user1", + metadata={"created_at": (datetime.now() - timedelta(days=30)).isoformat()} +) + +# Add a new memory +new_memory = memory.add( + messages="This is a new memory", + user_id="user1" +) + +# Search - new memory should rank higher even with same similarity +results = memory.search(query="memory", user_id="user1") +# New memory should appear first due to higher retention +``` + +## Configuration Mapping + +### Legacy Variables (MEMORY_DECAY_*) + +If you're using `MEMORY_DECAY_*` variables, they need to be mapped to `intelligent_memory` config: + +```python +# Manual mapping if needed +config = { + "intelligent_memory": { + "enabled": os.getenv("MEMORY_DECAY_ENABLED", "true").lower() == "true", + "decay_rate": float(os.getenv("MEMORY_DECAY_FORGETTING_RATE", "0.1")), + "initial_retention": float(os.getenv("MEMORY_DECAY_BASE_RETENTION", "1.0")), + "reinforcement_factor": float(os.getenv("MEMORY_DECAY_REINFORCEMENT_FACTOR", "0.3")), + } +} +``` + +### Recommended Variables (INTELLIGENT_MEMORY_*) + +Use these in your `.env` file: + +```env +INTELLIGENT_MEMORY_ENABLED=true +INTELLIGENT_MEMORY_DECAY_RATE=0.1 +INTELLIGENT_MEMORY_INITIAL_RETENTION=1.0 +INTELLIGENT_MEMORY_REINFORCEMENT_FACTOR=0.3 +INTELLIGENT_MEMORY_WORKING_THRESHOLD=0.3 +INTELLIGENT_MEMORY_SHORT_TERM_THRESHOLD=0.6 +INTELLIGENT_MEMORY_LONG_TERM_THRESHOLD=0.8 +``` + +## Summary + +1. **Automatic**: Ebbinghaus decay is automatically applied to search results +2. **No manual work**: You don't need to calculate or sort manually +3. **Configuration**: Just enable `INTELLIGENT_MEMORY_ENABLED=true` +4. **Transparent**: Results are automatically re-ranked by `final_score = similarity × retention` +5. **Documentation examples**: Manual calculation examples are for learning, not required usage + +The forgetting curve logic is **already included by default** when intelligent memory is enabled! + diff --git a/examples/agent_memory.py b/examples/agent_memory.py index 23df492..0234c57 100644 --- a/examples/agent_memory.py +++ b/examples/agent_memory.py @@ -30,7 +30,7 @@ def load_oceanbase_config(): Uses the auto_config() utility function to automatically load from .env. """ - oceanbase_env_path = os.path.join(os.path.dirname(__file__), '..', 'configs', '.env') + oceanbase_env_path = os.path.join(os.path.dirname(__file__), '..', '.env') if os.path.exists(oceanbase_env_path): load_dotenv(oceanbase_env_path, override=True) diff --git a/examples/basic_usage.py b/examples/basic_usage.py index cdd6ec7..f941a48 100644 --- a/examples/basic_usage.py +++ b/examples/basic_usage.py @@ -23,8 +23,8 @@ def main(): print("=" * 60) # Check if .env exists and load it - env_path = os.path.join(os.path.dirname(__file__), "..", "configs", ".env") - env_example_path = os.path.join(os.path.dirname(__file__), "..", "configs", "env.example") + env_path = os.path.join(os.path.dirname(__file__), "..", ".env") + env_example_path = os.path.join(os.path.dirname(__file__), "..", "env.example") if not os.path.exists(env_path): print(f"\n No .env file found at: {env_path}") diff --git a/examples/intelligent_memory_demo.py b/examples/intelligent_memory_demo.py index 90fff6d..a3fa38e 100644 --- a/examples/intelligent_memory_demo.py +++ b/examples/intelligent_memory_demo.py @@ -38,7 +38,7 @@ def load_config(): Uses the auto_config() utility function to automatically load from .env. """ - oceanbase_env_path = os.path.join(os.path.dirname(__file__), '..', 'configs', '.env') + oceanbase_env_path = os.path.join(os.path.dirname(__file__), '..', '.env') if os.path.exists(oceanbase_env_path): load_dotenv(oceanbase_env_path, override=True) diff --git a/src/powermem/core/async_memory.py b/src/powermem/core/async_memory.py index 866460b..cdea599 100644 --- a/src/powermem/core/async_memory.py +++ b/src/powermem/core/async_memory.py @@ -159,7 +159,8 @@ def __init__( # Extract embedder config embedder_config = self._get_component_config('embedder') - self.embedding = EmbedderFactory.create(self.embedding_provider, embedder_config, None) + # Pass vector_store_config so factory can extract embedding_model_dims for mock embeddings + self.embedding = EmbedderFactory.create(self.embedding_provider, embedder_config, vector_store_config) # Initialize storage adapter with embedding service # Automatically select adapter based on sub_stores configuration @@ -1339,10 +1340,12 @@ def _init_single_sub_store( if key not in sub_embedding_params and key in main_embedding_config: sub_embedding_params[key] = main_embedding_config[key] + # Create a config dict with embedding_model_dims for mock embeddings + sub_vector_config = {'embedding_model_dims': embedding_model_dims} sub_embedding = EmbedderFactory.create( sub_embedding_provider, sub_embedding_params, - None + sub_vector_config ) logger.info(f"Created sub embedding service for store {index}: {sub_embedding_provider}") else: diff --git a/src/powermem/core/memory.py b/src/powermem/core/memory.py index 69f2aab..c07e41b 100644 --- a/src/powermem/core/memory.py +++ b/src/powermem/core/memory.py @@ -228,7 +228,8 @@ def __init__( # Extract embedder config embedder_config = self._get_component_config('embedder') - self.embedding = EmbedderFactory.create(self.embedding_provider, embedder_config, None) + # Pass vector_store_config so factory can extract embedding_model_dims for mock embeddings + self.embedding = EmbedderFactory.create(self.embedding_provider, embedder_config, vector_store_config) # Initialize storage adapter with embedding service # Automatically select adapter based on sub_stores configuration @@ -1404,10 +1405,12 @@ def _init_single_sub_store( if key not in sub_embedding_params and key in main_embedding_config: sub_embedding_params[key] = main_embedding_config[key] + # Create a config dict with embedding_model_dims for mock embeddings + sub_vector_config = {'embedding_model_dims': embedding_model_dims} sub_embedding = EmbedderFactory.create( sub_embedding_provider, sub_embedding_params, - None + sub_vector_config ) logger.info(f"Created sub embedding service for store {index}: {sub_embedding_provider}") else: diff --git a/src/powermem/integrations/embeddings/factory.py b/src/powermem/integrations/embeddings/factory.py index e065476..8d6018f 100644 --- a/src/powermem/integrations/embeddings/factory.py +++ b/src/powermem/integrations/embeddings/factory.py @@ -34,11 +34,38 @@ class EmbedderFactory: @classmethod def create(cls, provider_name, config, vector_config: Optional[dict]): + # Helper function to extract dimension from vector_config (handles both dict and object) + def get_dimension_from_vector_config(vector_config, default=1536): + if not vector_config: + return default + if isinstance(vector_config, dict): + return vector_config.get('embedding_model_dims', default) + else: + return getattr(vector_config, 'embedding_model_dims', default) + # Handle mock provider directly if provider_name == "mock": - return MockEmbeddings() - if provider_name == "upstash_vector" and vector_config and vector_config.enable_embeddings: - return MockEmbeddings() + # Extract dimension from vector_config or embedder config, default to 1536 + dimension = 1536 # Default dimension + dimension = get_dimension_from_vector_config(vector_config, dimension) + if config: + dimension = config.get('embedding_dims', dimension) + return MockEmbeddings(dimension=dimension) + if provider_name == "upstash_vector" and vector_config: + # Check enable_embeddings (handles both dict and object) + enable_embeddings = False + if isinstance(vector_config, dict): + enable_embeddings = vector_config.get('enable_embeddings', False) + else: + enable_embeddings = getattr(vector_config, 'enable_embeddings', False) + + if enable_embeddings: + # Extract dimension from vector_config or embedder config, default to 1536 + dimension = 1536 # Default dimension + dimension = get_dimension_from_vector_config(vector_config, dimension) + if config: + dimension = config.get('embedding_dims', dimension) + return MockEmbeddings(dimension=dimension) class_type = cls.provider_to_class.get(provider_name) if class_type: embedder_instance = load_class(class_type) diff --git a/src/powermem/integrations/embeddings/mock.py b/src/powermem/integrations/embeddings/mock.py index 56a9e4b..941b993 100644 --- a/src/powermem/integrations/embeddings/mock.py +++ b/src/powermem/integrations/embeddings/mock.py @@ -4,8 +4,26 @@ class MockEmbeddings(EmbeddingBase): + def __init__(self, dimension: int = 1536): + """ + Initialize MockEmbeddings with specified dimension. + + Args: + dimension: Dimension of the mock embedding vector. Defaults to 1536 to match + common embedding models and OceanBase default. + """ + self.dimension = dimension + def embed(self, text, memory_action: Optional[Literal["add", "search", "update"]] = None): """ - Generate a mock embedding with dimension of 10. + Generate a mock embedding with the configured dimension. + + Returns a vector with values [0.1, 0.2, 0.3, ...] repeated to fill the dimension. """ - return [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] + # Generate a simple pattern that repeats to fill the dimension + base_values = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] + # Repeat the pattern to fill the required dimension + result = [] + for i in range(self.dimension): + result.append(base_values[i % len(base_values)]) + return result From 6f0410247dcc88a6175f7f8822f44b81058f0f3e Mon Sep 17 00:00:00 2001 From: Teingi Date: Fri, 28 Nov 2025 14:40:55 +0800 Subject: [PATCH 2/2] fixed: EMBEDDING_PROVIDER=mock --- docs/guides/ebbinghaus_automatic_usage.md | 221 ---------------------- 1 file changed, 221 deletions(-) delete mode 100644 docs/guides/ebbinghaus_automatic_usage.md diff --git a/docs/guides/ebbinghaus_automatic_usage.md b/docs/guides/ebbinghaus_automatic_usage.md deleted file mode 100644 index 83db42e..0000000 --- a/docs/guides/ebbinghaus_automatic_usage.md +++ /dev/null @@ -1,221 +0,0 @@ -# Ebbinghaus Forgetting Curve - Automatic Usage Explained - -## Overview - -This document clarifies how the Ebbinghaus forgetting curve is automatically applied in PowerMem's intelligent memory system, addressing common confusion about whether manual calculation is required. - -## Key Answer: It's Automatic! - -**The Ebbinghaus forgetting curve is automatically applied to search results when intelligent memory is enabled.** You do NOT need to manually calculate decay or sort results yourself. - -## How It Works Automatically - -### 1. Configuration Setup - -When you configure your `.env` file with: - -```env -MEMORY_DECAY_ENABLED=true -MEMORY_DECAY_ALGORITHM=ebbinghaus -MEMORY_DECAY_BASE_RETENTION=1.0 -MEMORY_DECAY_FORGETTING_RATE=0.1 -MEMORY_DECAY_REINFORCEMENT_FACTOR=0.3 -``` - -**Note:** These `MEMORY_DECAY_*` variables are legacy names. The current implementation uses `INTELLIGENT_MEMORY_*` variables. However, both work if properly mapped in your configuration. - -The recommended `.env` configuration is: - -```env -INTELLIGENT_MEMORY_ENABLED=true -INTELLIGENT_MEMORY_DECAY_RATE=0.1 -INTELLIGENT_MEMORY_INITIAL_RETENTION=1.0 -INTELLIGENT_MEMORY_REINFORCEMENT_FACTOR=0.3 -``` - -### 2. Automatic Application Flow - -When you call `memory.search()`, the following happens automatically: - -``` -1. User calls: memory.search(query="...") - ↓ -2. Memory.search() performs vector similarity search - ↓ -3. If intelligent_memory.enabled == True: - ↓ -4. IntelligenceManager.process_search_results() is called - ↓ -5. IntelligentMemoryManager.process_search_results() applies: - - Calculates relevance_score for each result - - Calculates decay_factor using Ebbinghaus formula - - Computes final_score = relevance_score × decay_factor - - Re-sorts results by final_score (descending) - ↓ -6. Returns automatically sorted results -``` - -### 3. Code Implementation - -The automatic decay is implemented in `IntelligentMemoryManager.process_search_results()`: - -```python -def process_search_results(self, results: List[Dict[str, Any]], query: str): - processed_results = [] - for result in results: - # Calculate relevance score - relevance_score = self.ebbinghaus_algorithm.calculate_relevance(result, query) - - # Apply decay based on age (AUTOMATIC) - decay_factor = self.ebbinghaus_algorithm.calculate_decay( - result.get("created_at", datetime.utcnow()) - ) - - # Combine scores - processed_result = result.copy() - processed_result["relevance_score"] = relevance_score - processed_result["decay_factor"] = decay_factor - processed_result["final_score"] = relevance_score * decay_factor - - processed_results.append(processed_result) - - # AUTOMATICALLY sort by final_score - processed_results.sort(key=lambda x: x["final_score"], reverse=True) - - return processed_results -``` - -This is called automatically in `Memory.search()`: - -```python -def search(self, query: str, ...): - # ... perform vector search ... - - # AUTOMATIC processing if intelligent memory enabled - if self.intelligence.enabled: - processed_results = self.intelligence.process_search_results(results, query) - else: - processed_results = results - - return {"results": processed_results} -``` - -## What You DON'T Need to Do - -❌ **You do NOT need to:** -- Manually calculate decay factors after search -- Manually sort results by retention scores -- Implement your own forgetting curve logic -- Call decay calculation functions yourself - -✅ **You just need to:** -- Enable intelligent memory in config -- Call `memory.search()` normally -- Results are automatically ranked by combined score (similarity × retention) - -## When Manual Calculation is Shown in Documentation - -The documentation examples showing manual calculation (like in `scenario_8_ebbinghaus_forgetting_curve.md`) are for: - -1. **Educational purposes**: Understanding how the curve works -2. **Custom use cases**: When you want to build your own ranking logic -3. **Analysis**: When you want to analyze retention scores separately -4. **Visualization**: Creating charts of the forgetting curve - -These are **optional** and **not required** for normal usage. - -## Verification: Is It Working? - -To verify that automatic decay is working: - -### Method 1: Check Configuration - -```python -from powermem import Memory, auto_config - -config = auto_config() -memory = Memory(config=config) - -# Check if intelligent memory is enabled -print(f"Intelligent memory enabled: {memory.intelligence.enabled}") -print(f"Intelligent memory manager: {memory.intelligence.intelligent_memory_manager is not None}") -``` - -### Method 2: Inspect Search Results - -```python -results = memory.search(query="your query", user_id="user1") - -# Check if results have decay_factor and final_score -for result in results.get("results", [])[:3]: - print(f"Score: {result.get('score')}") - print(f"Decay factor: {result.get('decay_factor', 'N/A')}") - print(f"Final score: {result.get('final_score', 'N/A')}") - print("---") -``` - -If you see `decay_factor` and `final_score` in results, automatic decay is working! - -### Method 3: Compare Old vs New Memories - -```python -# Add an old memory (simulated) -old_memory = memory.add( - messages="This is an old memory", - user_id="user1", - metadata={"created_at": (datetime.now() - timedelta(days=30)).isoformat()} -) - -# Add a new memory -new_memory = memory.add( - messages="This is a new memory", - user_id="user1" -) - -# Search - new memory should rank higher even with same similarity -results = memory.search(query="memory", user_id="user1") -# New memory should appear first due to higher retention -``` - -## Configuration Mapping - -### Legacy Variables (MEMORY_DECAY_*) - -If you're using `MEMORY_DECAY_*` variables, they need to be mapped to `intelligent_memory` config: - -```python -# Manual mapping if needed -config = { - "intelligent_memory": { - "enabled": os.getenv("MEMORY_DECAY_ENABLED", "true").lower() == "true", - "decay_rate": float(os.getenv("MEMORY_DECAY_FORGETTING_RATE", "0.1")), - "initial_retention": float(os.getenv("MEMORY_DECAY_BASE_RETENTION", "1.0")), - "reinforcement_factor": float(os.getenv("MEMORY_DECAY_REINFORCEMENT_FACTOR", "0.3")), - } -} -``` - -### Recommended Variables (INTELLIGENT_MEMORY_*) - -Use these in your `.env` file: - -```env -INTELLIGENT_MEMORY_ENABLED=true -INTELLIGENT_MEMORY_DECAY_RATE=0.1 -INTELLIGENT_MEMORY_INITIAL_RETENTION=1.0 -INTELLIGENT_MEMORY_REINFORCEMENT_FACTOR=0.3 -INTELLIGENT_MEMORY_WORKING_THRESHOLD=0.3 -INTELLIGENT_MEMORY_SHORT_TERM_THRESHOLD=0.6 -INTELLIGENT_MEMORY_LONG_TERM_THRESHOLD=0.8 -``` - -## Summary - -1. **Automatic**: Ebbinghaus decay is automatically applied to search results -2. **No manual work**: You don't need to calculate or sort manually -3. **Configuration**: Just enable `INTELLIGENT_MEMORY_ENABLED=true` -4. **Transparent**: Results are automatically re-ranked by `final_score = similarity × retention` -5. **Documentation examples**: Manual calculation examples are for learning, not required usage - -The forgetting curve logic is **already included by default** when intelligent memory is enabled! -