Merge pull request #2819 from wayyoungboy/add-powermem

Add powermem
This commit is contained in:
Sakura-RanChen
2026-01-08 17:54:57 +08:00
committed by GitHub
9 changed files with 659 additions and 4 deletions
+29
View File
@@ -275,6 +275,35 @@ Memory:
# https://app.mem0.ai/dashboard/api-keys
# 每月有1000次免费调用
api_key: 你的mem0ai api key
powermem:
# PowerMem是OceanBase开源的agent记忆组件,通过本地LLM进行记忆总结
# 费用说明:PowerMem本身免费,实际费用取决于所选LLM和数据库
# - 使用sqlite + 免费LLM(如glm-4-flash) = 完全免费
# - 使用云端LLM或云端数据库 = 按对应服务收费
# GitHub: https://github.com/oceanbase/powermem
# 官网: https://www.powermem.ai/
# 使用示例: https://github.com/oceanbase/powermem/tree/main/examples
type: powermem
# 是否启用用户画像功能(需要OceanBase作为存储后端)
# - false: 使用普通记忆模式(AsyncMemory),支持所有数据库
# - true: 使用用户画像模式(UserMemory),自动提取用户信息,仅支持OceanBase
enable_user_profile: false
# 数据库提供者: oceanbase(推荐,最佳性能), seekdb(推荐,AI应用存储一体), postgres, sqlite(轻量备选)
# 资源充足时建议使用 oceanbase 或 seekdb
# 注意:用户画像功能(enable_user_profile: true)仅支持oceanbase
database_provider: sqlite
# LLM提供者: qwen(默认), zhipu(免费glm-4-flash), openai, 等
llm_provider: qwen
# 嵌入模型提供者: qwen(默认), zhipu, openai, 等
embedding_provider: qwen
# LLM配置 (使用通义千问为例)
# 通义千问: https://bailian.console.aliyun.com/?apiKey=1#/api-key
# 智谱AI(免费): https://bigmodel.cn/usercenter/proj-mgmt/apikeys
llm_api_key: 你的LLM API密钥
llm_model: qwen-plus
# 嵌入模型配置 (使用通义千问为例)
embedding_api_key: 你的嵌入模型API密钥
embedding_model: text-embedding-v3
nomem:
# 不想使用记忆功能,可以使用nomem
type: nomem
@@ -0,0 +1,299 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
@time: 2026/01/08
@file: powermem.py
@desc: PowerMem memory provider for xiaozhi-esp32-server
PowerMem is an open-source agent memory component from OceanBase
GitHub: https://github.com/oceanbase/powermem
Website: https://www.powermem.ai/
@Author: wayyoungboy
"""
import traceback
from typing import Optional, Dict, Any
from ..base import MemoryProviderBase, logger
TAG = __name__
class MemoryProvider(MemoryProviderBase):
"""
PowerMem memory provider implementation.
PowerMem is an open-source agent memory component that provides
efficient memory management for AI agents.
Supports multiple storage backends (sqlite, oceanbase, postgres),
LLM providers (qwen, openai, etc.) and embedding providers.
Config options:
- enable_user_profile: bool - Enable UserMemory for user profiling (requires OceanBase)
- database_provider: str - Storage backend (sqlite, oceanbase, postgres)
- llm_provider: str - LLM provider (qwen, openai, etc.)
- embedding_provider: str - Embedding provider (qwen, openai, etc.)
"""
def __init__(self, config: Dict[str, Any], summary_memory: Optional[str] = None):
super().__init__(config)
self.use_powermem = False
self.memory_client = None
self.enable_user_profile = False
try:
# Check if user profile mode is enabled
self.enable_user_profile = config.get("enable_user_profile", False)
# Get configuration parameters
database_provider = config.get("database_provider", "sqlite")
llm_provider = config.get("llm_provider", "qwen")
embedding_provider = config.get("embedding_provider", "qwen")
# UserMemory requires OceanBase
if self.enable_user_profile and database_provider not in ["oceanbase"]:
logger.bind(tag=TAG).warning(
f"UserMemory requires OceanBase as storage backend, but got {database_provider}. "
"Falling back to AsyncMemory mode."
)
self.enable_user_profile = False
# Build powermem configuration dict
# PowerMem supports two config styles:
# 1. powermem style: database, llm, embedding
# 2. mem0 style: vector_store, llm, embedder
powermem_config = {}
# Configure vector store / database
if "vector_store" in config:
powermem_config["vector_store"] = config["vector_store"]
elif "database" in config:
powermem_config["database"] = config["database"]
else:
powermem_config["vector_store"] = {
"provider": database_provider,
"config": {}
}
# Configure LLM
if "llm" in config:
powermem_config["llm"] = config["llm"]
else:
llm_config = {}
if "llm_api_key" in config:
llm_config["api_key"] = config["llm_api_key"]
if "llm_model" in config:
llm_config["model"] = config["llm_model"]
if "llm_base_url" in config:
llm_config["base_url"] = config["llm_base_url"]
powermem_config["llm"] = {
"provider": llm_provider,
"config": llm_config
}
# Configure embedder
if "embedder" in config:
powermem_config["embedder"] = config["embedder"]
else:
embedder_config = {}
if "embedding_api_key" in config:
embedder_config["api_key"] = config["embedding_api_key"]
if "embedding_model" in config:
embedder_config["model"] = config["embedding_model"]
if "embedding_base_url" in config:
embedder_config["base_url"] = config["embedding_base_url"]
powermem_config["embedder"] = {
"provider": embedding_provider,
"config": embedder_config
}
# Initialize memory client based on mode
if self.enable_user_profile:
from powermem import UserMemory
self.memory_client = UserMemory(config=powermem_config)
memory_mode = "UserMemory (用户画像模式)"
else:
from powermem import AsyncMemory
self.memory_client = AsyncMemory(config=powermem_config)
memory_mode = "AsyncMemory (普通记忆模式)"
self.use_powermem = True
logger.bind(tag=TAG).info(
f"PowerMem initialized successfully: mode={memory_mode}, "
f"database={database_provider}, llm={llm_provider}, embedding={embedding_provider}"
)
except ImportError as e:
logger.bind(tag=TAG).error(
f"PowerMem not installed. Please install with: pip install powermem. Error: {e}"
)
self.use_powermem = False
except Exception as e:
logger.bind(tag=TAG).error(f"Failed to initialize PowerMem: {str(e)}")
logger.bind(tag=TAG).debug(f"Detailed error: {traceback.format_exc()}")
self.use_powermem = False
async def save_memory(self, msgs):
"""
Save conversation messages to PowerMem.
Args:
msgs: List of message objects with 'role' and 'content' attributes
Returns:
Result from PowerMem API or None if failed
"""
if not self.use_powermem or self.memory_client is None:
logger.bind(tag=TAG).warning("PowerMem is not available, skipping save_memory")
return None
if len(msgs) < 2:
logger.bind(tag=TAG).debug("Not enough messages to save (need at least 2)")
return None
try:
# Format the content as a message list for PowerMem
messages = [
{"role": message.role, "content": message.content}
for message in msgs
if message.role != "system"
]
# Add memory using PowerMem SDK
result = await self.memory_client.add(
messages=messages,
user_id=self.role_id
)
logger.bind(tag=TAG).debug(f"Save memory result: {result}")
return result
except Exception as e:
logger.bind(tag=TAG).error(f"Error saving memory: {str(e)}")
logger.bind(tag=TAG).debug(f"Detailed error: {traceback.format_exc()}")
return None
async def query_memory(self, query: str) -> str:
"""
Query memories from PowerMem based on similarity search.
Args:
query: The search query string
Returns:
Formatted string of relevant memories or empty string if none found
"""
if not self.use_powermem or self.memory_client is None:
logger.bind(tag=TAG).warning("PowerMem is not available, skipping query_memory")
return ""
try:
if not getattr(self, "role_id", None):
logger.bind(tag=TAG).debug("No role_id set, returning empty memory")
return ""
result_parts = []
# If user profile mode is enabled, include user profile in results
if self.enable_user_profile:
profile = await self.get_user_profile()
if profile:
result_parts.append(f"【用户画像】\n{profile}")
# Search memories using PowerMem SDK
results = await self.memory_client.search(
query=query,
user_id=self.role_id,
limit=30
)
if results and "results" in results:
# Format each memory entry with its update time
memories = []
for entry in results.get("results", []):
# Get timestamp from updated_at or created_at
timestamp = ""
if "updated_at" in entry and entry["updated_at"]:
timestamp = str(entry["updated_at"])
elif "created_at" in entry and entry["created_at"]:
timestamp = str(entry["created_at"])
if timestamp:
try:
# Parse and reformat the timestamp (remove milliseconds if present)
if "." in timestamp:
dt = timestamp.split(".")[0]
else:
dt = timestamp
formatted_time = dt.replace("T", " ")
except Exception:
formatted_time = timestamp
else:
formatted_time = ""
memory = entry.get("memory", "") or entry.get("content", "")
if memory:
if formatted_time:
# Store tuple of (timestamp, formatted_string) for sorting
memories.append((timestamp, f"[{formatted_time}] {memory}"))
else:
memories.append(("", memory))
# Sort by timestamp in descending order (newest first)
memories.sort(key=lambda x: x[0], reverse=True)
# Extract only the formatted strings
if memories:
memories_str = "\n".join(f"- {memory[1]}" for memory in memories)
result_parts.append(f"【相关记忆】\n{memories_str}")
final_result = "\n\n".join(result_parts)
logger.bind(tag=TAG).debug(f"Query results: {final_result}")
return final_result
except Exception as e:
logger.bind(tag=TAG).error(f"Error querying memory: {str(e)}")
logger.bind(tag=TAG).debug(f"Detailed error: {traceback.format_exc()}")
return ""
async def get_user_profile(self) -> str:
"""
Get user profile from PowerMem (only available in UserMemory mode).
Returns:
Formatted user profile string or empty string if not available
"""
if not self.use_powermem or self.memory_client is None:
return ""
if not self.enable_user_profile:
logger.bind(tag=TAG).debug("User profile mode is not enabled")
return ""
try:
if not getattr(self, "role_id", None):
return ""
# Get user profile using UserMemory SDK
profile = await self.memory_client.get_profile(user_id=self.role_id)
if not profile:
return ""
# Format profile as readable string
profile_parts = []
for key, value in profile.items():
if value:
profile_parts.append(f"- {key}: {value}")
return "\n".join(profile_parts)
except Exception as e:
logger.bind(tag=TAG).error(f"Error getting user profile: {str(e)}")
logger.bind(tag=TAG).debug(f"Detailed error: {traceback.format_exc()}")
return ""
# Register the memory provider instance
powermem = MemoryProvider({})
+1
View File
@@ -23,6 +23,7 @@ loguru==0.7.3
requests==2.32.5
cozepy==0.20.0
mem0ai==1.0.0
powermem>=0.2.1
bs4==0.0.2
modelscope==1.32.0
sherpa_onnx==1.12.17