Skip to content

feat(chat): awakening module with 6 trigger dimensions#28

Merged
3aKHP merged 7 commits into
mainfrom
feat/v1.7.0-awakening-module
May 26, 2026
Merged

feat(chat): awakening module with 6 trigger dimensions#28
3aKHP merged 7 commits into
mainfrom
feat/v1.7.0-awakening-module

Conversation

@3aKHP
Copy link
Copy Markdown
Owner

@3aKHP 3aKHP commented May 26, 2026

Summary

实现 AstrBot 风格的多维度唤醒系统,为 LLM 触发提供 6 种软触发条件。所有功能默认关闭(阈值 = 0 或 >= 1 时不产生任何 LLM 调用)。

6 种唤醒触发(优先级从高到低):

  • awakening_extend — 唤醒延长:LLM 回复后 N 秒内同一用户继续发言自动触发
  • awakening_relevance — 相关性唤醒:用户消息延续 bot 之前的对话(词重叠快筛 + LLM 仲裁)
  • awakening_qa — 答疑唤醒:检测到专业性问题(正则快筛 + LLM 仲裁)
  • awakening_interest — 兴趣话题:关键词匹配,支持全局 + 人格独立配置
  • awakening_fallback — 兜底概率:随机触发
  • awakening_boredom — 无聊唤醒:群聊沉寂检测,支持免打扰时间区间

基础设施:

  • config/awakening.toml — 配置文件,支持按群覆盖
  • [triggers.quick_judge] in llm.toml — 可配置的快速判定专用模型
  • BotMessageCache — 按群缓存 bot 最近 5 条回复
  • LLM 结果缓存(60s TTL)
  • /awakening 命令 — 运行时管理(status/on/off/boredom)

Test plan

  • 66 个单元测试覆盖所有触发类型、配置加载、状态管理、缓存机制
  • 手动测试:设置 interest_topics 发送匹配消息验证触发
  • 手动测试:设置 relevance_threshold = 0.3 验证相关性唤醒
  • 手动测试:设置 qa_threshold = 0.5 验证答疑唤醒
  • 手动测试:验证 threshold >= 1.0 时不产生 LLM 调用
  • 验证 /awakening status 命令输出

3aKHP added 2 commits May 27, 2026 05:01
Implements AstrBot-style multi-dimensional awakening system for LLM triggering. All features default to off (zero LLM calls when disabled).

Six awakening triggers (priority order):
- awakening_extend: keep conversation active within time window
- awakening_relevance: user message continues bot conversation (word overlap fast filter + LLM judge)
- awakening_qa: professional question detection (regex fast filter + LLM judge)
- awakening_interest: keyword match (global + per-persona topics)
- awakening_fallback: random probability
- awakening_boredom: group silence detection with DND support

Infrastructure:
- config/awakening.toml for per-group overrides
- [triggers.quick_judge] in llm.toml for dedicated judge model
- BotMessageCache for relevance checking
- LLM result cache with 60s TTL
- /awakening command for runtime management
- 66 unit tests covering all trigger types
Copy link
Copy Markdown

@khpilot khpilot Bot left a comment

Choose a reason for hiding this comment

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

Bot Review

The awakening module adds 6 trigger dimensions with LLM-based arbitration, config-driven defaults/overrides, and a scheduler-based boredom check. The core architecture is sound, but there is one correctness bug where boredom cooldown mutates before rate-limit gating, plus dead code and an unsafe deserialization path that can crash the scheduler.

🔴 HIGH: check_boredom() calls st.mark_boredom_triggered(group_id) before the caller (run_boredom_check) checks the rate limiter. If rate_limiter.allow() blocks, the group's boredom cooldown is still consumed — the group won't be eligible again for check_interval seconds despite no message being sent.

File: quickquip/chat/awakening.py

Move the mark_boredom_triggered() call out of check_boredom() into run_boredom_check(), placing it after the rate_limiter.allow() check succeeds. This way the cooldown only resets when a message is actually dispatched.

🟡 MEDIUM: Dead code: _extract_json_trigger() is defined but never called. The identical inline logic appears in _llm_judge() at line 446.

File: quickquip/chat/awakening.py

Either delete _extract_json_trigger() or refactor _llm_judge() to call it (prefer the latter to keep trigger parsing in one place).

🟡 MEDIUM: Unused imports: asyncio (line 3) and time/time() (line 7) are imported but never referenced in the file.

File: quickquip/adapters/nonebot/awakening_plugin.py

Remove both unused imports to keep the module clean and avoid misleading readers.

🟡 MEDIUM: BoredomEnabledGroups.load() does not validate group IDs. If the JSON file is manually edited to contain non-numeric strings (e.g. 'abc'), run_boredom_check() will call int(gid) at awakening.py line 750 which raises ValueError and crashes the scheduler job, disabling boredom for all groups.

File: quickquip/adapters/nonebot/awakening_plugin.py

Filter _groups during load() with _safe_group_id() validation, or at minimum wrap int(gid) in run_boredom_check() with a try/except and log+skip invalid entries.

🟢 LOW: awakening_state.record_message() is called at line 68 in group_messages.py AND again inside check_awakening_triggers() at awakening.py line 681. This double-records _last_message_times on every message, which is harmless but wasteful.

File: quickquip/adapters/nonebot/group_messages.py

Remove the record_message() call inside check_awakening_triggers() since the caller already records it before invocation.

🟢 LOW: Boredom messages sent via run_boredom_check() bypass the normal pipeline — awakening_state.bot_messages.add() is never called for them. Consequently, if a user replies to a boredom-initiated message, relevance detection won't find the bot's message in BotMessageCache.

File: quickquip/chat/awakening.py

After sending the boredom message, call awakening_state.bot_messages.add(gid, reply_result['reply']) to include it in relevance context for subsequent messages.


Automated review. Reply with @KHPilot[bot] to ask follow-up questions.

Comment thread quickquip/chat/awakening.py Outdated
return None
if random.random() >= settings.boredom_probability:
return None
st.mark_boredom_triggered(group_id)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH: check_boredom() calls st.mark_boredom_triggered(group_id) before the caller (run_boredom_check) checks the rate limiter. If rate_limiter.allow() blocks, the group's boredom cooldown is still consumed — the group won't be eligible again for check_interval seconds despite no message being sent.

Move the mark_boredom_triggered() call out of check_boredom() into run_boredom_check(), placing it after the rate_limiter.allow() check succeeds. This way the cooldown only resets when a message is actually dispatched.

Comment thread quickquip/chat/awakening.py Outdated
return False


def _extract_json_trigger(raw: str) -> bool:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM: Dead code: _extract_json_trigger() is defined but never called. The identical inline logic appears in _llm_judge() at line 446.

Either delete _extract_json_trigger() or refactor _llm_judge() to call it (prefer the latter to keep trigger parsing in one place).

@@ -0,0 +1,243 @@
from __future__ import annotations

import asyncio
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM: Unused imports: asyncio (line 3) and time/time() (line 7) are imported but never referenced in the file.

Remove both unused imports to keep the module clean and avoid misleading readers.

try:
with self.path.open("r", encoding="utf-8") as f:
data = json.load(f)
self._groups = {str(g) for g in data.get("enabled", [])}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM: BoredomEnabledGroups.load() does not validate group IDs. If the JSON file is manually edited to contain non-numeric strings (e.g. 'abc'), run_boredom_check() will call int(gid) at awakening.py line 750 which raises ValueError and crashes the scheduler job, disabling boredom for all groups.

Filter _groups during load() with _safe_group_id() validation, or at minimum wrap int(gid) in run_boredom_check() with a try/except and log+skip invalid entries.

from quickquip.adapters.nonebot.voice import append_voice_transcripts, transcribe_message_records
from quickquip.app.message_pipeline import (
_ensure_llm_bindings,
awakening_state,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LOW: awakening_state.record_message() is called at line 68 in group_messages.py AND again inside check_awakening_triggers() at awakening.py line 681. This double-records _last_message_times on every message, which is harmless but wasteful.

Remove the record_message() call inside check_awakening_triggers() since the caller already records it before invocation.

recent_messages=trigger_context,
message_id=None,
)
await bot.send_group_msg(group_id=int(gid), message=reply_result["reply"])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LOW: Boredom messages sent via run_boredom_check() bypass the normal pipeline — awakening_state.bot_messages.add() is never called for them. Consequently, if a user replies to a boredom-initiated message, relevance detection won't find the bot's message in BotMessageCache.

After sending the boredom message, call awakening_state.bot_messages.add(gid, reply_result['reply']) to include it in relevance context for subsequent messages.

3aKHP added 5 commits May 27, 2026 05:28
- Move mark_boredom_triggered() after rate_limiter check and send success (was consuming cooldown on rate-limit reject or LLM failure)
- Make _extract_json_trigger() robust: regex match + strip markdown code fences
- Remove dead code: deduplicate _extract_json_trigger logic in _llm_judge
- Remove unused imports (asyncio, time, awakening_state) in awakening_plugin
- Validate group IDs in BoredomEnabledGroups.load() via _safe_group_id()
- Remove duplicate record_message() call in check_awakening_triggers
- Add bot_messages.add() and stats_tracker.record_trigger() to boredom path
- Complete /awakening help text with all 6 rule names
- Respect group LLM state, rule switches, and rate-limit availability before running soft awakening checks

- Parse quick-judge JSON scores with threshold-aware cache keys

- Skip boredom wakeups when group LLM is disabled and cover rate-limit probes with tests
- Fall back to the system default SSL context when certifi is unavailable

- Preserve HTTPS context handling for OpenAI transcription requests
- Keep daily luck rolls as raw lognormal values without rounding away tiny multipliers

- Tighten regression coverage so generated luck stays strictly positive
@3aKHP 3aKHP merged commit 12d636f into main May 26, 2026
3 checks passed
@3aKHP 3aKHP deleted the feat/v1.7.0-awakening-module branch May 26, 2026 22:21
@3aKHP 3aKHP restored the feat/v1.7.0-awakening-module branch May 26, 2026 22:26
@3aKHP 3aKHP deleted the feat/v1.7.0-awakening-module branch May 26, 2026 22:30
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