From dadf05ab4ee99f616a62aef1388588be2f8fac10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=A0=E7=A3=8A?= Date: Thu, 8 Jan 2026 12:02:03 +0800 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20PowerMem=20?= =?UTF-8?q?=E6=99=BA=E8=83=BD=E8=AE=B0=E5=BF=86=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 PowerMem 配置选项和集成文档 - 更新 README 和多语言文档 - 添加 powermem 依赖包 --- README.md | 3 +- README_de.md | 3 +- README_en.md | 4 +- README_vi.md | 3 +- docs/FAQ.md | 1 + docs/powermem-integration.md | 253 ++++++++++++++++++ main/xiaozhi-server/config.yaml | 24 ++ .../providers/memory/powermem/powermem.py | 227 ++++++++++++++++ main/xiaozhi-server/requirements.txt | 1 + 9 files changed, 515 insertions(+), 4 deletions(-) create mode 100644 docs/powermem-integration.md create mode 100644 main/xiaozhi-server/core/providers/memory/powermem/powermem.py diff --git a/README.md b/README.md index eb718c24..17e2b886 100644 --- a/README.md +++ b/README.md @@ -242,7 +242,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/ | 智能对话 | 支持多种LLM(大语言模型),实现智能对话 | | 视觉感知 | 支持多种VLLM(视觉大模型),实现多模态交互 | | 意图识别 | 支持外挂的大模型意图识别、大模型自主函数调用,提供插件化意图处理机制 | -| 记忆系统 | 支持本地短期记忆、mem0ai接口记忆,具备记忆总结功能 | +| 记忆系统 | 支持本地短期记忆、mem0ai接口记忆、PowerMem智能记忆,具备记忆总结功能 | | 知识库 | 支持RAGFlow知识库,让大模型判断需要调度知识库后再回答 | | 工具调用 | 支持客户端IOT协议、客户MCP协议、服务端MCP协议、MCP接入点协议、自定义工具函数 | | 指令下发 | 依托MQTT协议,支持从智控台将MCP指令下发到ESP32设备 | @@ -330,6 +330,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/ | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | |:------:|:---------------:|:----:|:---------:|:--:| | Memory | mem0ai | 接口调用 | 1000次/月额度 | | +| Memory | [powermem](./docs/powermem-integration.md) | 本地总结 | 取决于LLM和DB | OceanBase开源,支持智能检索 | | Memory | mem_local_short | 本地总结 | 免费 | | | Memory | nomem | 无记忆模式 | 免费 | | diff --git a/README_de.md b/README_de.md index 0e4c74d5..403fc921 100644 --- a/README_de.md +++ b/README_de.md @@ -240,7 +240,7 @@ Dieses Projekt bietet die folgenden Testwerkzeuge, um Ihnen bei der Überprüfun | Intelligenter Dialog | Unterstützt mehrere LLM (große Sprachmodelle), implementiert intelligenten Dialog | | Visuelle Wahrnehmung | Unterstützt mehrere VLLM (Vision Large Models), implementiert multimodale Interaktion | | Absichtserkennung | Unterstützt LLM-Absichtserkennung, Function Call-Funktionsaufruf, bietet plugin-basierten Absichtsverarbeitungsmechanismus | -| Gedächtnissystem | Unterstützt lokales Kurzzeitgedächtnis, mem0ai-Schnittstellengedächtnis, mit Gedächtniszusammenfassungsfunktion | +| Gedächtnissystem | Unterstützt lokales Kurzzeitgedächtnis, mem0ai-Schnittstellengedächtnis, PowerMem intelligentes Gedächtnis, mit Gedächtniszusammenfassungsfunktion | | Wissensdatenbank | Unterstützt RAGFlow-Wissensdatenbank, ermöglicht großem Modell die Bewertung, ob Wissensdatenbank benötigt wird, bevor geantwortet wird | | Werkzeugaufruf | Unterstützt Client-IOT-Protokoll, Client-MCP-Protokoll, Server-MCP-Protokoll, MCP-Endpunktprotokoll, benutzerdefinierte Werkzeugfunktionen | | Befehlsübermittlung | Basierend auf MQTT-Protokoll, unterstützt die Übermittlung von MCP-Befehlen von der intelligenten Steuerkonsole an ESP32-Geräte | @@ -328,6 +328,7 @@ Tatsächlich kann jedes VLLM, das OpenAI-Schnittstellenaufrufe unterstützt, int | Typ | Plattformname | Verwendungsmethode | Preismodell | Hinweise | |:------:|:---------------:|:----:|:---------:|:--:| | Memory | mem0ai | Schnittstellenaufrufe | 1000 Mal/Monat Kontingent | | +| Memory | [powermem](./docs/powermem-integration.md) | Lokale Zusammenfassung | Abhängig von LLM und DB | OceanBase Open Source, unterstützt intelligente Abfrage | | Memory | mem_local_short | Lokale Zusammenfassung | Kostenlos | | | Memory | nomem | Kein Gedächtnismodus | Kostenlos | | diff --git a/README_en.md b/README_en.md index 196cf16f..e071e7fa 100644 --- a/README_en.md +++ b/README_en.md @@ -238,7 +238,7 @@ This project provides the following testing tools to help you verify the system | Intelligent Dialogue | Supports multiple LLM(large language models), implements intelligent dialogue | | Visual Perception | Supports multiple VLLM(vision large models), implements multimodal interaction | | Intent Recognition | Supports LLM intent recognition, Function Call function calling, provides plugin-based intent processing mechanism | -| Memory System | Supports local short-term memory, mem0ai interface memory, with memory summarization functionality | +| Memory System | Supports local short-term memory, mem0ai interface memory, PowerMem intelligent memory, with memory summarization functionality | | Knowledge Base | Supports RAGFlow knowledge base, enabling LLM to judge whether to schedule the knowledge base after receiving the user's question, and then answer the question | | Command Delivery | Supports MCP command delivery to ESP32 devices via MQTT protocol from Smart Console | | Tool Calling | Supports client IOT protocol, client MCP protocol, server MCP protocol, MCP endpoint protocol, custom tool functions | @@ -330,7 +330,9 @@ In fact, any VLLM that supports OpenAI interface calls can be integrated and use | Type | Platform Name | Usage Method | Pricing Model | Notes | |:------:|:---------------:|:----:|:---------:|:--:| | Memory | mem0ai | Interface calls | 1000 times/month quota | | +| Memory | [powermem](./docs/powermem-integration.md) | Local summarization | Depends on LLM and DB | OceanBase open source, supports intelligent retrieval | | Memory | mem_local_short | Local summarization | Free | | +| Memory | nomem | No memory mode | Free | | --- diff --git a/README_vi.md b/README_vi.md index e6d5f489..4a2fbb6b 100644 --- a/README_vi.md +++ b/README_vi.md @@ -241,7 +241,7 @@ Dự án này cung cấp các công cụ kiểm tra sau để giúp bạn xác m | Đối thoại thông minh | Hỗ trợ nhiều LLM(Mô hình ngôn ngữ lớn), thực hiện đối thoại thông minh | | Cảm nhận thị giác | Hỗ trợ nhiều VLLM(Mô hình lớn thị giác), thực hiện tương tác đa phương thức | | Nhận dạng ý định | Hỗ trợ nhận dạng ý định mô hình lớn gắn ngoài, gọi hàm tự chủ mô hình lớn, cung cấp cơ chế xử lý ý định dạng plugin | -| Hệ thống bộ nhớ | Hỗ trợ bộ nhớ ngắn hạn cục bộ, bộ nhớ giao diện mem0ai, có chức năng tóm tắt bộ nhớ | +| Hệ thống bộ nhớ | Hỗ trợ bộ nhớ ngắn hạn cục bộ, bộ nhớ giao diện mem0ai, bộ nhớ thông minh PowerMem, có chức năng tóm tắt bộ nhớ | | Kho tri thức | Hỗ trợ kho tri thức RAGFlow, cho phép mô hình lớn đánh giá cần gọi kho tri thức trước khi trả lời | | Gọi công cụ | Hỗ trợ giao thức IOT phía client, giao thức MCP phía client, giao thức MCP phía server, giao thức điểm truy cập MCP, hàm công cụ tùy chỉnh | | Gửi lệnh | Dựa vào giao thức MQTT, hỗ trợ gửi lệnh MCP từ bảng điều khiển thông minh xuống thiết bị ESP32 | @@ -329,6 +329,7 @@ Trên thực tế, bất kỳ VLLM nào hỗ trợ gọi giao diện openai đ | Loại | Tên nền tảng | Phương pháp sử dụng | Mô hình thu phí | Ghi chú | |:------:|:---------------:|:----:|:---------:|:--:| | Memory | mem0ai | Gọi giao diện | Hạn mức 1000 lần/tháng | | +| Memory | [powermem](./docs/powermem-integration.md) | Tóm tắt cục bộ | Phụ thuộc vào LLM và DB | OceanBase mã nguồn mở, hỗ trợ tìm kiếm thông minh | | Memory | mem_local_short | Tóm tắt cục bộ | Miễn phí | | | Memory | nomem | Chế độ không có bộ nhớ | Miễn phí | | diff --git a/docs/FAQ.md b/docs/FAQ.md index 69494c3d..b6bec471 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -82,6 +82,7 @@ VAD: 8、[新闻插件源配置指南](./newsnow_plugin_config.md)
9、[知识库ragflow集成指南](./ragflow-integration.md)
10、[如何部署上下文源](./context-provider-integration.md)
+11、[如何集成PowerMem智能记忆](./powermem-integration.md)
### 11、语音克隆、本地语音部署相关教程 1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)
diff --git a/docs/powermem-integration.md b/docs/powermem-integration.md new file mode 100644 index 00000000..76547b60 --- /dev/null +++ b/docs/powermem-integration.md @@ -0,0 +1,253 @@ +# PowerMem 记忆组件集成指南 + +## 简介 + +[PowerMem](https://www.powermem.ai/) 是由 OceanBase 开源的 Agent 记忆组件,通过本地 LLM 进行记忆总结和智能检索,为 AI 代理提供高效的记忆管理功能。 + +费用说明:PowerMem 本身开源免费,实际费用取决于您选择的 LLM 和数据库: +- 使用 SQLite + 免费 LLM(如智谱 glm-4-flash)= **完全免费** +- 使用云端 LLM 或云端数据库 = 按对应服务收费 + +> 💡 **最佳性能提示**:PowerMem 配合 OceanBase 使用可实现最大性能释放,SQLite 仅建议在资源不足的情况下使用。 + +- **GitHub**: https://github.com/oceanbase/powermem +- **官网**: https://www.powermem.ai/ +- **使用示例**: https://github.com/oceanbase/powermem/tree/main/examples + +## 功能特性 + +- **本地总结**:通过 LLM 在本地进行记忆总结和提取 +- **多种存储后端**:支持 OceanBase(推荐,最佳性能)、SeekDB(推荐,AI应用存储一体)、PostgreSQL、SQLite(轻量备选) +- **多种 LLM 支持**:通义千问、智谱(glm-4-flash 免费)、OpenAI 等 +- **智能检索**:基于向量搜索的语义检索能力 +- **私有部署**:完全支持本地私有化部署 +- **异步操作**:高效的异步记忆管理 + +## 安装 + +PowerMem 已添加到项目依赖中,如果需要手动安装: + +```bash +pip install powermem +``` + +## 配置说明 + +### 基础配置 + +在 `config.yaml` 中配置 PowerMem: + +```yaml +selected_module: + Memory: powermem + +Memory: + powermem: + type: powermem + # 数据库提供者: oceanbase(推荐,最佳性能), seekdb, postgres, sqlite(轻量备选) + database_provider: sqlite # 资源充足时建议使用 oceanbase 或 seekdb + # LLM提供者: qwen(默认), openai, 等 + llm_provider: qwen + # 嵌入模型提供者: qwen(默认), openai, 等 + embedding_provider: qwen + # LLM配置 + llm_api_key: 你的LLM API密钥 + llm_model: qwen-plus + # 嵌入模型配置 + embedding_api_key: 你的嵌入模型API密钥 + embedding_model: text-embedding-v3 +``` + +### 配置参数详解 + +| 参数 | 说明 | 默认值 | 可选值 | +|------|------|--------|--------| +| `database_provider` | 存储后端类型 | `sqlite` | `oceanbase`(推荐), `seekdb`, `postgres`, `sqlite`(轻量) | +| `llm_provider` | LLM 提供商 | `qwen` | `qwen`, `zhipu`(免费), `openai`, 等 | +| `embedding_provider` | 嵌入模型提供商 | `qwen` | `qwen`, `zhipu`, `openai`, 等 | +| `llm_api_key` | LLM API 密钥 | - | - | +| `llm_model` | LLM 模型名称 | - | 根据提供商选择 | +| `llm_base_url` | LLM API 地址(可选) | - | - | +| `embedding_api_key` | 嵌入模型 API 密钥 | - | - | +| `embedding_model` | 嵌入模型名称 | - | 根据提供商选择 | +| `embedding_base_url` | 嵌入模型 API 地址(可选) | - | - | + +### 使用通义千问(推荐) + +1. 访问 [阿里云百炼平台](https://bailian.console.aliyun.com/) 注册账号 +2. 在 [API Key 管理](https://bailian.console.aliyun.com/?apiKey=1#/api-key) 页面获取 API 密钥 +3. 配置如下: + +```yaml +Memory: + powermem: + type: powermem + database_provider: sqlite + llm_provider: qwen + embedding_provider: qwen + llm_api_key: sk-xxxxxxxxxxxxxxxx + llm_model: qwen-plus + embedding_api_key: sk-xxxxxxxxxxxxxxxx + embedding_model: text-embedding-v3 +``` + +### 使用智谱免费 LLM(完全免费方案) + +智谱提供免费的 glm-4-flash 模型,配合 SQLite 可实现完全免费使用: + +1. 访问 [智谱AI开放平台](https://bigmodel.cn/) 注册账号 +2. 在 [API Keys](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) 页面获取 API 密钥 +3. 配置如下: + +```yaml +Memory: + powermem: + type: powermem + database_provider: sqlite + llm_provider: zhipu + embedding_provider: zhipu + llm_api_key: xxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxx + llm_model: glm-4-flash + llm_base_url: https://open.bigmodel.cn/api/paas/v4/ + embedding_api_key: xxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxx + embedding_model: embedding-3 + embedding_base_url: https://open.bigmodel.cn/api/paas/v4/ +``` + +### 使用 OpenAI + +```yaml +Memory: + powermem: + type: powermem + database_provider: sqlite + llm_provider: openai + embedding_provider: openai + llm_api_key: sk-xxxxxxxxxxxxxxxx + llm_model: gpt-4o-mini + llm_base_url: https://api.openai.com/v1 + embedding_api_key: sk-xxxxxxxxxxxxxxxx + embedding_model: text-embedding-3-small + embedding_base_url: https://api.openai.com/v1 +``` + +### 使用 OceanBase(最佳性能方案) + +OceanBase 是 PowerMem 的最佳搭档,可实现最大性能释放: + +1. 部署 OceanBase 数据库(支持开源本地部署或使用云服务) + - 开源部署:https://github.com/oceanbase/oceanbase + - 云服务:https://www.oceanbase.com/ +2. 配置如下: + +```yaml +Memory: + powermem: + type: powermem + database_provider: oceanbase + llm_provider: qwen + embedding_provider: qwen + llm_api_key: sk-xxxxxxxxxxxxxxxx + llm_model: qwen-plus + embedding_api_key: sk-xxxxxxxxxxxxxxxx + embedding_model: text-embedding-v3 + # OceanBase 数据库连接配置 + vector_store: + provider: oceanbase + config: + host: 127.0.0.1 + port: 2881 + user: root@test + password: your_password + database: powermem +``` + + +### 高级配置 + +如果需要更精细的控制,可以使用完整的配置结构: + +```yaml +Memory: + powermem: + type: powermem + # 向量存储配置 + vector_store: + provider: sqlite + config: + path: ./data/powermem.db + # LLM 配置 + llm: + provider: qwen + config: + api_key: sk-xxxxxxxxxxxxxxxx + model: qwen-plus + # 嵌入模型配置 + embedder: + provider: qwen + config: + api_key: sk-xxxxxxxxxxxxxxxx + model: text-embedding-v3 +``` + +## 设备记忆隔离 + +PowerMem 会自动使用设备 ID(`device_id`)作为 `user_id` 进行记忆隔离。这意味着: + +- 每个设备拥有独立的记忆空间 +- 不同设备之间的记忆完全隔离 +- 同一设备的多次对话可以共享记忆上下文 + +## 与其他记忆组件的对比 + +| 特性 | PowerMem | mem0ai | mem_local_short | +|------|----------|--------|-----------------| +| 工作方式 | 本地总结 | 云端接口 | 本地总结 | +| 存储位置 | 本地/云端DB | 云端 | 本地YAML | +| 费用 | 取决于LLM和DB | 1000次/月免费 | 完全免费 | +| 智能检索 | ✅ 向量搜索 | ✅ 向量搜索 | ❌ 全量返回 | +| 私有部署 | ✅ 支持 | ❌ 仅云端 | ✅ 支持 | +| 数据库支持 | OceanBase(推荐)/SeekDB/PostgreSQL/SQLite | - | YAML 文件 | + +## 常见问题 + +### 1. API 密钥错误 + +如果出现 `API key is required` 错误,请检查: +- `llm_api_key` 和 `embedding_api_key` 是否正确填写 +- API 密钥是否有效 + +### 2. 模型不存在 + +如果出现模型不存在的错误,请确认: +- `llm_model` 和 `embedding_model` 名称是否正确 +- 对应的模型服务是否已开通 + +### 3. 连接超时 + +如果出现连接超时,可以尝试: +- 检查网络连接 +- 如果使用代理,配置 `llm_base_url` 和 `embedding_base_url` + +## 测试验证 + +可以在虚拟环境中测试 PowerMem 是否正常工作: + +```bash +# 激活虚拟环境 +source .venv/bin/activate + +# 测试 PowerMem 导入 +python -c "from powermem import AsyncMemory; print('PowerMem 导入成功')" +``` + +## 更多资源 + +- [PowerMem 官方文档](https://www.powermem.ai/) +- [PowerMem GitHub 仓库](https://github.com/oceanbase/powermem) +- [PowerMem 使用示例](https://github.com/oceanbase/powermem/tree/main/examples) +- [OceanBase 官网](https://www.oceanbase.com/) +- [OceanBase GitHub](https://github.com/oceanbase/oceanbase) +- [SeekDB GitHub](https://github.com/oceanbase/seekdb)(AI原生搜索数据库) +- [阿里云百炼平台](https://bailian.console.aliyun.com/) + diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index a3c54c17..cb735e63 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -275,6 +275,30 @@ 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(推荐,最佳性能), seekdb(推荐,AI应用存储一体), postgres, sqlite(轻量备选) + # 资源充足时建议使用 oceanbase 或 seekdb + 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 diff --git a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py new file mode 100644 index 00000000..ec9fea7a --- /dev/null +++ b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py @@ -0,0 +1,227 @@ +#!/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/ +""" + +import traceback +from typing import Optional, Dict, Any + +from ..base import MemoryProviderBase, logger +from powermem import AsyncMemory + +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. + """ + + def __init__(self, config: Dict[str, Any], summary_memory: Optional[str] = None): + super().__init__(config) + self.use_powermem = False + self.memory_client = None + + try: + # Get configuration parameters + database_provider = config.get("database_provider", "sqlite") + llm_provider = config.get("llm_provider", "qwen") + embedding_provider = config.get("embedding_provider", "qwen") + + # 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 AsyncMemory client + self.memory_client = AsyncMemory(config=powermem_config) + self.use_powermem = True + + logger.bind(tag=TAG).info( + f"PowerMem initialized successfully with database={database_provider}, " + f"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 "" + + # Search memories using PowerMem SDK + results = await self.memory_client.search( + query=query, + user_id=self.role_id, + limit=30 + ) + + if not results or "results" not in results: + logger.bind(tag=TAG).debug("No memory results found") + return "" + + # 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 + memories_str = "\n".join(f"- {memory[1]}" for memory in memories) + logger.bind(tag=TAG).debug(f"Query results: {memories_str}") + return memories_str + + 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 "" + + +# Register the memory provider instance +powermem = MemoryProvider({}) diff --git a/main/xiaozhi-server/requirements.txt b/main/xiaozhi-server/requirements.txt index cc4cd8fe..20167f39 100644 --- a/main/xiaozhi-server/requirements.txt +++ b/main/xiaozhi-server/requirements.txt @@ -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 From de7aedab96104512fc3f2a55c39df988725e3c8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=A0=E7=A3=8A?= Date: Thu, 8 Jan 2026 14:43:48 +0800 Subject: [PATCH 02/14] =?UTF-8?q?feat(memory):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=94=BB=E5=83=8F=E5=8A=9F=E8=83=BD=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 `enable_user_profile` 配置项,支持用户画像模式 - 实现 `UserMemory` 类集成,自动提取用户信息 - 更新文档说明用户画像功能及配置要求 --- docs/powermem-integration.md | 67 ++++++++ main/xiaozhi-server/config.yaml | 5 + .../providers/memory/powermem/powermem.py | 162 +++++++++++++----- 3 files changed, 189 insertions(+), 45 deletions(-) diff --git a/docs/powermem-integration.md b/docs/powermem-integration.md index 76547b60..818e65ed 100644 --- a/docs/powermem-integration.md +++ b/docs/powermem-integration.md @@ -17,6 +17,8 @@ ## 功能特性 - **本地总结**:通过 LLM 在本地进行记忆总结和提取 +- **用户画像**:通过 `UserMemory` 自动提取用户信息(姓名、职业、兴趣等),持续更新用户画像 +- **智能遗忘**:基于艾宾浩斯遗忘曲线,自动"遗忘"过时噪声信息 - **多种存储后端**:支持 OceanBase(推荐,最佳性能)、SeekDB(推荐,AI应用存储一体)、PostgreSQL、SQLite(轻量备选) - **多种 LLM 支持**:通义千问、智谱(glm-4-flash 免费)、OpenAI 等 - **智能检索**:基于向量搜索的语义检索能力 @@ -44,6 +46,8 @@ selected_module: Memory: powermem: type: powermem + # 是否启用用户画像功能(需要OceanBase) + enable_user_profile: false # 数据库提供者: oceanbase(推荐,最佳性能), seekdb, postgres, sqlite(轻量备选) database_provider: sqlite # 资源充足时建议使用 oceanbase 或 seekdb # LLM提供者: qwen(默认), openai, 等 @@ -62,6 +66,7 @@ Memory: | 参数 | 说明 | 默认值 | 可选值 | |------|------|--------|--------| +| `enable_user_profile` | 启用用户画像模式 | `false` | `true`(需OceanBase), `false` | | `database_provider` | 存储后端类型 | `sqlite` | `oceanbase`(推荐), `seekdb`, `postgres`, `sqlite`(轻量) | | `llm_provider` | LLM 提供商 | `qwen` | `qwen`, `zhipu`(免费), `openai`, 等 | | `embedding_provider` | 嵌入模型提供商 | `qwen` | `qwen`, `zhipu`, `openai`, 等 | @@ -72,6 +77,15 @@ Memory: | `embedding_model` | 嵌入模型名称 | - | 根据提供商选择 | | `embedding_base_url` | 嵌入模型 API 地址(可选) | - | - | +### 记忆模式说明 + +PowerMem 支持两种记忆模式: + +| 模式 | 配置 | 功能 | 存储要求 | +|------|------|------|----------| +| **普通记忆** | `enable_user_profile: false` | 对话记忆存储与检索 | 支持所有数据库 | +| **用户画像** | `enable_user_profile: true` | 记忆 + 自动提取用户画像 | 仅支持 OceanBase | + ### 使用通义千问(推荐) 1. 访问 [阿里云百炼平台](https://bailian.console.aliyun.com/) 注册账号 @@ -198,6 +212,54 @@ PowerMem 会自动使用设备 ID(`device_id`)作为 `user_id` 进行记忆 - 不同设备之间的记忆完全隔离 - 同一设备的多次对话可以共享记忆上下文 +## 用户画像(UserMemory) + +PowerMem 提供 `UserMemory` 类,可自动从对话中提取用户画像信息。 + +### 启用用户画像 + +在配置中设置 `enable_user_profile: true` 即可启用: + +```yaml +Memory: + powermem: + type: powermem + enable_user_profile: true # 启用用户画像 + database_provider: oceanbase # 必须使用 OceanBase + llm_provider: qwen + embedding_provider: qwen + llm_api_key: sk-xxxxxxxxxxxxxxxx + llm_model: qwen-plus + embedding_api_key: sk-xxxxxxxxxxxxxxxx + embedding_model: text-embedding-v3 + # OceanBase 数据库连接配置 + vector_store: + provider: oceanbase + config: + host: 127.0.0.1 + port: 2881 + user: root@test + password: your_password + database: powermem +``` + +### 用户画像能力 + +| 能力 | 说明 | +|------|------| +| **信息提取** | 自动从对话中提取姓名、年龄、职业、兴趣等 | +| **持续更新** | 随着对话进行,不断完善用户画像 | +| **画像检索** | 将用户画像与记忆搜索结合,提升检索相关性 | +| **智能遗忘** | 基于艾宾浩斯遗忘曲线,淡化过时信息 | + +### 工作原理 + +启用用户画像后,小智在查询记忆时会自动返回: +1. **用户画像**:用户的基本信息、兴趣爱好等 +2. **相关记忆**:与当前对话相关的历史记忆 + +> ⚠️ **注意**:`UserMemory` 功能需要 OceanBase 作为存储后端,其他数据库暂不支持。 + ## 与其他记忆组件的对比 | 特性 | PowerMem | mem0ai | mem_local_short | @@ -206,6 +268,8 @@ PowerMem 会自动使用设备 ID(`device_id`)作为 `user_id` 进行记忆 | 存储位置 | 本地/云端DB | 云端 | 本地YAML | | 费用 | 取决于LLM和DB | 1000次/月免费 | 完全免费 | | 智能检索 | ✅ 向量搜索 | ✅ 向量搜索 | ❌ 全量返回 | +| 用户画像 | ✅ UserMemory | ❌ | ❌ | +| 智能遗忘 | ✅ 遗忘曲线 | ❌ | ❌ | | 私有部署 | ✅ 支持 | ❌ 仅云端 | ✅ 支持 | | 数据库支持 | OceanBase(推荐)/SeekDB/PostgreSQL/SQLite | - | YAML 文件 | @@ -239,6 +303,9 @@ source .venv/bin/activate # 测试 PowerMem 导入 python -c "from powermem import AsyncMemory; print('PowerMem 导入成功')" + +# 测试 UserMemory 导入(用户画像功能) +python -c "from powermem import UserMemory; print('UserMemory 导入成功')" ``` ## 更多资源 diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index cb735e63..e5420b2c 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -284,8 +284,13 @@ Memory: # 官网: 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 diff --git a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py index ec9fea7a..c1891932 100644 --- a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py +++ b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py @@ -7,13 +7,13 @@ 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 -from powermem import AsyncMemory TAG = __name__ @@ -27,19 +27,37 @@ class MemoryProvider(MemoryProviderBase): 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: + 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 @@ -89,13 +107,21 @@ class MemoryProvider(MemoryProviderBase): "config": embedder_config } - # Initialize AsyncMemory client - self.memory_client = AsyncMemory(config=powermem_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 with database={database_provider}, " - f"llm={llm_provider}, embedding={embedding_provider}" + f"PowerMem initialized successfully: mode={memory_mode}, " + f"database={database_provider}, llm={llm_provider}, embedding={embedding_provider}" ) except ImportError as e: @@ -167,6 +193,14 @@ class MemoryProvider(MemoryProviderBase): 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, @@ -174,54 +208,92 @@ class MemoryProvider(MemoryProviderBase): limit=30 ) - if not results or "results" not in results: - logger.bind(tag=TAG).debug("No memory results found") - return "" - - # 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}")) + 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: - memories.append(("", memory)) + 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) + # Sort by timestamp in descending order (newest first) + memories.sort(key=lambda x: x[0], reverse=True) - # Extract only the formatted strings - memories_str = "\n".join(f"- {memory[1]}" for memory in memories) - logger.bind(tag=TAG).debug(f"Query results: {memories_str}") - return memories_str + # 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({}) From 2bf1123647a727aecca21a8c2a4c61f9e92db641 Mon Sep 17 00:00:00 2001 From: wayyoungboy <1017761807@qq.com> Date: Thu, 8 Jan 2026 23:52:19 +0800 Subject: [PATCH 03/14] =?UTF-8?q?=E9=83=A8=E5=88=86=E9=85=8D=E7=BD=AEkey?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/powermem-integration.md | 42 +++++--- main/xiaozhi-server/config.yaml | 8 +- .../providers/memory/powermem/powermem.py | 101 +++++++++++------- 3 files changed, 97 insertions(+), 54 deletions(-) diff --git a/docs/powermem-integration.md b/docs/powermem-integration.md index 818e65ed..5b194106 100644 --- a/docs/powermem-integration.md +++ b/docs/powermem-integration.md @@ -59,7 +59,9 @@ Memory: llm_model: qwen-plus # 嵌入模型配置 embedding_api_key: 你的嵌入模型API密钥 - embedding_model: text-embedding-v3 + embedding_model: text-embedding-v4 + # 可选:自定义嵌入服务地址(OpenAI兼容模式) + # embedding_openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 ``` ### 配置参数详解 @@ -72,10 +74,21 @@ Memory: | `embedding_provider` | 嵌入模型提供商 | `qwen` | `qwen`, `zhipu`, `openai`, 等 | | `llm_api_key` | LLM API 密钥 | - | - | | `llm_model` | LLM 模型名称 | - | 根据提供商选择 | -| `llm_base_url` | LLM API 地址(可选) | - | - | +| `llm_base_url` | LLM API 地址(可选) | - | 根据provider自动选择参数名 | | `embedding_api_key` | 嵌入模型 API 密钥 | - | - | | `embedding_model` | 嵌入模型名称 | - | 根据提供商选择 | -| `embedding_base_url` | 嵌入模型 API 地址(可选) | - | - | +| `embedding_base_url` | 嵌入模型 API 地址(可选) | - | 通用配置,根据provider自动选择 | +| `embedding_openai_base_url` | OpenAI兼容的嵌入服务地址(可选) | - | 优先级高于 embedding_base_url | +| `embedding_dashscope_base_url` | 阿里云灵积嵌入服务地址(可选) | - | 优先级高于 embedding_base_url | + +#### Base URL 配置优先级 + +| 配置项 | 优先级 | 适用 Provider | +|--------|--------|---------------| +| `embedding_openai_base_url` | 最高 | openai | +| `embedding_dashscope_base_url` | 最高 | qwen | +| `embedding_base_url` | 中 | 通用,根据 provider 自动选择 | +| 默认值 | 最低 | 使用 provider 的默认服务地址 | ### 记忆模式说明 @@ -102,7 +115,9 @@ Memory: llm_api_key: sk-xxxxxxxxxxxxxxxx llm_model: qwen-plus embedding_api_key: sk-xxxxxxxxxxxxxxxx - embedding_model: text-embedding-v3 + embedding_model: text-embedding-v4 + # 可选:使用 OpenAI 兼容模式的服务地址 + embedding_openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 ``` ### 使用智谱免费 LLM(完全免费方案) @@ -118,14 +133,14 @@ Memory: powermem: type: powermem database_provider: sqlite - llm_provider: zhipu - embedding_provider: zhipu + llm_provider: openai # 使用 openai 兼容模式 + embedding_provider: openai # 使用 openai 兼容模式 llm_api_key: xxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxx llm_model: glm-4-flash llm_base_url: https://open.bigmodel.cn/api/paas/v4/ embedding_api_key: xxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxx embedding_model: embedding-3 - embedding_base_url: https://open.bigmodel.cn/api/paas/v4/ + embedding_openai_base_url: https://open.bigmodel.cn/api/paas/v4/ ``` ### 使用 OpenAI @@ -142,7 +157,7 @@ Memory: llm_base_url: https://api.openai.com/v1 embedding_api_key: sk-xxxxxxxxxxxxxxxx embedding_model: text-embedding-3-small - embedding_base_url: https://api.openai.com/v1 + embedding_openai_base_url: https://api.openai.com/v1 ``` ### 使用 OceanBase(最佳性能方案) @@ -164,7 +179,8 @@ Memory: llm_api_key: sk-xxxxxxxxxxxxxxxx llm_model: qwen-plus embedding_api_key: sk-xxxxxxxxxxxxxxxx - embedding_model: text-embedding-v3 + embedding_model: text-embedding-v4 + embedding_openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 # OceanBase 数据库连接配置 vector_store: provider: oceanbase @@ -198,10 +214,11 @@ Memory: model: qwen-plus # 嵌入模型配置 embedder: - provider: qwen + provider: openai # 使用 OpenAI 兼容模式 config: api_key: sk-xxxxxxxxxxxxxxxx - model: text-embedding-v3 + model: text-embedding-v4 + openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 ``` ## 设备记忆隔离 @@ -231,7 +248,8 @@ Memory: llm_api_key: sk-xxxxxxxxxxxxxxxx llm_model: qwen-plus embedding_api_key: sk-xxxxxxxxxxxxxxxx - embedding_model: text-embedding-v3 + embedding_model: text-embedding-v4 + embedding_openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 # OceanBase 数据库连接配置 vector_store: provider: oceanbase diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index e5420b2c..324fb972 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -301,9 +301,15 @@ Memory: # 智谱AI(免费): https://bigmodel.cn/usercenter/proj-mgmt/apikeys llm_api_key: 你的LLM API密钥 llm_model: qwen-plus + # llm_base_url: https://dashscope.aliyuncs.com/api/v1 # 可选,自定义LLM服务地址 # 嵌入模型配置 (使用通义千问为例) embedding_api_key: 你的嵌入模型API密钥 - embedding_model: text-embedding-v3 + embedding_model: text-embedding-v4 + embedding_openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 # OpenAI兼容的嵌入服务地址 + # embedding_base_url: https://dashscope.aliyuncs.com/api/v1 # 可选,自定义嵌入模型服务地址 + # 或使用独立的base_url配置(优先级更高): + # embedding_openai_base_url: https://api.openai.com/v1 # OpenAI兼容的嵌入服务地址 + # embedding_dashscope_base_url: https://dashscope.aliyuncs.com/api/v1 # 阿里云灵积嵌入服务地址 nomem: # 不想使用记忆功能,可以使用nomem type: nomem diff --git a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py index c1891932..fc883c68 100644 --- a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py +++ b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py @@ -21,13 +21,13 @@ 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) @@ -40,16 +40,16 @@ class MemoryProvider(MemoryProviderBase): 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( @@ -57,13 +57,13 @@ class MemoryProvider(MemoryProviderBase): "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"] @@ -74,7 +74,7 @@ class MemoryProvider(MemoryProviderBase): "provider": database_provider, "config": {} } - + # Configure LLM if "llm" in config: powermem_config["llm"] = config["llm"] @@ -84,13 +84,23 @@ class MemoryProvider(MemoryProviderBase): llm_config["api_key"] = config["llm_api_key"] if "llm_model" in config: llm_config["model"] = config["llm_model"] + # Handle base_url based on provider type + # - qwen provider uses dashscope_base_url + # - openai provider uses openai_base_url if "llm_base_url" in config: - llm_config["base_url"] = config["llm_base_url"] + if llm_provider == "qwen": + llm_config["dashscope_base_url"] = config["llm_base_url"] + else: + llm_config["openai_base_url"] = config["llm_base_url"] + if "openai_base_url" in config: + llm_config["openai_base_url"] = config["openai_base_url"] + if "dashscope_base_url" in config: + llm_config["dashscope_base_url"] = config["dashscope_base_url"] powermem_config["llm"] = { "provider": llm_provider, "config": llm_config } - + # Configure embedder if "embedder" in config: powermem_config["embedder"] = config["embedder"] @@ -100,13 +110,25 @@ class MemoryProvider(MemoryProviderBase): embedder_config["api_key"] = config["embedding_api_key"] if "embedding_model" in config: embedder_config["model"] = config["embedding_model"] + # Handle base_url based on provider type + # - qwen provider uses dashscope_base_url + # - openai provider uses openai_base_url + # Priority: embedding_xxx_base_url > embedding_base_url > xxx_base_url if "embedding_base_url" in config: - embedder_config["base_url"] = config["embedding_base_url"] + if embedding_provider == "qwen": + embedder_config["dashscope_base_url"] = config["embedding_base_url"] + else: + embedder_config["openai_base_url"] = config["embedding_base_url"] + # Embedding-specific base_url (higher priority) + if "embedding_openai_base_url" in config: + embedder_config["openai_base_url"] = config["embedding_openai_base_url"] + if "embedding_dashscope_base_url" in config: + embedder_config["dashscope_base_url"] = config["embedding_dashscope_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 @@ -116,14 +138,14 @@ class MemoryProvider(MemoryProviderBase): 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}" @@ -137,17 +159,17 @@ class MemoryProvider(MemoryProviderBase): 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 @@ -159,16 +181,16 @@ class MemoryProvider(MemoryProviderBase): 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()}") @@ -177,24 +199,24 @@ class MemoryProvider(MemoryProviderBase): 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() @@ -207,7 +229,7 @@ class MemoryProvider(MemoryProviderBase): user_id=self.role_id, limit=30 ) - + if results and "results" in results: # Format each memory entry with its update time memories = [] @@ -218,7 +240,7 @@ class MemoryProvider(MemoryProviderBase): 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) @@ -231,7 +253,7 @@ class MemoryProvider(MemoryProviderBase): formatted_time = timestamp else: formatted_time = "" - + memory = entry.get("memory", "") or entry.get("content", "") if memory: if formatted_time: @@ -251,7 +273,7 @@ class MemoryProvider(MemoryProviderBase): 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()}") @@ -260,40 +282,37 @@ class MemoryProvider(MemoryProviderBase): 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({}) From ad93f431453358f0e7944b286c45e79719ffd7be Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Fri, 9 Jan 2026 10:49:24 +0800 Subject: [PATCH 04/14] =?UTF-8?q?=E5=8E=BB=E9=99=A4=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=EF=BC=8C=E8=A1=A5=E5=85=85=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E7=AD=BE=E5=90=8D=E7=BC=BA=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/providers/memory/powermem/powermem.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py index c1891932..1689b423 100644 --- a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py +++ b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py @@ -134,13 +134,14 @@ class MemoryProvider(MemoryProviderBase): logger.bind(tag=TAG).debug(f"Detailed error: {traceback.format_exc()}") self.use_powermem = False - async def save_memory(self, msgs): + async def save_memory(self, msgs, session_id=None): """ Save conversation messages to PowerMem. - + Args: msgs: List of message objects with 'role' and 'content' attributes - + session_id: Session identifier (optional, for compatibility) + Returns: Result from PowerMem API or None if failed """ @@ -293,7 +294,3 @@ class MemoryProvider(MemoryProviderBase): 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({}) From 564adbd8518510073e5be8d3d5db59652cda5420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=A0=E7=A3=8A?= Date: Fri, 9 Jan 2026 12:25:17 +0800 Subject: [PATCH 05/14] =?UTF-8?q?feat(powermem):=20=E5=8D=87=E7=BA=A7?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=BB=93=E6=9E=84=E5=B9=B6=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=A4=9A=E6=95=B0=E6=8D=AE=E5=BA=93=E7=94=A8=E6=88=B7=E7=94=BB?= =?UTF-8?q?=E5=83=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构配置结构为模块化设计(llm/embedder/vector_store) - 用户画像功能支持oceanbase/seekdb/sqlite三种存储后端 - 更新文档说明和示例配置 --- docs/powermem-integration.md | 248 +++++++++--------- main/xiaozhi-server/config.yaml | 62 +++-- .../providers/memory/powermem/powermem.py | 50 ++-- main/xiaozhi-server/requirements.txt | 2 +- 4 files changed, 186 insertions(+), 176 deletions(-) diff --git a/docs/powermem-integration.md b/docs/powermem-integration.md index 5b194106..86e47925 100644 --- a/docs/powermem-integration.md +++ b/docs/powermem-integration.md @@ -46,49 +46,58 @@ selected_module: Memory: powermem: type: powermem - # 是否启用用户画像功能(需要OceanBase) - enable_user_profile: false - # 数据库提供者: oceanbase(推荐,最佳性能), seekdb, postgres, sqlite(轻量备选) - database_provider: sqlite # 资源充足时建议使用 oceanbase 或 seekdb - # LLM提供者: qwen(默认), openai, 等 - llm_provider: qwen - # 嵌入模型提供者: qwen(默认), openai, 等 - embedding_provider: qwen - # LLM配置 - llm_api_key: 你的LLM API密钥 - llm_model: qwen-plus - # 嵌入模型配置 - embedding_api_key: 你的嵌入模型API密钥 - embedding_model: text-embedding-v4 - # 可选:自定义嵌入服务地址(OpenAI兼容模式) - # embedding_openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 + # 是否启用用户画像功能 + # 用户画像支持: oceanbase、seekdb、sqlite (powermem 0.3.0+) + enable_user_profile: true + + # ========== LLM 配置 ========== + llm: + provider: openai # 可选: qwen, openai, zhipu 等 + config: + api_key: 你的LLM API密钥 + model: qwen-plus + # openai_base_url: https://api.openai.com/v1 # 可选,自定义服务地址 + + # ========== Embedding 配置 ========== + embedder: + provider: openai # 可选: qwen, openai 等 + config: + api_key: 你的嵌入模型API密钥 + model: text-embedding-v4 + openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 + + # ========== Database 配置 ========== + vector_store: + provider: sqlite # 可选: oceanbase(推荐), seekdb(推荐), postgres, sqlite(轻量) + config: {} # SQLite 无需额外配置 ``` ### 配置参数详解 -| 参数 | 说明 | 默认值 | 可选值 | -|------|------|--------|--------| -| `enable_user_profile` | 启用用户画像模式 | `false` | `true`(需OceanBase), `false` | -| `database_provider` | 存储后端类型 | `sqlite` | `oceanbase`(推荐), `seekdb`, `postgres`, `sqlite`(轻量) | -| `llm_provider` | LLM 提供商 | `qwen` | `qwen`, `zhipu`(免费), `openai`, 等 | -| `embedding_provider` | 嵌入模型提供商 | `qwen` | `qwen`, `zhipu`, `openai`, 等 | -| `llm_api_key` | LLM API 密钥 | - | - | -| `llm_model` | LLM 模型名称 | - | 根据提供商选择 | -| `llm_base_url` | LLM API 地址(可选) | - | 根据provider自动选择参数名 | -| `embedding_api_key` | 嵌入模型 API 密钥 | - | - | -| `embedding_model` | 嵌入模型名称 | - | 根据提供商选择 | -| `embedding_base_url` | 嵌入模型 API 地址(可选) | - | 通用配置,根据provider自动选择 | -| `embedding_openai_base_url` | OpenAI兼容的嵌入服务地址(可选) | - | 优先级高于 embedding_base_url | -| `embedding_dashscope_base_url` | 阿里云灵积嵌入服务地址(可选) | - | 优先级高于 embedding_base_url | +#### LLM 配置 -#### Base URL 配置优先级 +| 参数 | 说明 | 可选值 | +|------|------|--------| +| `llm.provider` | LLM 提供商 | `qwen`, `openai`, `zhipu` 等 | +| `llm.config.api_key` | API 密钥 | - | +| `llm.config.model` | 模型名称 | 根据提供商选择 | +| `llm.config.openai_base_url` | 自定义服务地址(可选) | - | -| 配置项 | 优先级 | 适用 Provider | -|--------|--------|---------------| -| `embedding_openai_base_url` | 最高 | openai | -| `embedding_dashscope_base_url` | 最高 | qwen | -| `embedding_base_url` | 中 | 通用,根据 provider 自动选择 | -| 默认值 | 最低 | 使用 provider 的默认服务地址 | +#### Embedding 配置 + +| 参数 | 说明 | 可选值 | +|------|------|--------| +| `embedder.provider` | 嵌入模型提供商 | `qwen`, `openai` 等 | +| `embedder.config.api_key` | API 密钥 | - | +| `embedder.config.model` | 模型名称 | 根据提供商选择 | +| `embedder.config.openai_base_url` | 自定义服务地址(可选) | - | + +#### Database 配置 + +| 参数 | 说明 | 可选值 | +|------|------|--------| +| `vector_store.provider` | 存储后端类型 | `oceanbase`(推荐), `seekdb`(推荐), `postgres`, `sqlite`(轻量) | +| `vector_store.config` | 数据库连接配置 | 根据 provider 设置 | ### 记忆模式说明 @@ -97,7 +106,9 @@ PowerMem 支持两种记忆模式: | 模式 | 配置 | 功能 | 存储要求 | |------|------|------|----------| | **普通记忆** | `enable_user_profile: false` | 对话记忆存储与检索 | 支持所有数据库 | -| **用户画像** | `enable_user_profile: true` | 记忆 + 自动提取用户画像 | 仅支持 OceanBase | +| **用户画像** | `enable_user_profile: true` | 记忆 + 自动提取用户画像 | oceanbase、seekdb、sqlite | + +> 📌 **版本说明**:PowerMem 0.3.0+ 版本,用户画像功能支持 OceanBase、SeekDB、SQLite 三种存储后端。 ### 使用通义千问(推荐) @@ -109,15 +120,21 @@ PowerMem 支持两种记忆模式: Memory: powermem: type: powermem - database_provider: sqlite - llm_provider: qwen - embedding_provider: qwen - llm_api_key: sk-xxxxxxxxxxxxxxxx - llm_model: qwen-plus - embedding_api_key: sk-xxxxxxxxxxxxxxxx - embedding_model: text-embedding-v4 - # 可选:使用 OpenAI 兼容模式的服务地址 - embedding_openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 + enable_user_profile: true + llm: + provider: qwen + config: + api_key: sk-xxxxxxxxxxxxxxxx + model: qwen-plus + embedder: + provider: openai + config: + api_key: sk-xxxxxxxxxxxxxxxx + model: text-embedding-v4 + openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 + vector_store: + provider: sqlite + config: {} ``` ### 使用智谱免费 LLM(完全免费方案) @@ -132,15 +149,22 @@ Memory: Memory: powermem: type: powermem - database_provider: sqlite - llm_provider: openai # 使用 openai 兼容模式 - embedding_provider: openai # 使用 openai 兼容模式 - llm_api_key: xxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxx - llm_model: glm-4-flash - llm_base_url: https://open.bigmodel.cn/api/paas/v4/ - embedding_api_key: xxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxx - embedding_model: embedding-3 - embedding_openai_base_url: https://open.bigmodel.cn/api/paas/v4/ + enable_user_profile: true + llm: + provider: openai # 使用 openai 兼容模式 + config: + api_key: xxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxx + model: glm-4-flash + openai_base_url: https://open.bigmodel.cn/api/paas/v4/ + embedder: + provider: openai + config: + api_key: xxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxx + model: embedding-3 + openai_base_url: https://open.bigmodel.cn/api/paas/v4/ + vector_store: + provider: sqlite + config: {} ``` ### 使用 OpenAI @@ -149,15 +173,22 @@ Memory: Memory: powermem: type: powermem - database_provider: sqlite - llm_provider: openai - embedding_provider: openai - llm_api_key: sk-xxxxxxxxxxxxxxxx - llm_model: gpt-4o-mini - llm_base_url: https://api.openai.com/v1 - embedding_api_key: sk-xxxxxxxxxxxxxxxx - embedding_model: text-embedding-3-small - embedding_openai_base_url: https://api.openai.com/v1 + enable_user_profile: true + llm: + provider: openai + config: + api_key: sk-xxxxxxxxxxxxxxxx + model: gpt-4o-mini + openai_base_url: https://api.openai.com/v1 + embedder: + provider: openai + config: + api_key: sk-xxxxxxxxxxxxxxxx + model: text-embedding-3-small + openai_base_url: https://api.openai.com/v1 + vector_store: + provider: sqlite + config: {} ``` ### 使用 OceanBase(最佳性能方案) @@ -173,15 +204,18 @@ OceanBase 是 PowerMem 的最佳搭档,可实现最大性能释放: Memory: powermem: type: powermem - database_provider: oceanbase - llm_provider: qwen - embedding_provider: qwen - llm_api_key: sk-xxxxxxxxxxxxxxxx - llm_model: qwen-plus - embedding_api_key: sk-xxxxxxxxxxxxxxxx - embedding_model: text-embedding-v4 - embedding_openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 - # OceanBase 数据库连接配置 + enable_user_profile: true + llm: + provider: qwen + config: + api_key: sk-xxxxxxxxxxxxxxxx + model: qwen-plus + embedder: + provider: openai + config: + api_key: sk-xxxxxxxxxxxxxxxx + model: text-embedding-v4 + openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 vector_store: provider: oceanbase config: @@ -189,36 +223,9 @@ Memory: port: 2881 user: root@test password: your_password - database: powermem -``` - - -### 高级配置 - -如果需要更精细的控制,可以使用完整的配置结构: - -```yaml -Memory: - powermem: - type: powermem - # 向量存储配置 - vector_store: - provider: sqlite - config: - path: ./data/powermem.db - # LLM 配置 - llm: - provider: qwen - config: - api_key: sk-xxxxxxxxxxxxxxxx - model: qwen-plus - # 嵌入模型配置 - embedder: - provider: openai # 使用 OpenAI 兼容模式 - config: - api_key: sk-xxxxxxxxxxxxxxxx - model: text-embedding-v4 - openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 + db_name: powermem + collection_name: memories # 默认值 + embedding_model_dims: 1536 # 嵌入向量维度,必需参数 ``` ## 设备记忆隔离 @@ -233,6 +240,8 @@ PowerMem 会自动使用设备 ID(`device_id`)作为 `user_id` 进行记忆 PowerMem 提供 `UserMemory` 类,可自动从对话中提取用户画像信息。 +> 📌 **版本说明**:PowerMem 0.3.0+ 版本,用户画像功能支持 OceanBase、SeekDB、SQLite 三种存储后端。 + ### 启用用户画像 在配置中设置 `enable_user_profile: true` 即可启用: @@ -242,23 +251,20 @@ Memory: powermem: type: powermem enable_user_profile: true # 启用用户画像 - database_provider: oceanbase # 必须使用 OceanBase - llm_provider: qwen - embedding_provider: qwen - llm_api_key: sk-xxxxxxxxxxxxxxxx - llm_model: qwen-plus - embedding_api_key: sk-xxxxxxxxxxxxxxxx - embedding_model: text-embedding-v4 - embedding_openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 - # OceanBase 数据库连接配置 - vector_store: - provider: oceanbase + llm: + provider: qwen config: - host: 127.0.0.1 - port: 2881 - user: root@test - password: your_password - database: powermem + api_key: sk-xxxxxxxxxxxxxxxx + model: qwen-plus + embedder: + provider: openai + config: + api_key: sk-xxxxxxxxxxxxxxxx + model: text-embedding-v4 + openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 + vector_store: + provider: sqlite # 用户画像支持: oceanbase、seekdb、sqlite + config: {} ``` ### 用户画像能力 @@ -276,7 +282,7 @@ Memory: 1. **用户画像**:用户的基本信息、兴趣爱好等 2. **相关记忆**:与当前对话相关的历史记忆 -> ⚠️ **注意**:`UserMemory` 功能需要 OceanBase 作为存储后端,其他数据库暂不支持。 +> ✅ **版本说明**:PowerMem 0.3.0+ 版本,用户画像功能支持 OceanBase、SeekDB、SQLite 三种存储后端。 ## 与其他记忆组件的对比 diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 324fb972..09e4bd8c 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -284,32 +284,46 @@ Memory: # 官网: 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配置 (使用通义千问为例) + # 是否启用用户画像功能 + # - false: 使用普通记忆模式(AsyncMemory) + # - true: 使用用户画像模式(UserMemory),自动提取用户信息 + # 用户画像功能支持: oceanbase、seekdb、sqlite (powermem 0.3.0+) + enable_user_profile: true + + # ========== 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 - # llm_base_url: https://dashscope.aliyuncs.com/api/v1 # 可选,自定义LLM服务地址 - # 嵌入模型配置 (使用通义千问为例) - embedding_api_key: 你的嵌入模型API密钥 - embedding_model: text-embedding-v4 - embedding_openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 # OpenAI兼容的嵌入服务地址 - # embedding_base_url: https://dashscope.aliyuncs.com/api/v1 # 可选,自定义嵌入模型服务地址 - # 或使用独立的base_url配置(优先级更高): - # embedding_openai_base_url: https://api.openai.com/v1 # OpenAI兼容的嵌入服务地址 - # embedding_dashscope_base_url: https://dashscope.aliyuncs.com/api/v1 # 阿里云灵积嵌入服务地址 + llm: + provider: openai # 可选: qwen, openai, zhipu 等 + config: + api_key: 你的LLM API密钥 + model: qwen-plus + # openai_base_url: https://api.openai.com/v1 # 可选,自定义服务地址 + + # ========== Embedding 配置 ========== + embedder: + provider: openai # 可选: qwen, openai 等 + config: + api_key: 你的嵌入模型API密钥 + model: text-embedding-v4 + openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 + + # ========== Database 配置 ========== + # oceanbase(推荐,最佳性能), seekdb(推荐,AI应用存储一体), postgres, sqlite(轻量备选) + # 用户画像功能支持: oceanbase、seekdb、sqlite + vector_store: + provider: sqlite + config: {} # SQLite 无需额外配置 + # OceanBase 配置示例: + # provider: oceanbase + # config: + # host: 127.0.0.1 + # port: 2881 + # user: root@test + # password: your_password + # db_name: powermem + # collection_name: memories # 默认值 + # embedding_model_dims: 1536 # 嵌入向量维度,必需参数 nomem: # 不想使用记忆功能,可以使用nomem type: nomem diff --git a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py index fc883c68..76f84bb7 100644 --- a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py +++ b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py @@ -10,6 +10,7 @@ @Author: wayyoungboy """ +import asyncio import traceback from typing import Optional, Dict, Any @@ -40,6 +41,7 @@ class MemoryProvider(MemoryProviderBase): self.use_powermem = False self.memory_client = None self.enable_user_profile = False + self.last_profile_content = "" # Cache for user profile from UserMemory try: # Check if user profile mode is enabled @@ -50,14 +52,6 @@ class MemoryProvider(MemoryProviderBase): 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 @@ -183,12 +177,22 @@ class MemoryProvider(MemoryProviderBase): ] # Add memory using PowerMem SDK - result = await self.memory_client.add( + result = self.memory_client.add( messages=messages, user_id=self.role_id ) + # Handle both sync and async returns + if asyncio.iscoroutine(result): + result = await result logger.bind(tag=TAG).debug(f"Save memory result: {result}") + + # Cache user profile if UserMemory mode and profile was extracted + if self.enable_user_profile and result: + if result.get('profile_extracted'): + self.last_profile_content = result.get('profile_content', '') + logger.bind(tag=TAG).debug(f"User profile extracted: {self.last_profile_content}") + return result except Exception as e: @@ -282,6 +286,9 @@ class MemoryProvider(MemoryProviderBase): async def get_user_profile(self) -> str: """ Get user profile from PowerMem (only available in UserMemory mode). + + In PowerMem 0.3.0+, user profile is automatically extracted during add() + and cached in last_profile_content. Returns: Formatted user profile string or empty string if not available @@ -293,26 +300,9 @@ class MemoryProvider(MemoryProviderBase): logger.bind(tag=TAG).debug("User profile mode is not enabled") return "" - try: - if not getattr(self, "role_id", None): - return "" + # Return cached profile content from last add() operation + if self.last_profile_content: + return self.last_profile_content - # 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 "" + return "" diff --git a/main/xiaozhi-server/requirements.txt b/main/xiaozhi-server/requirements.txt index 20167f39..b343742a 100644 --- a/main/xiaozhi-server/requirements.txt +++ b/main/xiaozhi-server/requirements.txt @@ -23,7 +23,7 @@ loguru==0.7.3 requests==2.32.5 cozepy==0.20.0 mem0ai==1.0.0 -powermem>=0.2.1 +powermem>=0.3.0 bs4==0.0.2 modelscope==1.32.0 sherpa_onnx==1.12.17 From 06a2e6fbfa46859e89911cd63fe44cb01bb767a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=A0=E7=A3=8A?= Date: Fri, 9 Jan 2026 12:34:55 +0800 Subject: [PATCH 06/14] =?UTF-8?q?refactor(powermem):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84=E5=86=85=E5=AD=98=E6=8F=90?= =?UTF-8?q?=E4=BE=9B=E8=80=85=E5=AE=9E=E4=BE=8B=E6=B3=A8=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/providers/memory/powermem/powermem.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py index 96387ec4..a92b8ab3 100644 --- a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py +++ b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py @@ -308,6 +308,3 @@ class MemoryProvider(MemoryProviderBase): return "" - -# Register the memory provider instance -powermem = MemoryProvider({}) From 111909c26db24d8238c85458e7d6045a9cff7524 Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Fri, 9 Jan 2026 15:46:08 +0800 Subject: [PATCH 07/14] =?UTF-8?q?fix:=20=E5=90=AF=E7=94=A8=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=94=BB=E5=83=8F=E6=A8=A1=E5=BC=8F=E6=97=B6=EF=BC=8C?= =?UTF-8?q?search=E4=B8=BA=E5=90=8C=E6=AD=A5=E6=96=B9=E6=B3=95=E9=9C=80?= =?UTF-8?q?=E5=8C=85=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../providers/memory/powermem/powermem.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py index a92b8ab3..c33c58da 100644 --- a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py +++ b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py @@ -230,11 +230,21 @@ class MemoryProvider(MemoryProviderBase): 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 self.enable_user_profile: + # UserMemory uses sync search + results = await asyncio.to_thread( + self.memory_client.search, + query=query, + user_id=self.role_id, + limit=30 + ) + else: + # AsyncMemory uses async search + 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 From 134000996b7b3592ec8378e67e8617e573b09999 Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Fri, 9 Jan 2026 16:09:05 +0800 Subject: [PATCH 08/14] =?UTF-8?q?query=E5=8F=AA=E4=BC=A0=E9=80=92=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E7=9A=84=E6=96=87=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/providers/memory/mem0ai/mem0ai.py | 12 +++++++++++- .../core/providers/memory/powermem/powermem.py | 18 +++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/main/xiaozhi-server/core/providers/memory/mem0ai/mem0ai.py b/main/xiaozhi-server/core/providers/memory/mem0ai/mem0ai.py index 7156ab72..a7efc36e 100644 --- a/main/xiaozhi-server/core/providers/memory/mem0ai/mem0ai.py +++ b/main/xiaozhi-server/core/providers/memory/mem0ai/mem0ai.py @@ -1,3 +1,4 @@ +import json import traceback from ..base import MemoryProviderBase, logger @@ -56,7 +57,16 @@ class MemoryProvider(MemoryProviderBase): filters = {"user_id": self.role_id} - results = self.client.search(query, filters=filters) + search_query = query + try: + if query.strip().startswith("{") and query.strip().endswith("}"): + data = json.loads(query) + if "content" in data: + search_query = data["content"] + except (json.JSONDecodeError, KeyError): + pass + + results = self.client.search(search_query, filters=filters) if not results or "results" not in results: return "" diff --git a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py index c33c58da..dc1853f7 100644 --- a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py +++ b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py @@ -11,6 +11,7 @@ """ import asyncio +import json import traceback from typing import Optional, Dict, Any @@ -207,7 +208,7 @@ class MemoryProvider(MemoryProviderBase): Query memories from PowerMem based on similarity search. Args: - query: The search query string + query: The search query string (may be JSON format with metadata) Returns: Formatted string of relevant memories or empty string if none found @@ -221,6 +222,17 @@ class MemoryProvider(MemoryProviderBase): logger.bind(tag=TAG).debug("No role_id set, returning empty memory") return "" + # Extract content from JSON format if present (for ASR with emotion/language tags) + search_query = query + try: + if query.strip().startswith("{") and query.strip().endswith("}"): + data = json.loads(query) + if "content" in data: + search_query = data["content"] + except (json.JSONDecodeError, KeyError): + # If parsing fails, use original query + pass + result_parts = [] # If user profile mode is enabled, include user profile in results @@ -234,14 +246,14 @@ class MemoryProvider(MemoryProviderBase): # UserMemory uses sync search results = await asyncio.to_thread( self.memory_client.search, - query=query, + query=search_query, user_id=self.role_id, limit=30 ) else: # AsyncMemory uses async search results = await self.memory_client.search( - query=query, + query=search_query, user_id=self.role_id, limit=30 ) From c4fc510a27d8c6c64645fc37c22e3a6bf48e9b96 Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Fri, 9 Jan 2026 17:25:03 +0800 Subject: [PATCH 09/14] =?UTF-8?q?=E8=A1=A5=E5=85=85=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config.yaml | 7 ++----- .../core/providers/memory/powermem/powermem.py | 6 +++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 09e4bd8c..621383d5 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -289,7 +289,6 @@ Memory: # - true: 使用用户画像模式(UserMemory),自动提取用户信息 # 用户画像功能支持: oceanbase、seekdb、sqlite (powermem 0.3.0+) enable_user_profile: true - # ========== LLM 配置 ========== # 通义千问: https://bailian.console.aliyun.com/?apiKey=1#/api-key # 智谱AI(免费): https://bigmodel.cn/usercenter/proj-mgmt/apikeys @@ -299,7 +298,6 @@ Memory: api_key: 你的LLM API密钥 model: qwen-plus # openai_base_url: https://api.openai.com/v1 # 可选,自定义服务地址 - # ========== Embedding 配置 ========== embedder: provider: openai # 可选: qwen, openai 等 @@ -307,7 +305,6 @@ Memory: api_key: 你的嵌入模型API密钥 model: text-embedding-v4 openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 - # ========== Database 配置 ========== # oceanbase(推荐,最佳性能), seekdb(推荐,AI应用存储一体), postgres, sqlite(轻量备选) # 用户画像功能支持: oceanbase、seekdb、sqlite @@ -322,8 +319,8 @@ Memory: # user: root@test # password: your_password # db_name: powermem - # collection_name: memories # 默认值 - # embedding_model_dims: 1536 # 嵌入向量维度,必需参数 + # collection_name: memories # 默认表名,如创建维度错误请删除此表或更改名称 + # embedding_model_dims: 1024 # 嵌入向量维度,注意跟使用模型适配,以智普为例:embedding-2的维度是1024,embedding-3的维度是2048 nomem: # 不想使用记忆功能,可以使用nomem type: nomem diff --git a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py index dc1853f7..28b78f8f 100644 --- a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py +++ b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py @@ -135,11 +135,11 @@ class MemoryProvider(MemoryProviderBase): 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}" - ) + f"database={powermem_config['vector_store']['provider']}, llm={powermem_config['llm']['provider']}, embedding={powermem_config['embedder']['provider']}" + ) except ImportError as e: logger.bind(tag=TAG).error( From 8edc23b888cad5fb98028c2c122f5c5ba6fa93d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=A0=E7=A3=8A?= Date: Fri, 9 Jan 2026 17:42:30 +0800 Subject: [PATCH 10/14] =?UTF-8?q?docs(config):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E5=92=8C=E9=9B=86=E6=88=90?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加embedding_dims配置项注释 - 同步配置文件修改到集成文档 --- docs/powermem-integration.md | 1 + main/xiaozhi-server/config.yaml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/powermem-integration.md b/docs/powermem-integration.md index 86e47925..b9a77ed5 100644 --- a/docs/powermem-integration.md +++ b/docs/powermem-integration.md @@ -65,6 +65,7 @@ Memory: api_key: 你的嵌入模型API密钥 model: text-embedding-v4 openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 + # embedding_dims: 1024 # 向量维度,非1536时需配置 # ========== Database 配置 ========== vector_store: diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 621383d5..855466db 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -305,6 +305,8 @@ Memory: api_key: 你的嵌入模型API密钥 model: text-embedding-v4 openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 + # embedding_dims: 1024 # 向量维度,非1536时需配置 + # ========== Database 配置 ========== # oceanbase(推荐,最佳性能), seekdb(推荐,AI应用存储一体), postgres, sqlite(轻量备选) # 用户画像功能支持: oceanbase、seekdb、sqlite From f0c95bc987fbf48a576f590873528063311fa95a Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Mon, 12 Jan 2026 15:06:38 +0800 Subject: [PATCH 11/14] =?UTF-8?q?=E4=B8=BA=E7=A9=BA=E6=97=B6=E4=B8=8D?= =?UTF-8?q?=E5=81=9A=E7=9B=B8=E5=85=B3=E6=9F=A5=E8=AF=A2=EF=BC=88=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E8=B0=83=E7=94=A8function=5Fcall=E6=97=B6=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E7=9B=B8=E5=85=B3query=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 449fbd2d..d90b540f 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -836,7 +836,8 @@ class ConnectionHandler: try: # 使用带记忆的对话 memory_str = None - if self.memory is not None: + # 仅当query非空(代表用户询问)时查询记忆 + if self.memory is not None and query: future = asyncio.run_coroutine_threadsafe( self.memory.query_memory(query), self.loop ) From 3bdccfea62f7347abbb0d4af3c9d147cd0be5d43 Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Mon, 12 Jan 2026 17:47:29 +0800 Subject: [PATCH 12/14] =?UTF-8?q?=E8=A7=A3=E6=9E=90json=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E4=B8=B2=E6=8F=90=E5=8F=96=E7=9B=B8=E5=85=B3=E6=96=87=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/providers/memory/mem0ai/mem0ai.py | 25 +++++++++++++++---- .../memory/mem_local_short/mem_local_short.py | 16 ++++++++++-- .../providers/memory/powermem/powermem.py | 24 ++++++++++++++---- 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/main/xiaozhi-server/core/providers/memory/mem0ai/mem0ai.py b/main/xiaozhi-server/core/providers/memory/mem0ai/mem0ai.py index a7efc36e..80dbfeb5 100644 --- a/main/xiaozhi-server/core/providers/memory/mem0ai/mem0ai.py +++ b/main/xiaozhi-server/core/providers/memory/mem0ai/mem0ai.py @@ -37,11 +37,26 @@ class MemoryProvider(MemoryProviderBase): try: # Format the content as a message list for mem0 - messages = [ - {"role": message.role, "content": message.content} - for message in msgs - if message.role != "system" - ] + messages = [] + for message in msgs: + if message.role == "system": + continue + + content = message.content + + # Extract content from JSON format if present (for ASR with emotion/language tags) + # Same logic as in query_memory method + try: + if content and content.strip().startswith("{") and content.strip().endswith("}"): + data = json.loads(content) + if "content" in data: + content = data["content"] + except (json.JSONDecodeError, KeyError, TypeError): + # If parsing fails, use original content + pass + + messages.append({"role": message.role, "content": content}) + result = self.client.add(messages, user_id=self.role_id) logger.bind(tag=TAG).debug(f"Save memory result: {result}") except Exception as e: diff --git a/main/xiaozhi-server/core/providers/memory/mem_local_short/mem_local_short.py b/main/xiaozhi-server/core/providers/memory/mem_local_short/mem_local_short.py index 5904d440..bf43d629 100644 --- a/main/xiaozhi-server/core/providers/memory/mem_local_short/mem_local_short.py +++ b/main/xiaozhi-server/core/providers/memory/mem_local_short/mem_local_short.py @@ -149,10 +149,22 @@ class MemoryProvider(MemoryProviderBase): msgStr = "" for msg in msgs: + content = msg.content + + # Extract content from JSON format if present (for ASR with emotion/language tags) + try: + if content and content.strip().startswith("{") and content.strip().endswith("}"): + data = json.loads(content) + if "content" in data: + content = data["content"] + except (json.JSONDecodeError, KeyError, TypeError): + # If parsing fails, use original content + pass + if msg.role == "user": - msgStr += f"User: {msg.content}\n" + msgStr += f"User: {content}\n" elif msg.role == "assistant": - msgStr += f"Assistant: {msg.content}\n" + msgStr += f"Assistant: {content}\n" if self.short_memory and len(self.short_memory) > 0: msgStr += "历史记忆:\n" msgStr += self.short_memory diff --git a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py index 28b78f8f..5c453c07 100644 --- a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py +++ b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py @@ -173,11 +173,25 @@ class MemoryProvider(MemoryProviderBase): 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" - ] + messages = [] + for message in msgs: + if message.role == "system": + continue + + content = message.content + + # Extract content from JSON format if present (for ASR with emotion/language tags) + # Same logic as in query_memory method + try: + if content and content.strip().startswith("{") and content.strip().endswith("}"): + data = json.loads(content) + if "content" in data: + content = data["content"] + except (json.JSONDecodeError, KeyError, TypeError): + # If parsing fails, use original content + pass + + messages.append({"role": message.role, "content": content}) # Add memory using PowerMem SDK result = self.memory_client.add( From bfa638e17ffa53d4018b92c2a0d49f518e1fb48f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=A0=E7=A3=8A?= Date: Mon, 19 Jan 2026 14:16:12 +0800 Subject: [PATCH 13/14] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=20powermem=20?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E8=87=B3=200.3.1=20=E7=89=88=E6=9C=AC,=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=A7=92=E8=89=B2=E8=BF=87=E6=BB=A4=E7=9A=84?= =?UTF-8?q?=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/xiaozhi-server/requirements.txt b/main/xiaozhi-server/requirements.txt index b343742a..b2b2565e 100644 --- a/main/xiaozhi-server/requirements.txt +++ b/main/xiaozhi-server/requirements.txt @@ -23,7 +23,7 @@ loguru==0.7.3 requests==2.32.5 cozepy==0.20.0 mem0ai==1.0.0 -powermem>=0.3.0 +powermem>=0.3.1 bs4==0.0.2 modelscope==1.32.0 sherpa_onnx==1.12.17 From 42b495d556c6966c9b08c6c6b0de8c33acb14c3f Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Thu, 5 Feb 2026 10:37:48 +0800 Subject: [PATCH 14/14] =?UTF-8?q?=E8=A1=A5=E5=85=85sql=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/db/changelog/202602051017.sql | 115 ++++++++++++++++++ .../db/changelog/db.changelog-master.yaml | 7 ++ 2 files changed, 122 insertions(+) create mode 100644 main/manager-api/src/main/resources/db/changelog/202602051017.sql diff --git a/main/manager-api/src/main/resources/db/changelog/202602051017.sql b/main/manager-api/src/main/resources/db/changelog/202602051017.sql new file mode 100644 index 00000000..c29347c1 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202602051017.sql @@ -0,0 +1,115 @@ +-- 新增powermem记忆模型供应器 +INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) +VALUES ('SYSTEM_Memory_powermem', 'Memory', 'powermem', 'PowerMem记忆', '[ + {"key":"enable_user_profile","label":"启用用户画像","type":"boolean"}, + {"key":"llm_provider","label":"LLM提供商","type":"string"}, + {"key":"llm_api_key","label":"LLM API密钥","type":"string"}, + {"key":"llm_model","label":"LLM模型","type":"string"}, + {"key":"openai_base_url","label":"OpenAI基础URL","type":"string"}, + {"key":"embedding_provider","label":"Embedding提供商","type":"string"}, + {"key":"embedding_api_key","label":"Embedding API密钥","type":"string"}, + {"key":"embedding_model","label":"Embedding模型","type":"string"}, + {"key":"embedding_openai_base_url","label":"Embedding OpenAI基础URL","type":"string"}, + {"key":"embedding_dims","label":"Embedding维度","type":"integer"}, + {"key":"vector_store","label":"向量存储配置(JSON)","type":"dict"} +]', 4, 1, NOW(), 1, NOW()); + +-- 新增PowerMem记忆模型配置 +INSERT INTO `ai_model_config` VALUES ( + 'Memory_powermem', + 'Memory', + 'powermem', + 'PowerMem记忆', + 0, + 1, + '{\"type\": \"powermem\", \"enable_user_profile\": true, \"llm_provider\": \"openai\", \"llm_api_key\": \"你的LLM API密钥\", \"llm_model\": \"qwen-plus\", \"openai_base_url\": \"\", \"embedding_provider\": \"openai\", \"embedding_api_key\": \"你的嵌入模型API密钥\", \"embedding_model\": \"text-embedding-v4\", \"embedding_openai_base_url\": \"https://api.openai.com/v1\", \"embedding_dims\": \"\", \"vector_store\": {\"provider\": \"sqlite\", \"config\": {}}}', + NULL, + NULL, + 4, + NULL, + NULL, + NULL, + NULL +); + + +-- PowerMem记忆配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/oceanbase/powermem', +`remark` = 'PowerMem是OceanBase开源的agent记忆组件,通过本地LLM进行记忆总结 +GitHub: https://github.com/oceanbase/powermem +官网: https://www.powermem.ai/ +使用示例: https://github.com/oceanbase/powermem/tree/main/examples + +【费用说明】 +PowerMem本身免费,实际费用取决于所选LLM和数据库: +- 使用sqlite + 免费LLM(如glm-4-flash) = 完全免费 +- 使用云端LLM或云端数据库 = 按对应服务收费 + +【enable_user_profile】用户画像功能 +- false: 使用普通记忆模式(AsyncMemory) +- true: 使用用户画像模式(UserMemory),自动提取用户信息 +- 用户画像功能支持: oceanbase、seekdb、sqlite (powermem 0.3.0+) + +【llm】LLM配置 - 用于记忆总结和用户画像提取 + provider: LLM提供商,可选值: + - qwen: 通义千问 (https://bailian.console.aliyun.com/?apiKey=1#/api-key) + - openai: OpenAI兼容接口 + - zhipu: 智谱AI (https://bigmodel.cn/usercenter/proj-mgmt/apikeys) - 推荐使用免费的glm-4-flash + config: LLM配置参数 + - api_key: API密钥 (必填) + - model: 模型名称,如 qwen-plus、glm-4-flash 等 + - openai_base_url: 自定义服务地址 (可选),如 https://api.openai.com/v1 + 示例: + {"provider": "zhipu", "config": {"api_key": "your_key", "model": "glm-4-flash"}} + {"provider": "qwen", "config": {"api_key": "your_key", "model": "qwen-plus"}} + +【embedder】Embedding配置 - 用于向量化记忆内容 + provider: 嵌入模型提供商,可选值: + - qwen: 通义千问 + - openai: OpenAI兼容接口 + config: Embedding配置参数 + - api_key: API密钥 (必填) + - model: 模型名称,如 text-embedding-v4、text-embedding-3-small 等 + - openai_base_url: 自定义服务地址 (可选) + - embedding_dims: 向量维度 (可选),非1536时需配置 + 示例: + {"provider": "openai", "config": {"api_key": "your_key", "model": "text-embedding-v4", "openai_base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1"}} + +【vector_store】数据库存储配置 - 用于存储向量化的记忆 + provider: 数据库类型,可选值: + - sqlite: 轻量级本地数据库 (推荐入门使用,无需额外配置) + - oceanbase: OceanBase数据库 (推荐生产使用,最佳性能) + - seekdb: SeekDB (推荐,AI应用存储一体) + - postgres: PostgreSQL数据库 + + SQLite配置 (无需额外配置): + {"provider": "sqlite", "config": {}} + + OceanBase配置示例: + {"provider": "oceanbase", "config": { + "host": "127.0.0.1", + "port": 2881, + "user": "root@test", + "password": "your_password", + "db_name": "powermem", + "collection_name": "memories", + "embedding_model_dims": 1024 + }} + 注意: + - collection_name: 默认表名,如创建维度错误请删除此表或更改名称 + - embedding_model_dims: 嵌入向量维度,需与embedder的模型维度匹配 + 例如智谱:embedding-2维度是1024,embedding-3维度是2048 + +【推荐配置组合】 +1. 完全免费方案: + - LLM: zhipu + glm-4-flash (免费) + - Embedder: 通义千问 text-embedding-v4 + - Database: sqlite + +2. 生产环境方案: + - LLM: qwen-plus 或其他商业模型 + - Embedder: text-embedding-v4 + - Database: oceanbase 或 seekdb +' +WHERE `id` = 'Memory_powermem'; diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index 720e6af5..80c011b0 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -515,3 +515,10 @@ databaseChangeLog: - sqlFile: encoding: utf8 path: classpath:db/changelog/202602021555.sql + - changeSet: + id: 202602051017 + author: RanChen + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202602051017.sql