Skip to content

Commit 2ad87a4

Browse files
authored
Merge pull request #141 from lxfight/fix/open-issues-v2.1.7
fix: resolve open issues for v2.1.7
2 parents aee47d3 + cdca9d2 commit 2ad87a4

17 files changed

Lines changed: 535 additions & 34 deletions

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
---
1212

1313
[![License](https://img.shields.io/badge/license-Custom-blue.svg)](./LICENSE)
14-
[![Version](https://img.shields.io/badge/version-v2.0.0-green.svg)](https://github.com/lxfight/astrbot_plugin_mnemosyne)
14+
[![Version](https://img.shields.io/badge/version-v2.1.7-green.svg)](https://github.com/lxfight/astrbot_plugin_mnemosyne)
1515
[![QQ Group](https://img.shields.io/badge/QQ群-953245617-blue?style=flat-square&logo=tencent-qq)](https://qm.qq.com/cgi-bin/qm/qr?k=WdyqoP-AOEXqGAN08lOFfVSguF2EmBeO&jump_from=webapi&authKey=tPyfv90TVYSGVhbAhsAZCcSBotJuTTLf03wnn7/lQZPUkWfoQ/J8e9nkAipkOzwh)
1616

1717
</div>
@@ -143,7 +143,7 @@
143143
</tr>
144144
<tr>
145145
<td><code>memory_injection_method</code></td>
146-
<td>记忆注入方式</td>
146+
<td>记忆注入方式;独立系统消息可能降低提示词缓存命中率</td>
147147
<td>user_prompt</td>
148148
</tr>
149149
<tr>
@@ -152,6 +152,16 @@
152152
<td>prepend</td>
153153
</tr>
154154
<tr>
155+
<td><code>summary_fallback_provider_id</code></td>
156+
<td>主总结模型失败或返回空内容时使用的备用模型</td>
157+
<td>空</td>
158+
</tr>
159+
<tr>
160+
<td><code>summary_speaker_mapping_prompt</code></td>
161+
<td>约束总结中 user、assistant 与第一人称的映射</td>
162+
<td>内置通用映射</td>
163+
</tr>
164+
<tr>
155165
<td><code>use_personality_filtering</code></td>
156166
<td>是否启用人格过滤</td>
157167
<td>true</td>

_conf_schema.json

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
"_special":"select_provider",
77
"default":""
88
},
9+
"summary_fallback_provider_id":{
10+
"description":"记忆总结备用 LLM 服务商",
11+
"type":"string",
12+
"hint":"主总结模型调用失败或返回空内容时重试一次。留空表示不启用备用模型",
13+
"_special":"select_provider",
14+
"default":""
15+
},
916
"embedding_provider_id":{
1017
"description":"Embedding 服务提供商",
1118
"type":"string",
@@ -266,7 +273,7 @@
266273
"memory_injection_method":{
267274
"description":"记忆插入方式",
268275
"type":"string",
269-
"hint":"user_prompt 会嵌入到用户消息的最前面,system_prompt会嵌入到系统消息中,insert_system_prompt会向上下文列表中添加一条新的系统消息",
276+
"hint":"user_prompt 注入到 LLM 用户提示词,位置由 memory_injection_position 控制;system_prompt 注入到 LLM 系统提示词;insert_system_prompt 会新增一条独立系统消息,可能降低部分模型的提示词缓存命中率",
270277
"options": ["user_prompt", "system_prompt", "insert_system_prompt"],
271278
"default": "user_prompt"
272279
},
@@ -303,10 +310,16 @@
303310
"hint":"减少模型在总结文本中臆造随机年份/日期",
304311
"default":true
305312
},
313+
"summary_speaker_mapping_prompt":{
314+
"description":"记忆总结说话人映射提示词",
315+
"type":"text",
316+
"hint":"用于约束 user/assistant 与第一人称的归属。支持 {persona_id}、{session_id}、{sender_id}、{sender_name} 变量;留空可禁用",
317+
"default":"说话人映射:assistant 表示当前会话正在运行的人格角色(persona_id={persona_id}),user 表示当前对话用户(sender_name={sender_name},sender_id={sender_id},session_id={session_id})。总结中的“我/我的”只能指 assistant 对应的人格角色;user 发言中的第一人称应改写为用户昵称、用户或其。除非原始对话或人设明确如此自称,不要把 assistant 称为 AI、助手、bot 或模型。"
318+
},
306319
"long_memory_prompt":{
307320
"description":"对话总结提示词",
308321
"type":"string",
309-
"default":"请基于以下对话内容生成一段连贯的总结性文字,要求:1. 使用单段自然语言表述,不加序号或分点;2. 聚焦提取核心要素,包括但不限于参与者身份(姓名/角色)、核心事件、关键时间节点、特殊需求、争议点及解决方案;3. 保留涉及金额/数量/规格等量化信息;4.不输出多余的解释性内容;5. 用简洁书面语整合信息,确保信息完整准确。注意:避免添加解释性内容,仅客观呈现对话要素的整合结果。6.以AI的第一人称视角记录信息",
322+
"default":"请基于以下对话内容生成一段连贯的长期记忆:1. 使用单段自然语言,不加序号或分点;2. 聚焦参与者身份、核心事件、关键时间、特殊需求、争议与解决方案;3. 保留金额、数量、规格等信息;4. 严格遵循说话人映射,不混淆 user 与 assistant 的经历和第一人称;5. 不输出解释或分析过程。",
310323
"minLength":10,
311324
"maxLength":1000
312325
}

core/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
] # 默认查询返回字段
1616
# 查询记忆条数的上限
1717
MAX_TOTAL_FETCH_RECORDS = 10000
18+
SESSION_ID_MAX_LENGTH = 500
1819

1920
# --- 对话上下文相关常量 ---
2021
DEFAULT_MAX_TURNS = 10 # 短期记忆最大对话轮数(用于总结)

core/initialization.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class DataType:
3333
DEFAULT_EMBEDDING_DIM,
3434
DEFAULT_OUTPUT_FIELDS,
3535
PRIMARY_FIELD_NAME,
36+
SESSION_ID_MAX_LENGTH,
3637
VECTOR_FIELD_NAME,
3738
)
3839
from .tools import parse_address
@@ -226,9 +227,9 @@ def initialize_config_and_schema(plugin: "Mnemosyne"):
226227
_build_field_schema(
227228
name="session_id",
228229
dtype=DataType.VARCHAR,
229-
max_length=72,
230+
max_length=SESSION_ID_MAX_LENGTH,
230231
description="会话ID",
231-
), # 增加了长度限制
232+
),
232233
_build_field_schema(
233234
name="content",
234235
dtype=DataType.VARCHAR,
@@ -765,6 +766,17 @@ def setup_vector_db_collection_and_index(
765766
# 对于 Milvus,确保索引存在
766767
db_type = plugin.config.get("vector_db_type", "chroma").lower()
767768
if db_type == "milvus":
769+
manager = getattr(plugin, "milvus_manager", None)
770+
if manager and not manager.ensure_varchar_field_max_length(
771+
collection_name,
772+
"session_id",
773+
SESSION_ID_MAX_LENGTH,
774+
):
775+
init_logger.warning(
776+
f"集合 '{collection_name}' 的 session_id 字段无法自动扩容到 "
777+
f"{SESSION_ID_MAX_LENGTH}。长会话 ID 仍可能写入失败;"
778+
"请升级 PyMilvus/Milvus 后重载插件,或按故障排查文档迁移集合。"
779+
)
768780
ensure_milvus_index(plugin, collection_name)
769781

770782
init_logger.info(

core/memory_operations.py

Lines changed: 137 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ class MilvusException(Exception):
5050

5151
logger = LogManager.GetLogger(__name__)
5252

53+
DEFAULT_SUMMARY_SPEAKER_MAPPING_PROMPT = (
54+
"说话人映射:assistant 表示当前会话正在运行的人格角色"
55+
"(persona_id={persona_id}),user 表示当前对话用户"
56+
"(sender_name={sender_name},sender_id={sender_id},session_id={session_id})。"
57+
"总结中的“我/我的”只能指 assistant 对应的人格角色;"
58+
"user 发言中的第一人称应改写为用户昵称、用户或其。"
59+
"除非原始对话或人设明确如此自称,不要把 assistant 称为 AI、助手、bot 或模型。"
60+
)
61+
5362

5463
def _get_vector_db(plugin: "Mnemosyne"):
5564
"""获取当前配置的向量数据库实例。"""
@@ -1100,8 +1109,74 @@ async def _check_summary_prerequisites(plugin: "Mnemosyne", memory_text: str) ->
11001109
return True
11011110

11021111

1112+
def _summary_response_has_text(response: Any) -> bool:
1113+
if isinstance(response, LLMResponse):
1114+
completion_text = response.completion_text
1115+
elif isinstance(response, dict):
1116+
completion_text = response.get("completion_text")
1117+
else:
1118+
return False
1119+
return isinstance(completion_text, str) and bool(completion_text.strip())
1120+
1121+
1122+
def _build_summary_speaker_mapping_prompt(
1123+
plugin: "Mnemosyne",
1124+
persona_id: str | None,
1125+
session_id: str,
1126+
context_history: list[dict] | None,
1127+
) -> str:
1128+
template = plugin.config.get(
1129+
"summary_speaker_mapping_prompt",
1130+
DEFAULT_SUMMARY_SPEAKER_MAPPING_PROMPT,
1131+
)
1132+
if not isinstance(template, str) or not template.strip():
1133+
return ""
1134+
1135+
sender_id = "UNKNOWN_USER"
1136+
sender_name = "用户"
1137+
for message in reversed(context_history or []):
1138+
if not isinstance(message, dict) or message.get("role") != "user":
1139+
continue
1140+
metadata = message.get("metadata")
1141+
if isinstance(metadata, dict):
1142+
candidate_id = metadata.get("speaker_id")
1143+
if isinstance(candidate_id, str) and candidate_id.strip():
1144+
sender_id = candidate_id.strip()
1145+
content = message.get("content")
1146+
if isinstance(content, str):
1147+
matched = re.match(r"^\[([^\]()]+)\(([^()]*)\)\]:", content)
1148+
if matched and matched.group(1).strip():
1149+
sender_name = matched.group(1).strip()
1150+
break
1151+
1152+
replacements = {
1153+
"{persona_id}": persona_id or DEFAULT_PERSONA_ON_NONE,
1154+
"{session_id}": session_id,
1155+
"{sender_id}": sender_id,
1156+
"{sender_name}": sender_name,
1157+
}
1158+
rendered = template
1159+
for placeholder, value in replacements.items():
1160+
rendered = rendered.replace(placeholder, value)
1161+
return rendered.strip()
1162+
1163+
1164+
def _get_current_summary_provider(plugin: "Mnemosyne", session_id: str):
1165+
if plugin.provider:
1166+
return plugin.provider
1167+
try:
1168+
return plugin.context.get_using_provider(umo=session_id)
1169+
except TypeError:
1170+
return plugin.context.get_using_provider()
1171+
1172+
11031173
async def _get_summary_llm_response(
1104-
plugin: "Mnemosyne", memory_text: str
1174+
plugin: "Mnemosyne",
1175+
memory_text: str,
1176+
*,
1177+
persona_id: str | None = None,
1178+
session_id: str = "",
1179+
context_history: list[dict] | None = None,
11051180
) -> LLMResponse | None:
11061181
"""
11071182
请求 LLM 进行记忆总结。
@@ -1114,17 +1189,33 @@ async def _get_summary_llm_response(
11141189
LLMResponse 对象,如果请求失败则为 None。
11151190
"""
11161191
# logger = plugin.logger
1117-
llm_provider = plugin.provider
1118-
# TODO: 优化LLM Provider获取逻辑,确保在plugin.provider不可用时能正确回退到当前使用的Provider
1192+
providers: list[tuple[str, Any]] = []
11191193
try:
1120-
if not llm_provider:
1121-
# 如果plugin.provider不正确,在这时候,使用当前使用的LLM服务商,避免错误
1122-
llm_provider = plugin.context.get_using_provider()
1123-
if not llm_provider:
1124-
logger.error("无法获取用于总结记忆的 LLM Provider。")
1125-
return None
1194+
primary_provider = _get_current_summary_provider(plugin, session_id)
1195+
if primary_provider:
1196+
providers.append(("主", primary_provider))
11261197
except Exception as e:
11271198
logger.error(f"获取 LLM Provider 时出错: {e}", exc_info=True)
1199+
1200+
fallback_provider_id = plugin.config.get("summary_fallback_provider_id", "")
1201+
if isinstance(fallback_provider_id, str) and fallback_provider_id.strip():
1202+
try:
1203+
fallback_provider = plugin.context.get_provider_by_id(
1204+
fallback_provider_id.strip()
1205+
)
1206+
if fallback_provider and all(
1207+
fallback_provider is not provider for _, provider in providers
1208+
):
1209+
providers.append(("备用", fallback_provider))
1210+
elif not fallback_provider:
1211+
logger.warning(
1212+
f"未找到备用总结 Provider: {fallback_provider_id.strip()}"
1213+
)
1214+
except Exception as e:
1215+
logger.error(f"获取备用总结 Provider 时出错: {e}", exc_info=True)
1216+
1217+
if not providers:
1218+
logger.error("无法获取用于总结记忆的 LLM Provider。")
11281219
return None
11291220

11301221
long_memory_prompt = plugin.config.get(
@@ -1152,16 +1243,38 @@ async def _get_summary_llm_response(
11521243
}
11531244
)
11541245

1155-
# M24 修复: 添加 text_chat 方法的类型忽略
1156-
llm_response = await llm_provider.text_chat( # type: ignore
1157-
prompt=memory_text,
1158-
contexts=summary_contexts,
1159-
**summary_llm_config,
1246+
speaker_mapping = _build_summary_speaker_mapping_prompt(
1247+
plugin,
1248+
persona_id,
1249+
session_id,
1250+
context_history,
11601251
)
1161-
logger.debug(f"LLM 总结响应原始数据: {llm_response}")
1162-
return llm_response
1252+
if speaker_mapping:
1253+
summary_contexts.append(
1254+
{"role": "system", "content": speaker_mapping}
1255+
)
1256+
1257+
for provider_kind, llm_provider in providers:
1258+
try:
1259+
llm_response = await llm_provider.text_chat( # type: ignore
1260+
prompt=memory_text,
1261+
contexts=summary_contexts,
1262+
**summary_llm_config,
1263+
)
1264+
logger.debug(
1265+
f"{provider_kind} LLM 总结响应原始数据: {llm_response}"
1266+
)
1267+
if _summary_response_has_text(llm_response):
1268+
return llm_response
1269+
logger.warning(f"{provider_kind}总结 Provider 返回空内容。")
1270+
except Exception as e:
1271+
logger.error(
1272+
f"{provider_kind}总结 Provider 请求失败: {e}",
1273+
exc_info=True,
1274+
)
1275+
return None
11631276
except Exception as e:
1164-
logger.error(f"LLM 总结请求失败: {e}", exc_info=True)
1277+
logger.error(f"构造总结请求时失败: {e}", exc_info=True)
11651278
return None
11661279

11671280

@@ -1328,7 +1441,13 @@ async def handle_summary_long_memory(
13281441

13291442
try:
13301443
# 1. 请求 LLM 进行总结
1331-
llm_response = await _get_summary_llm_response(plugin, memory_text)
1444+
llm_response = await _get_summary_llm_response(
1445+
plugin,
1446+
memory_text,
1447+
persona_id=persona_id,
1448+
session_id=session_id,
1449+
context_history=context_history,
1450+
)
13321451
if not llm_response:
13331452
return False
13341453

docs/en/guide/configuration.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,16 @@ See [Database Options](/en/guide/database) for backend guidance.
3535
| `num_pairs` | Conversation rounds required before automatic summarization. | `5` |
3636
| `top_k` | Number of memories returned by retrieval. | `3` |
3737
| `contexts_memory_len` | Number of long-term memories injected into context. | `3` |
38-
| `memory_injection_method` | Inject memory into the user prompt or system prompt. | `user_prompt` |
38+
| `memory_injection_method` | Use the user prompt, the LLM system prompt, or a separate system message. | `user_prompt` |
3939
| `memory_injection_position` | Inject before or after the active prompt. | `prepend` |
40+
| `summary_fallback_provider_id` | Retry with this provider when the primary summarizer fails or returns empty text. | Empty |
41+
| `summary_speaker_mapping_prompt` | Constrain `user`, `assistant`, and first-person ownership with session and speaker variables. | Built-in mapping |
4042
| `score_threshold` | Filter out memories below this similarity score. | `0.0` |
4143

4244
If memories become too noisy, lower `top_k`, increase `score_threshold`, or reduce `contexts_memory_len`.
4345

46+
`memory_injection_position` controls placement for both `user_prompt` and `system_prompt`. `insert_system_prompt` creates a separate system message and may reduce prompt-cache hits on some models; prefer the first two modes when cache stability matters.
47+
4448
## Filters
4549

4650
| Option | Description | Default |

docs/en/guide/database.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ If `persist_directory` is empty, Mnemosyne uses the default plugin data path. Se
3535
Milvus is suitable for existing Milvus deployments or larger datasets. Install the optional dependency first:
3636

3737
```bash
38-
uv pip install 'pymilvus[milvus_lite]>=2.5.4,<3.0.0'
38+
uv pip install 'pymilvus[milvus_lite]>=2.6.0,<3.0.0'
3939
```
4040

4141
Milvus Lite example:

docs/en/reference/troubleshooting.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ nc -vz localhost 19530
3939

4040
For Milvus Lite, make sure `milvus_lite_path` points to a writable path.
4141

42+
If the log contains `No module named 'pkg_resources'`, an outdated Milvus Lite build is relying on legacy setuptools behavior. Do not downgrade AstrBot's setuptools; upgrade the optional dependency instead:
43+
44+
```bash
45+
uv pip install --upgrade 'pymilvus[milvus_lite]>=2.6.0,<3.0.0'
46+
```
47+
48+
New Milvus collections use a `session_id` field length of 500. For existing standard Milvus collections, Mnemosyne attempts to expand the old 72-character field online at startup; if the server cannot alter it, upgrade PyMilvus and Milvus and reload the plugin. Milvus Lite does not currently support online field alteration, so use a new collection name or export the old collection and migrate it into a new one. Mnemosyne never drops or rebuilds existing collections automatically.
49+
4250
## Memories Disappear After Switching Databases
4351

4452
Different vector backends do not share data automatically. After switching, initialize the new backend and rebuild memories over time, or export records from the old backend and import them into the new one.

docs/guide/configuration.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,16 @@ Embedding Provider 的向量维度决定集合结构。更换 Embedding 模型
3535
| `num_pairs` | 触发自动总结的对话轮数。 | `5` |
3636
| `top_k` | 检索时返回的记忆数量。 | `3` |
3737
| `contexts_memory_len` | 注入上下文的长期记忆数量。 | `3` |
38-
| `memory_injection_method` | 记忆注入方式,可放入用户提示或系统提示| `user_prompt` |
38+
| `memory_injection_method` | `user_prompt` 注入用户提示;`system_prompt` 注入 LLM 系统提示;`insert_system_prompt` 新增独立系统消息| `user_prompt` |
3939
| `memory_injection_position` | 注入位置,可选前置或后置。 | `prepend` |
40+
| `summary_fallback_provider_id` | 主总结模型失败或返回空内容时重试的备用模型。 ||
41+
| `summary_speaker_mapping_prompt` | 约束 `user``assistant` 和第一人称归属,支持会话与说话人变量。 | 内置通用映射 |
4042
| `score_threshold` | 相似度阈值,低于阈值的记忆会被过滤。 | `0.0` |
4143

4244
如果记忆过多干扰回复,可以降低 `top_k`、提高 `score_threshold` 或减少 `contexts_memory_len`
4345

46+
`memory_injection_position` 同时控制 `user_prompt``system_prompt` 的前后位置。`insert_system_prompt` 会创建独立系统消息;部分模型会因此重新计算提示词缓存,缓存成本敏感时优先使用前两种方式。
47+
4448
## 过滤能力
4549

4650
| 配置项 | 说明 | 默认值 |

docs/guide/database.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Chroma 是默认后端。最小配置如下:
3535
Milvus 适合已有 Milvus 服务或数据规模较大的部署。启用前需要安装可选依赖:
3636

3737
```bash
38-
uv pip install 'pymilvus[milvus_lite]>=2.5.4,<3.0.0'
38+
uv pip install 'pymilvus[milvus_lite]>=2.6.0,<3.0.0'
3939
```
4040

4141
本地 Milvus Lite 示例:

0 commit comments

Comments
 (0)