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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .astrbot-plugin/i18n/en-US.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"metadata": {
"display_name": "Simple Long Memory",
"short_desc": "Knowledge-base backed long-term memory, recall, and management tools.",
"desc": "Adds long-term memory to AstrBot using the built-in knowledge base for user preferences, past interactions, and important facts."
},
"config": {
"kb_name": {
"description": "Memory Knowledge Base",
"hint": "Select the knowledge base used to store long-term memories. Required. Create one first in Knowledge Base Management and configure an embedding model."
},
"extraction_provider_id": {
"description": "Memory Extraction Model",
"hint": "LLM used to extract memories from conversations. Leave empty to use the current chat model."
},
"summarization_provider_id": {
"description": "Memory Summarization Model",
"hint": "LLM used for future memory merging and compression. Leave empty to use the current chat model."
},
"auto_memorize": {
"description": "Automatic Memory Mode",
"hint": "When enabled, memories are automatically extracted and stored after conversations."
},
"extraction_interval": {
"description": "Extraction Interval (turns)",
"hint": "Run memory extraction every N conversation turns and summarize those turns."
},
"extraction_min_content_length": {
"description": "Minimum Extraction Length",
"hint": "Skip memory extraction when the accumulated conversation is shorter than this many characters."
},
"global_memory": {
"description": "Cross-Session Personal Recall",
"hint": "When enabled, personal memories can be recalled across sessions for the same user. When disabled, only personal memories from the current session are recalled."
},
"max_memories_per_inject": {
"description": "Memories Per Injection",
"hint": "Maximum number of memories injected into each LLM request."
},
"max_memory_list_scan": {
"description": "Memory List Scan Limit",
"hint": "Limits how many records /memory list scans to calculate memories visible to the current user. The actual scan amount is derived from the page and page size and will not exceed this limit."
},
"memory_delete_scan_page_size": {
"description": "Delete Scan Page Size",
"hint": "Number of matched records scanned per page before deleting or clearing memories. Larger values are faster but use more memory per operation."
},
"memory_ttl_days": {
"description": "Memory TTL (days)",
"hint": "Low-frequency memories older than this many days may be compressed into impressions."
},
"use_reranker": {
"description": "Enable Reranker",
"hint": "Use the knowledge base reranker to improve memory recall ordering. Requires a reranker model configured on the knowledge base."
},
"optimize_recall_query": {
"description": "Optimize Recall Query",
"hint": "Before each memory recall, call the extraction model to extract search keywords for better recall accuracy. This runs on every conversation, so use a fast lightweight model."
},
"optimize_recall_query_timeout": {
"description": "Recall Optimization Timeout (seconds)",
"hint": "Maximum wait time for recall query optimization. On timeout, the original query is used so conversation responses are not blocked."
},
"enable_admin_global_memory_tool": {
"description": "Enable Admin Global Memory Tool",
"hint": "When enabled, provides the memory_store_global tool so administrators can ask AI to add global memories that apply to all chats."
}
}
}
69 changes: 69 additions & 0 deletions .astrbot-plugin/i18n/zh-CN.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"metadata": {
"display_name": "简单长期记忆",
"short_desc": "基于知识库的长期记忆、召回和管理工具。",
"desc": "为 AstrBot 提供长期记忆能力,基于内置知识库实现用户偏好、历史交互和重要事实的记忆存储与召回。"
},
"config": {
"kb_name": {
"description": "记忆知识库",
"hint": "选择用于存储长期记忆的知识库(必填,请先在知识库管理中创建,知识库需配置嵌入模型)"
},
"extraction_provider_id": {
"description": "记忆提取模型",
"hint": "用于从对话中提取记忆的 LLM 模型(留空则使用会话主 LLM)"
},
"summarization_provider_id": {
"description": "记忆总结模型",
"hint": "用于记忆合并、压缩等总结操作的 LLM 模型(留空则使用会话主 LLM)"
},
"auto_memorize": {
"description": "自动记忆模式",
"hint": "开启后会在对话后自动提取并存储记忆"
},
"extraction_interval": {
"description": "记忆提取间隔(轮)",
"hint": "每 N 轮对话后触发一次记忆提取,对这 N 轮对话内容进行总结"
},
"extraction_min_content_length": {
"description": "最小提取内容长度",
"hint": "对话总内容低于此字符数时跳过记忆提取,避免无意义短对话被记忆"
},
"global_memory": {
"description": "跨会话召回个人记忆",
"hint": "开启后同一用户可跨会话召回个人记忆;关闭后仅召回当前会话的个人记忆"
},
"max_memories_per_inject": {
"description": "每次注入的记忆数量",
"hint": "LLM 请求时注入的最大记忆条数"
},
"max_memory_list_scan": {
"description": "记忆列表扫描上限",
"hint": "限制 /memory list 为计算当前用户可见记忆而扫描的最大记录数。实际扫描量会按当前页码和每页数量推导,并不会超过此上限"
},
"memory_delete_scan_page_size": {
"description": "记忆删除扫描分页大小",
"hint": "删除或清空记忆前,每页扫描多少条匹配记录用于同步清理知识库文档记录。数值越大速度越快但单次内存占用更高"
},
"memory_ttl_days": {
"description": "记忆生命周期(天)",
"hint": "超过此天数的低频记忆将被压缩为印象"
},
"use_reranker": {
"description": "启用重排序",
"hint": "记忆召回时使用知识库配置的重排序模型优化结果排序。需先在知识库设置中配置重排序模型,未配置时此选项无效"
},
"optimize_recall_query": {
"description": "启用检索优化",
"hint": "开启后在每次记忆召回前调用记忆提取模型提炼检索关键词,提高召回准确率。每次对话都会调用,建议配置响应速度快的轻量模型"
},
"optimize_recall_query_timeout": {
"description": "检索优化超时(秒)",
"hint": "限制检索优化模型调用的最长等待时间。超时后将跳过优化并使用原始检索内容,避免阻塞对话响应"
},
"enable_admin_global_memory_tool": {
"description": "启用管理员全局记忆工具",
"hint": "开启后提供 memory_store_global 工具,仅管理员可通过 AI 添加对所有会话生效的全局记忆"
}
}
}
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## v0.3.2 (2026-05-14)

### 新增
- 新增 AstrBot 插件 i18n 配置,提供 `zh-CN` / `en-US` 双语 WebUI 元数据和配置项文案。
- 新增 `enable_admin_global_memory_tool` 配置和 `memory_store_global` LLM 工具。开启后管理员可指挥 AI 写入 `global` 作用域记忆,后续所有会话都会参与召回。

### 变更
- 记忆注入优先使用 AstrBot v4.24+ 的临时用户内容区,并标记为本轮临时内容,避免写入会话历史;旧版回退到最早的 `user` 上下文位置,不再拼接到当前 prompt 前。
- 记忆注入包裹说明强化为“长期记忆检索参考”,明确不是当前正在发生的事情,也不是用户指令;同时在顶部提醒 AI 必要时使用 `memory_recall(query)` 工具继续搜索更多记忆。

## v0.3.1 (2026-05-03)

### 新增
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ https://github.com/piexian/astrbot_plugin_simple_long_memory
| use_reranker | 记忆召回时启用重排序(需知识库已配置重排序模型) | `true` |
| optimize_recall_query | 启用检索优化(LLM 提炼关键词) | `false` |
| optimize_recall_query_timeout | 检索优化超时(秒) | `10` |
| enable_admin_global_memory_tool | 启用管理员全局记忆工具 | `false` |

## 使用方法

Expand Down Expand Up @@ -68,6 +69,7 @@ https://github.com/piexian/astrbot_plugin_simple_long_memory

| 作用域 | 说明 | 日常例子 |
|--------|------|----------|
| `global` | 全局记忆,所有会话可召回(仅管理员工具可写入) | "机器人回答某项目问题时优先使用内部术语表" |
| `personal` | 个人记忆,仅自己可见 | "我比较喜欢喝拿铁"、"下周要出差" |
| `group` | 群共享记忆,群友都可见 | "群里约了每周五打游戏"、"这个群的固定梗" |
| `conversation` | 当前会话临时上下文 | "刚才说的那个 bug 还没修完" |
Expand Down Expand Up @@ -109,6 +111,7 @@ AI 可以通过以下工具主动操作记忆:

- `memory_recall(query)` — 搜索长期记忆
- `memory_store(content, memory_type, disclosure)` — 存储记忆
- `memory_store_global(content, memory_type, disclosure)` — 存储全局记忆(需开启 `enable_admin_global_memory_tool`,仅管理员可用)
- `memory_forget(uri)` — 删除记忆

### 记忆类型
Expand All @@ -122,7 +125,7 @@ AI 可以通过以下工具主动操作记忆:

## 工作原理

1. **记忆注入**:在每次 LLM 请求前,根据用户输入通过 embedding 检索召回相关记忆,以 `user` 角色注入到对话上下文顶部(不占用 system prompt)
1. **记忆注入**:在每次 LLM 请求前,根据用户输入通过 embedding 检索召回相关记忆;AstrBot v4.24+ 优先注入到临时用户内容区(仅本轮请求生效),旧版回退到最早的 `user` 上下文位置(不占用 system prompt,不覆盖当前输入
2. **自动提取**:每隔 `extraction_interval` 轮对话,将累积的对话内容发送给 LLM 提取值得记忆的信息并自动存储
3. **用户隔离**:所有记忆操作通过 metadata 中的 `user_id` 字段过滤,确保用户间记忆完全隔离
4. **记忆存储**:记忆以向量形式存储在知识库中,支持语义检索
Expand Down
6 changes: 6 additions & 0 deletions _conf_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,11 @@
"default": 10,
"hint": "限制检索优化模型调用的最长等待时间。超时后将跳过优化并使用原始检索内容,避免阻塞对话响应",
"slider": {"min": 1, "max": 20, "step": 1}
},
"enable_admin_global_memory_tool": {
"description": "启用管理员全局记忆工具",
"type": "bool",
"default": false,
"hint": "开启后提供 memory_store_global 工具,仅管理员可通过 AI 添加对所有会话生效的全局记忆"
}
}
122 changes: 106 additions & 16 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,13 @@ def _sanitize_string_list(value: Any, limit: int = 8) -> list[str]:


def _normalize_extracted_scope(scope: str, session_type: str) -> str:
"""规范化自动提取的记忆作用域。

自动提取不能写入全局记忆;全局记忆只能由管理员工具显式创建。
"""
scope = normalize_memory_scope(scope)
if scope == MemoryScope.GLOBAL:
return MemoryScope.PERSONAL
if session_type != "group" and scope == MemoryScope.GROUP:
return MemoryScope.PERSONAL
return scope
Expand Down Expand Up @@ -133,6 +139,54 @@ def _build_recall_query(prompt: str, contexts: list[dict[str, Any]]) -> str:
return "\n".join(parts)


def _make_memory_text_part(content: str) -> Any | None:
"""构造 AstrBot 用户内容块;新版支持标记为本轮临时内容。"""
try:
from astrbot.core.agent.message import TextPart
except Exception:
return None

part = TextPart(text=content)
mark_as_temp = getattr(part, "mark_as_temp", None)
if callable(mark_as_temp):
marked_part = mark_as_temp()
if marked_part is not None:
part = marked_part
return part


def _append_extra_user_content(request: ProviderRequest, content: str) -> bool:
"""优先使用 AstrBot 动态用户内容块注入记忆。"""
if not hasattr(request, "extra_user_content_parts"):
return False

parts = getattr(request, "extra_user_content_parts", None)
if parts is None:
parts = []
request.extra_user_content_parts = parts
if not hasattr(parts, "append"):
return False

part = _make_memory_text_part(content)
if part is None:
return False

parts.append(part)
return True


def _inject_memory_context(request: ProviderRequest, content: str) -> str:
"""注入记忆上下文,返回实际使用的注入位置。"""
if _append_extra_user_content(request, content):
return "extra_user_content_parts"

contexts = _normalize_contexts(getattr(request, "contexts", None))
memory_msg = {"role": "user", "content": content}
# 旧版回退:放在 contexts 最前面,使当前 prompt 仍保持最后、优先级更高。
request.contexts = [memory_msg] + contexts
return "contexts 顶部"


def _clamp_timeout(
value: Any, default: int = DEFAULT_RECALL_QUERY_OPTIMIZATION_TIMEOUT
) -> int:
Expand Down Expand Up @@ -770,22 +824,10 @@ async def inject_memories(self, event: AstrMessageEvent, request: ProviderReques
# format_memory_for_injection 已返回含 <user_context_reference> 包装的完整字符串
safe_memory_context = format_memory_for_injection(memories)
if safe_memory_context:
# 优先注入到 contexts 顶部(如果存在)
# 使用 user 角色而非 system,降低优先级
if contexts:
memory_msg = {"role": "user", "content": safe_memory_context}
request.contexts = [memory_msg] + contexts
logger.debug(
f"[简单长期记忆] 注入 {len(memories)} 条记忆到 contexts 顶部"
)
else:
# 回退:注入到 prompt 前面
request.prompt = (
f"{safe_memory_context}\n\n{request.prompt or ''}"
)
logger.debug(
f"[简单长期记忆] 注入 {len(memories)} 条记忆到 prompt 前"
)
inject_target = _inject_memory_context(request, safe_memory_context)
logger.debug(
f"[简单长期记忆] 注入 {len(memories)} 条记忆到 {inject_target}"
)

except Exception as e:
logger.warning(f"[简单长期记忆] 注入记忆失败: {e}")
Expand Down Expand Up @@ -1459,6 +1501,54 @@ async def tool_store(
)
return f"Memory stored: {uri}"

@filter.llm_tool(name="memory_store_global")
async def tool_store_global(
self,
event: AstrMessageEvent,
content: str,
memory_type: str = "fact",
disclosure: str = "",
) -> str:
"""Store a global long-term memory visible to all chats.

Only use this when the current user is an admin and explicitly asks to
add a global memory, global rule, shared fact, or bot-wide preference.

Args:
content(string): global content to remember for all users/chats
memory_type(string): memory type (fact/preference/event/context)
disclosure(string): condition description for triggering recall
"""
if not self.config.get("enable_admin_global_memory_tool", False):
return "Global memory tool is disabled in plugin config"

if not event.is_admin():
return "Only administrators can store global memories"

if not self.memory_mgr:
return "Memory plugin not initialized"

content = _sanitize_memory_content(content)
if not content:
return "Invalid memory content"

domain = normalize_domain(memory_type)
uri = str(MemoryURI.generate(domain))

await self.memory_mgr.store_memory(
event=event,
content=content,
domain=domain,
uri=uri,
memory_type=MemoryType.PERMANENT,
disclosure=_sanitize_memory_content(disclosure)[:200] if disclosure else "",
importance=5,
memory_scope=MemoryScope.GLOBAL,
visibility="group",
subject="global",
)
return f"Global memory stored: {uri}"

@filter.llm_tool(name="memory_forget")
async def tool_forget(self, event: AstrMessageEvent, uri: str) -> str:
"""Delete a specific memory by URI
Expand Down
Loading
Loading