Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: add LLM direct follow-up reply gate#1850

Open
rmqg wants to merge 1 commit into
Mai-with-u:mainfrom
rmqg:feature/direct-followup-reply-gate
Open

feat: add LLM direct follow-up reply gate#1850
rmqg wants to merge 1 commit into
Mai-with-u:mainfrom
rmqg:feature/direct-followup-reply-gate

Conversation

@rmqg

@rmqg rmqg commented Jun 25, 2026

Copy link
Copy Markdown

Summary

  • add a chat config switch for LLM-based direct follow-up reply gating
  • reuse the existing forced timing continue path when a user semantically continues Mai's last message
  • add zh-CN, en-US, and ja-JP prompt templates plus schema metadata

Tests

  • uv run pytest pytests/webui/test_config_schema.py::test_direct_followup_reply_schema_metadata
  • uv run python -m compileall -q src/config/official_configs.py src/maisaka/runtime.py
  • uv run ruff check src/config/official_configs.py src/maisaka/runtime.py pytests/webui/test_config_schema.py
  • git diff --check
  • npm run build

Summary by CodeRabbit

  • 新功能
    • 新增“直接续接/后续跟进”判定能力:识别用户消息是否在语义上承接上一句回复,并在命中时进入正常回复流程。
    • 为聊天配置新增布尔开关(包含 enable_direct_followup_reply、以及相关“必回复”选项)以控制该行为。
    • 补充中英日提示模板与多语言配置说明。
  • Bug Fixes
    • 优化 WebUI 中新开关的展示元数据与一致性。
  • Tests
    • 新增配置 schema 测试,并新增直连跟进上下文拼接规则测试。

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

新增了直接接话判定配置、多语言提示词和运行时 LLM 门控,并补充了上下文判定与 schema 测试。

Changes

直接接话判定

Layer / File(s) Summary
配置开关与 schema
dashboard/src/routes/config/bot/types.ts, src/config/official_configs.py, pytests/webui/test_config_schema.py
ChatConfig 新增 inevitable_at_replyenable_direct_followup_reply,并为 enable_direct_followup_reply 补充官方配置与 WebUI schema 测试。
提示词与多语言元数据
prompts/en-US/.meta.toml, prompts/en-US/maisaka_direct_followup_gate.prompt, prompts/ja-JP/.meta.toml, prompts/ja-JP/maisaka_direct_followup_gate.prompt, prompts/zh-CN/.meta.toml, prompts/zh-CN/maisaka_direct_followup_gate.prompt
三个语言目录新增 maisaka_direct_followup_gate 的 meta 条目和 prompt 内容,输出包含 direct_followupreason 的 JSON。
运行时依赖与异步接入
src/maisaka/runtime.py
运行时新增 LLMGenerationOptionsload_promptLLMServiceClient 依赖,初始化 direct-followup gate client,并将消息触发状态更新改为异步调用。
直接接话判定与 continue 触发
src/maisaka/runtime.py, pytests/maisaka_test/test_direct_followup_context.py
运行时新增最近上下文构造、LLM 输出 JSON 解析、direct-followup 判定调用和一次性 continue 触发逻辑,并补充上下文拼接、空上下文与自发消息跳过的测试。

Sequence Diagram(s)

sequenceDiagram
  participant MaisakaHeartFlowChatting
  participant LLMServiceClient
  participant 外部LLM服务
  MaisakaHeartFlowChatting->>LLMServiceClient: generate_response(direct_followup_gate, chat_context)
  LLMServiceClient->>外部LLM服务: prompt + generation options
  外部LLM服务-->>LLMServiceClient: JSON 结果
  LLMServiceClient-->>MaisakaHeartFlowChatting: 响应文本
  MaisakaHeartFlowChatting->>MaisakaHeartFlowChatting: _parse_direct_followup_gate_result
  alt direct_followup = true
    MaisakaHeartFlowChatting->>MaisakaHeartFlowChatting: _arm_force_next_timing_continue(trigger_reason="接话消息")
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • Mai-with-u/MaiBot#1614:同样调整了 Timing Gate continue 的触发与授权路径,与本次 runtime 中的一次性 continue 逻辑直接相关。
  • Mai-with-u/MaiBot#1668:同一运行时文件内的消息触发流程也被修改,和本次 register_message / _update_message_trigger_state 的异步接入有直接代码关联。
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning 描述只包含 Summary 和 Tests,缺少模板要求的勾选项、破坏性更新、Issue、截图/GIF 和附加信息等。 按模板补全分支确认、贡献指南、更新类型、测试、A_memorix 相关确认、破坏性更新、内容目的、关联 Issue 和附加信息。
Docstring Coverage ⚠️ Warning Docstring coverage is 58.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 标题简洁且准确概括了新增的 LLM 直连续接回复门控功能。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/maisaka/runtime.py (1)

1053-1060: 🎯 Functional Correctness | 🔴 Critical

修复强制回复逻辑中的提前返回问题

reply_probability_boost >= 1.0 单独命中时,由于 Line 1058 的后半部分条件 (not message.is_at and not message.is_mentioned) 为真,会导致函数提前返回,从而丢失了强制继续的逻辑。该判断条件应予以修正,仅在明确不应强制回复时才允许提前返回。

        should_force_reply = (
            reply_probability_boost >= 1.0
            or (message.is_at and global_config.chat.inevitable_at_reply)
            or (message.is_mentioned and global_config.chat.mentioned_bot_reply)
        )
        # 修正:只有当 not should_force_reply 时才执行 fallback 逻辑
        if not should_force_reply:
            await self._maybe_arm_direct_followup_continue(message)
            return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/maisaka/runtime.py` around lines 1053 - 1060, The forced-reply guard in
runtime logic is too broad and causes an early return even when
reply_probability_boost >= 1.0 should force continuation. Update the conditional
around should_force_reply in the relevant reply-handling flow so that
_maybe_arm_direct_followup_continue(message) is called only when
should_force_reply is false, and remove the extra
message.is_at/message.is_mentioned check that overrides the forced-reply path.
🧹 Nitpick comments (1)
src/maisaka/runtime.py (1)

1166-1168: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

更新函数注释以覆盖 direct-followup 触发。

这个函数现在也服务于“接话消息”,但 docstring 仍写成只处理 @/提及,后续维护容易误删 trigger_reason 分支。按项目规则,重构时应保留并更新注释准确性。

建议修复
-        """在检测到 @ 或提及时,要求下一次 Timing Gate 直接 continue。"""
+        """在检测到强制回复触发时,要求下一次 Timing Gate 直接 continue。"""

As per coding guidelines, “Maintain good comments in the code. When refactoring, preserve existing comments unless the code is deleted; update comments for accuracy but do not remove them”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/maisaka/runtime.py` around lines 1166 - 1168, Update the docstring for
the function around trigger_reason so it accurately describes all supported
trigger types, including direct-followup/接话消息 in addition to @ and 提及消息. Keep
the existing trigger_reason branching in place and revise the comment near the
logic that sets trigger_reason so future refactors don’t remove the
direct-followup path by mistake.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/maisaka/runtime.py`:
- Around line 1109-1117: Make the gate check fully fail-closed by wrapping
prompt loading, the LLM call in generate_response, and the response parsing in
one guarded flow so any load or runtime error returns a safe non-match instead
of bubbling up and blocking message registration. Update the parser used around
the direct_followup/reason extraction to validate that the decoded JSON is a
dict before accessing keys, and keep the existing invalid_json warning/false
fallback path in the same handling around the relevant runtime helpers.
- Around line 709-710: The message handling flow in runtime should not wait for
the direct-followup LLM gate before adding the new message to cache and
continuing local processing. In the message ingestion path around
_update_message_trigger_state and message_cache.append, move the cache append
and any local `@/mention` marking ahead of the external gate, then run the
direct-followup check in a background task with a timeout. If the gate later
passes, call _schedule_message_turn() from that async follow-up path; apply the
same reordering to the related logic in the other referenced block as well.
- Around line 1074-1075: The is_bot_self check is being called with the whole
SessionMessage object instead of the required platform and user_id values, which
can raise a TypeError in register_message when direct-followup is enabled and
text is present. Update the call at the message handling path in runtime.py to
pass the same platform and user_id fields used elsewhere around
register_message, so the bot-self filter works with the correct signature and
the function can continue normally.

---

Outside diff comments:
In `@src/maisaka/runtime.py`:
- Around line 1053-1060: The forced-reply guard in runtime logic is too broad
and causes an early return even when reply_probability_boost >= 1.0 should force
continuation. Update the conditional around should_force_reply in the relevant
reply-handling flow so that _maybe_arm_direct_followup_continue(message) is
called only when should_force_reply is false, and remove the extra
message.is_at/message.is_mentioned check that overrides the forced-reply path.

---

Nitpick comments:
In `@src/maisaka/runtime.py`:
- Around line 1166-1168: Update the docstring for the function around
trigger_reason so it accurately describes all supported trigger types, including
direct-followup/接话消息 in addition to @ and 提及消息. Keep the existing trigger_reason
branching in place and revise the comment near the logic that sets
trigger_reason so future refactors don’t remove the direct-followup path by
mistake.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 23e32a71-9949-4fc7-8fea-97e8e9712cf4

📥 Commits

Reviewing files that changed from the base of the PR and between 071474c and 5b1c118.

📒 Files selected for processing (10)
  • dashboard/src/routes/config/bot/types.ts
  • prompts/en-US/.meta.toml
  • prompts/en-US/maisaka_direct_followup_gate.prompt
  • prompts/ja-JP/.meta.toml
  • prompts/ja-JP/maisaka_direct_followup_gate.prompt
  • prompts/zh-CN/.meta.toml
  • prompts/zh-CN/maisaka_direct_followup_gate.prompt
  • pytests/webui/test_config_schema.py
  • src/config/official_configs.py
  • src/maisaka/runtime.py

Comment thread src/maisaka/runtime.py
Comment on lines +709 to 710
await self._update_message_trigger_state(message)
self.message_cache.append(message)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

不要在消息入缓存前等待 direct-followup LLM 门控。

Line 709 会在 message_cache.append() 之前等待一次外部 LLM 调用;如果模型慢、排队或无响应,这条新消息不会入缓存、不会广播监控、也不会进入调度。建议先完成本地 @/提及标记和消息入队,再把 direct-followup gate 放到带超时的后台任务中;命中后再 _schedule_message_turn()

Also applies to: 1125-1138

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/maisaka/runtime.py` around lines 709 - 710, The message handling flow in
runtime should not wait for the direct-followup LLM gate before adding the new
message to cache and continuing local processing. In the message ingestion path
around _update_message_trigger_state and message_cache.append, move the cache
append and any local `@/mention` marking ahead of the external gate, then run the
direct-followup check in a background task with a timeout. If the gate later
passes, call _schedule_message_turn() from that async follow-up path; apply the
same reordering to the related logic in the other referenced block as well.

Comment thread src/maisaka/runtime.py Outdated
Comment thread src/maisaka/runtime.py
Comment on lines +1109 to +1117
try:
payload = json.loads(normalized_text)
except json.JSONDecodeError:
logger.warning(f"接话必回复判定结果不是有效 JSON,将按未命中处理: {response_text[:200]}")
return False, "invalid_json"

direct_followup = bool(payload.get("direct_followup"))
reason = str(payload.get("reason") or "").strip()
return direct_followup, reason

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

让门控解析和 prompt 加载也 fail-closed。

当前只有 generate_responsetry 内;load_prompt 失败,或 LLM 返回合法 JSON 但顶层不是对象时,异常会向上传播并打断消息注册。把 prompt 加载、调用、解析放进同一个 fail-closed 块,并在解析器里校验 dict

建议修复
         try:
+            prompt = load_prompt(
+                "maisaka_direct_followup_gate",
+                bot_name=global_config.bot.nickname,
+                chat_context=context_text,
+            )
             result = await self._direct_followup_gate_client.generate_response(
                 prompt,
                 LLMGenerationOptions(temperature=0.0, max_tokens=180),
             )
+            direct_followup, reason = self._parse_direct_followup_gate_result(result.response or "")
         except Exception:
             logger.exception(f"{self.log_prefix} 接话必回复 LLM 判定失败,按未命中处理")
             return
 
-        direct_followup, reason = self._parse_direct_followup_gate_result(result.response or "")
         try:
             payload = json.loads(normalized_text)
         except json.JSONDecodeError:
             logger.warning(f"接话必回复判定结果不是有效 JSON,将按未命中处理: {response_text[:200]}")
             return False, "invalid_json"
+        if not isinstance(payload, dict):
+            logger.warning(f"接话必回复判定结果 JSON 顶层不是对象,将按未命中处理: {response_text[:200]}")
+            return False, "invalid_json_shape"
 
         direct_followup = bool(payload.get("direct_followup"))

Also applies to: 1129-1143

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/maisaka/runtime.py` around lines 1109 - 1117, Make the gate check fully
fail-closed by wrapping prompt loading, the LLM call in generate_response, and
the response parsing in one guarded flow so any load or runtime error returns a
safe non-match instead of bubbling up and blocking message registration. Update
the parser used around the direct_followup/reason extraction to validate that
the decoded JSON is a dict before accessing keys, and keep the existing
invalid_json warning/false fallback path in the same handling around the
relevant runtime helpers.

@rmqg rmqg force-pushed the feature/direct-followup-reply-gate branch from 5b1c118 to f5ea8a9 Compare June 25, 2026 04:15

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/maisaka/runtime.py`:
- Around line 1121-1123: The direct_followup parsing in the payload handling is
too loose because bool() treats non-empty strings like "false" as true. Update
the logic in the relevant runtime parser around the direct_followup/reason
extraction to strictly interpret only real boolean values (or explicit string
forms like true/false) and default everything else safely, so the follow-up flag
cannot be accidentally enabled by an LLM string response.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 180816ad-de7c-4db7-b1e9-7897c4d38af0

📥 Commits

Reviewing files that changed from the base of the PR and between 5b1c118 and f5ea8a9.

📒 Files selected for processing (11)
  • dashboard/src/routes/config/bot/types.ts
  • prompts/en-US/.meta.toml
  • prompts/en-US/maisaka_direct_followup_gate.prompt
  • prompts/ja-JP/.meta.toml
  • prompts/ja-JP/maisaka_direct_followup_gate.prompt
  • prompts/zh-CN/.meta.toml
  • prompts/zh-CN/maisaka_direct_followup_gate.prompt
  • pytests/maisaka_test/test_direct_followup_context.py
  • pytests/webui/test_config_schema.py
  • src/config/official_configs.py
  • src/maisaka/runtime.py
✅ Files skipped from review due to trivial changes (2)
  • prompts/ja-JP/.meta.toml
  • prompts/en-US/.meta.toml
🚧 Files skipped from review as they are similar to previous changes (7)
  • dashboard/src/routes/config/bot/types.ts
  • prompts/ja-JP/maisaka_direct_followup_gate.prompt
  • pytests/webui/test_config_schema.py
  • prompts/en-US/maisaka_direct_followup_gate.prompt
  • src/config/official_configs.py
  • prompts/zh-CN/maisaka_direct_followup_gate.prompt
  • prompts/zh-CN/.meta.toml

Comment thread src/maisaka/runtime.py
Comment on lines +1121 to +1123
direct_followup = bool(payload.get("direct_followup"))
reason = str(payload.get("reason") or "").strip()
return direct_followup, reason

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

严格解析 direct_followup 布尔值,避免 "false" 被当成命中。

bool("false") 会得到 True,LLM 若返回字符串 "false" 会误触发强制接话。

建议修复
-        direct_followup = bool(payload.get("direct_followup"))
+        direct_followup_value = payload.get("direct_followup")
+        if not isinstance(direct_followup_value, bool):
+            logger.warning(
+                f"接话必回复判定 direct_followup 字段不是布尔值,将按未命中处理: "
+                f"{response_text[:200]}"
+            )
+            return False, "invalid_direct_followup_type"
+        direct_followup = direct_followup_value
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
direct_followup = bool(payload.get("direct_followup"))
reason = str(payload.get("reason") or "").strip()
return direct_followup, reason
direct_followup_value = payload.get("direct_followup")
if not isinstance(direct_followup_value, bool):
logger.warning(
f"接话必回复判定 direct_followup 字段不是布尔值,将按未命中处理: "
f"{response_text[:200]}"
)
return False, "invalid_direct_followup_type"
direct_followup = direct_followup_value
reason = str(payload.get("reason") or "").strip()
return direct_followup, reason
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/maisaka/runtime.py` around lines 1121 - 1123, The direct_followup parsing
in the payload handling is too loose because bool() treats non-empty strings
like "false" as true. Update the logic in the relevant runtime parser around the
direct_followup/reason extraction to strictly interpret only real boolean values
(or explicit string forms like true/false) and default everything else safely,
so the follow-up flag cannot be accidentally enabled by an LLM string response.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant