Skip to content

Commit 4da06f3

Browse files
committed
feat(ltm): make user segment truncation limits configurable
1 parent c784128 commit 4da06f3

5 files changed

Lines changed: 82 additions & 9 deletions

File tree

astrbot/builtin_stars/astrbot/long_term_memory.py

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@ def cfg(self, event: AstrMessageEvent):
109109
)
110110
ltm_summary_provider_id = ltm_cfg.get("ltm_summary_provider_id", "")
111111
ltm_summary_prompt = ltm_cfg.get("ltm_summary_prompt", "")
112+
ltm_max_msgs_per_user_segment = int(
113+
ltm_cfg.get("ltm_max_msgs_per_user_segment", MAX_MSGS_PER_USER_SEGMENT)
114+
or MAX_MSGS_PER_USER_SEGMENT
115+
)
116+
ltm_max_chars_per_user_segment = int(
117+
ltm_cfg.get("ltm_max_chars_per_user_segment", MAX_CHARS_PER_USER_SEGMENT)
118+
or MAX_CHARS_PER_USER_SEGMENT
119+
)
112120
return {
113121
"image_caption": image_caption,
114122
"image_caption_prompt": image_caption_prompt,
@@ -130,6 +138,8 @@ def cfg(self, event: AstrMessageEvent):
130138
"ltm_raw_records_max_bytes": ltm_cfg.get(
131139
"ltm_raw_records_max_bytes", MAX_RAW_BYTES
132140
),
141+
"ltm_max_msgs_per_user_segment": max(1, ltm_max_msgs_per_user_segment),
142+
"ltm_max_chars_per_user_segment": max(1, ltm_max_chars_per_user_segment),
133143
}
134144

135145
# =========================================================================
@@ -266,12 +276,18 @@ async def on_req_llm(self, event: AstrMessageEvent, req: ProviderRequest) -> Non
266276
if umo not in self.raw_records:
267277
return
268278

279+
cfg = self.cfg(event)
280+
269281
raw_list = list(self.raw_records[umo])
270282
cursor = self._raw_cursor[umo]
271283
new_raw = raw_list[cursor:prompt_idx] if prompt_idx > cursor else []
272284

273285
if new_raw:
274-
new_segs = _build_segments(new_raw)
286+
new_segs = _build_segments(
287+
new_raw,
288+
max_msgs=cfg["ltm_max_msgs_per_user_segment"],
289+
max_chars=cfg["ltm_max_chars_per_user_segment"],
290+
)
275291
self.contexts[umo].extend(new_segs)
276292
self._raw_cursor[umo] = prompt_idx
277293

@@ -380,7 +396,11 @@ async def on_agent_done(
380396
cursor = self._raw_cursor[umo]
381397
remaining = raw_list[cursor:]
382398
if remaining:
383-
new_segs = _build_segments(remaining)
399+
new_segs = _build_segments(
400+
remaining,
401+
max_msgs=cfg["ltm_max_msgs_per_user_segment"],
402+
max_chars=cfg["ltm_max_chars_per_user_segment"],
403+
)
384404
self.contexts[umo].extend(new_segs)
385405
self._raw_cursor[umo] = len(raw_list)
386406

@@ -572,14 +592,18 @@ def _trim_raw_records(self, umo: str, max_bytes: int = MAX_RAW_BYTES) -> None:
572592
# =============================================================================
573593

574594

575-
def _build_segments(raw_lines: list[str]) -> list[dict]:
595+
def _build_segments(
596+
raw_lines: list[str],
597+
max_msgs: int = MAX_MSGS_PER_USER_SEGMENT,
598+
max_chars: int = MAX_CHARS_PER_USER_SEGMENT,
599+
) -> list[dict]:
576600
"""从 raw strings 构建 OpenAI 格式 contexts 段。
577601
578602
规则:
579603
1. <T:CALL>json</T:CALL> → 连续多条合并为一个 assistant(tool_calls)
580604
2. <T:RES id=xxx>content</T:RES> → tool 消息,tool_call_id 配对
581605
3. <BOT/时间>: content → assistant(纯文本)
582-
4. 其它行 → user(合并为段,段内裁剪 MAX_MSGS/MAX_CHARS
606+
4. 其它行 → user(合并为段,段内裁剪 max_msgs/max_chars
583607
"""
584608
if not raw_lines:
585609
return []
@@ -591,7 +615,7 @@ def _build_segments(raw_lines: list[str]) -> list[dict]:
591615
def flush_user():
592616
if not user_buf:
593617
return
594-
truncated = _truncate_user_segment(user_buf)
618+
truncated = _truncate_user_segment(user_buf, max_msgs, max_chars)
595619
segments.append({"role": "user", "content": "\n".join(truncated)})
596620
user_buf.clear()
597621

@@ -711,14 +735,23 @@ def _extract_tag_content(line: str, start_tag: str, end_tag: str) -> str | None:
711735
return line[len(start_tag) : -len(end_tag)].strip()
712736

713737

714-
def _truncate_user_segment(lines: list[str]) -> list[str]:
715-
"""段内裁剪:保留最近 N 条,不超字符上限。从段内最早的消息开始丢弃。"""
738+
def _truncate_user_segment(
739+
lines: list[str],
740+
max_msgs: int = MAX_MSGS_PER_USER_SEGMENT,
741+
max_chars: int = MAX_CHARS_PER_USER_SEGMENT,
742+
) -> list[str]:
743+
"""段内裁剪:保留最近 N 条,不超字符上限。从段内最早的消息开始丢弃。
744+
745+
Both limits are active simultaneously — whichever cap is hit first
746+
(by count or by chars) stops accumulation. At least one message is
747+
always retained even if it alone exceeds max_chars.
748+
"""
716749
result: list[str] = []
717750
total = 0
718751
for line in reversed(lines):
719-
if len(result) >= MAX_MSGS_PER_USER_SEGMENT:
752+
if len(result) >= max_msgs:
720753
break
721-
if total + len(line) > MAX_CHARS_PER_USER_SEGMENT and result:
754+
if total + len(line) > max_chars and result:
722755
break
723756
result.append(line)
724757
total += len(line) + 1 # +1 for \n

astrbot/core/config/default.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,12 @@
229229
"ltm_summary_provider_id": "",
230230
"ltm_summary_prompt": "",
231231
"ltm_raw_records_max_bytes": 500000,
232+
# When building user segments, both limits are active simultaneously:
233+
# whichever cap is hit first (by count or by chars) stops accumulation.
234+
# At least one message is always retained even if it alone exceeds the
235+
# character limit.
236+
"ltm_max_msgs_per_user_segment": 50,
237+
"ltm_max_chars_per_user_segment": 3000,
232238
"active_reply": {
233239
"enable": False,
234240
"method": "possibility_reply",
@@ -4199,6 +4205,16 @@
41994205
"type": "int",
42004206
"hint": "每个群聊允许 raw_records 占用的最大字节数,默认 500000 (500KB)。",
42014207
},
4208+
"provider_ltm_settings.ltm_max_msgs_per_user_segment": {
4209+
"description": "用户段最大消息数",
4210+
"type": "int",
4211+
"hint": "两次 @bot 之间积累的群聊消息合并为一个 user segment 时,最多保留多少条,默认 50。与字符上限同时生效,先到先停,至少保留一条。",
4212+
},
4213+
"provider_ltm_settings.ltm_max_chars_per_user_segment": {
4214+
"description": "用户段最大字符数",
4215+
"type": "int",
4216+
"hint": "两次 @bot 之间积累的群聊消息合并为一个 user segment 时,最多保留多少字符,默认 3000。与条数上限同时生效,先到先停,至少保留一条。",
4217+
},
42024218
"provider_ltm_settings.active_reply.enable": {
42034219
"description": "主动回复",
42044220
"type": "bool",

dashboard/src/i18n/locales/en-US/features/config-metadata.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,14 @@
10331033
"description": "Raw Message Buffer Memory Limit",
10341034
"hint": "Maximum bytes for the unprocessed message buffer per group. Prevents memory overflow in groups where the bot hasn't been @-mentioned for a long time. Default 500000 (500KB)."
10351035
},
1036+
"ltm_max_msgs_per_user_segment": {
1037+
"description": "User segment max messages",
1038+
"hint": "Maximum number of group messages between two @-mentions to retain when merging into one user segment. Default 50. Works simultaneously with the character cap — first limit hit stops accumulation. At least one message is always kept."
1039+
},
1040+
"ltm_max_chars_per_user_segment": {
1041+
"description": "User segment max characters",
1042+
"hint": "Maximum characters of group messages between two @-mentions to retain when merging into one user segment. Default 3000. Works simultaneously with the message cap — first limit hit stops accumulation. At least one message is always kept."
1043+
},
10361044
"active_reply": {
10371045
"enable": {
10381046
"description": "Active Reply"

dashboard/src/i18n/locales/ru-RU/features/config-metadata.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,14 @@
10341034
"description": "Лимит буфера сообщений",
10351035
"hint": "Максимальный размер необработанного буфера сообщений на группу. Предотвращает переполнение памяти в группах, где бот долго не упоминался. По умолчанию 500000 (500KB)."
10361036
},
1037+
"ltm_max_msgs_per_user_segment": {
1038+
"description": "Макс. сообщений в сегменте",
1039+
"hint": "Максимальное количество сообщений группы между двумя @-упоминаниями, сохраняемых в одном пользовательском сегменте. По умолчанию 50. Действует одновременно с лимитом символов — срабатывает первый достигнутый. Минимум одно сообщение сохраняется всегда."
1040+
},
1041+
"ltm_max_chars_per_user_segment": {
1042+
"description": "Макс. символов в сегменте",
1043+
"hint": "Максимальное количество символов сообщений группы между двумя @-упоминаниями, сохраняемых в одном пользовательском сегменте. По умолчанию 3000. Действует одновременно с лимитом сообщений — срабатывает первый достигнутый. Минимум одно сообщение сохраняется всегда."
1044+
},
10371045
"active_reply": {
10381046
"enable": {
10391047
"description": "Активный ответ"

dashboard/src/i18n/locales/zh-CN/features/config-metadata.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,14 @@
10351035
"description": "原始消息缓冲区内存上限",
10361036
"hint": "每个群聊的未消费消息缓冲区的最大字节数。用于防止长期未 @bot 的群内存溢出,默认 500000 (500KB)。"
10371037
},
1038+
"ltm_max_msgs_per_user_segment": {
1039+
"description": "用户段最大消息数",
1040+
"hint": "两次 @bot 之间积累的群聊消息合并为一个 user segment 时,最多保留多少条,默认 50。与字符上限同时生效,先到先停,至少保留一条。"
1041+
},
1042+
"ltm_max_chars_per_user_segment": {
1043+
"description": "用户段最大字符数",
1044+
"hint": "两次 @bot 之间积累的群聊消息合并为一个 user segment 时,最多保留多少字符,默认 3000。与条数上限同时生效,先到先停,至少保留一条。"
1045+
},
10381046
"active_reply": {
10391047
"enable": {
10401048
"description": "主动回复"

0 commit comments

Comments
 (0)