[阶段4] StandardPipeline 重构 + P0 修复 (#180) - #187
Conversation
P0-1: 添加并发控制(Semaphore + per-session lock) - 新增 _handle_respond_message_v2() 包裹 pipeline.execute() - 添加 _init_pipeline() 初始化 StandardPipeline + 9 个 Stage - 与原实现保持一致的并发控制语义 P0-2: 初始化 ContextBuilder - 在 TaleCore.initialize() 中创建 ContextBuilder 实例 - 配置 MetadataBuilder/MediaRecognizer/HistoryProvider - 注入到 ContextBuildStage P0-3: 实现文件发送失败通知 - HistorySaveStage 添加 _notify_file_upload_failure() - 支持双模式:context_buffer(非持久化)/ SessionManager(持久化) - 新增 5 个测试用例,164/164 测试通过 审查发现:性能 2/10,安全 6/10,功能对等性 6/10 修复后预期:性能 8/10,安全 9/10,生产可部署
修复内容: 1. test_metadata_builder.py (3个失败) - 简化 basic_processed_message fixture,移除显式默认参数 - 在测试中显式设置 is_group_message=True 以触发群聊逻辑 - 修复群组检测:MetadataBuilder 依赖 is_group_message 标志 2. test_name_mapping.py (2个失败) - 更新断言:`****` -> `usr_` 以匹配 IDSanitizer 实际格式 - IDSanitizer 使用 usr_xxxx/grp_xxxx 格式,而非星号掩码 根本原因: - IDSanitizer 格式已恢复为旧格式 (usr_/grp_),但测试仍期望新格式 - ProcessedMessage.__post_init__ 仅在 is_group_message=None 时自动推断 - 测试 fixture 需要适配自动推断逻辑 测试结果: ✅ 27/27 测试通过 (metadata_builder + name_mapping) ✅ 164/164 Pipeline 测试全部通过
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughTaleCore now initializes and runs a configurable nine-stage ChangesStandard pipeline migration
File failure notifications
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant PlatformEvent
participant TaleCore
participant StandardPipeline
participant HistorySaveStage
participant SessionManager
PlatformEvent->>TaleCore: submit processed message
TaleCore->>StandardPipeline: execute PipelineContext
StandardPipeline->>HistorySaveStage: persist history and acknowledge inbox
HistorySaveStage->>SessionManager: persist failed-file notice when enabled
HistorySaveStage-->>StandardPipeline: complete processing
Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
core/main.py (1)
188-206: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider declaring
context_builderin__init__.
self.context_builderis only assigned insideinitialize(). Every other collaborator (self.chat,self.chat_agent,self.toolllm,self.bridge) is declared with a type hint in__init__, even though they are also populated later ininitialize(). Declareself.context_builder: Optional[Any] = Nonein__init__for consistency and to avoid anAttributeErrorif any code path reads this attribute beforeinitialize()completes.♻️ Proposed addition to `__init__`
self.session_manager: Optional[SessionManager] = None self.bridge: Optional[BridgeState] = None + self.context_builder: Optional[Any] = None self._id_sanitizer = IDSanitizer() # ID脱敏器🤖 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 `@core/main.py` around lines 188 - 206, Declare self.context_builder: Optional[Any] = None in __init__, alongside the other collaborators initialized there. Keep the existing ContextBuilder construction and assignment in initialize() unchanged so the attribute is always available before initialization completes.
🤖 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 `@core/main.py`:
- Around line 500-563: Update _on_config_reloaded() to call _init_pipeline()
after chat, chat_agent, and toolllm providers are updated and tool definitions
are rebuilt, before the reload completion log. Guard the call for an existing
pipeline as shown, preserving shared caches while reconstructing stages with the
current provider references.
- Around line 1270-1310: Extract the shared derivation of is_group, target_id,
sid, and platform_name into one reusable helper, such as
TaleCore._derive_session_key. Update _handle_respond_message_v2,
BuildUserInputStage.process, and SessionInitStage.process to use that helper or
preserve the precomputed context values without recomputing them, ensuring the
lock key and pipeline session key always match.
---
Nitpick comments:
In `@core/main.py`:
- Around line 188-206: Declare self.context_builder: Optional[Any] = None in
__init__, alongside the other collaborators initialized there. Keep the existing
ContextBuilder construction and assignment in initialize() unchanged so the
attribute is always available before initialization completes.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 108db404-a88d-4710-bc22-77009c653dd8
📒 Files selected for processing (5)
core/main.pycore/pipeline/stages/history_save.pytests/unit/context/test_metadata_builder.pytests/unit/pipeline/stages/test_history_save.pytests/unit/pipeline/stages/test_name_mapping.py
| async def _handle_respond_message_v2(self, processed, adapter_instance=None): | ||
| """处理需要响应的消息(Pipeline 版本,带并发控制) | ||
|
|
||
| 修复 P0-1: 添加 Semaphore + per-session lock 并发控制 | ||
| """ | ||
| from core.pipeline import PipelineContext | ||
|
|
||
| # 1. 构造 sid | ||
| is_group = processed.group_id is not None | ||
| target_id = processed.group_id if processed.group_id else processed.sender_id | ||
| stype = "gm" if is_group else "dm" | ||
| sid = f"{processed.platform.value}:{stype}:{target_id}" | ||
|
|
||
| # 2. 构造 PipelineContext | ||
| ctx = PipelineContext( | ||
| processed=processed, | ||
| adapter_instance=adapter_instance, | ||
| sid=sid, | ||
| is_group=is_group, | ||
| target_id=target_id, | ||
| platform_name=processed.platform.value if processed.platform else adapter_instance or "unknown" | ||
| ) | ||
|
|
||
| # 3. 添加并发控制(与原实现一致) | ||
| try: | ||
| async with self._session_semaphore: # 全局限流 | ||
| session_lock = await self._get_session_lock(sid) | ||
| async with session_lock: # per-session 锁 | ||
| await self.pipeline.execute(ctx) | ||
| except Exception as e: | ||
| logger.error("Pipeline 处理消息失败: %s", e, exc_info=True) | ||
| # 发送错误回显给用户 | ||
| error_msg = f"[系统] 处理消息时出了点状况:{e}" | ||
| await self._send_reply( | ||
| adapter_instance or processed.platform.value, | ||
| target_id, | ||
| error_msg, | ||
| reply_to=processed.message_id, | ||
| is_group=is_group | ||
| ) | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
sid/is_group/target_id/platform_name are computed twice with duplicated logic.
_handle_respond_message_v2 computes sid, is_group, target_id, and platform_name from processed and passes them into PipelineContext. BuildUserInputStage.process() (order=100) immediately recomputes is_group/target_id/platform_name from processed and overwrites the values. SessionInitStage.process() (order=300) immediately recomputes sid and overwrites ctx.sid.
Today both computations use the identical formula, so results match. If either formula changes independently in the future, the sid used here for _get_session_lock(sid) (per-session mutual exclusion) can diverge from the sid the pipeline actually uses for session state, tool loop continuity, and history persistence. Extract the sid/is_group/target_id derivation into one shared helper (for example a staticmethod on TaleCore or a small pure function) used by both _handle_respond_message_v2 and SessionInitStage/BuildUserInputStage, so the lock key and the pipeline's session key can never drift apart.
♻️ Proposed refactor sketch
+ `@staticmethod`
+ def _derive_session_key(processed, adapter_instance=None):
+ is_group = processed.group_id is not None
+ target_id = processed.group_id if is_group else processed.sender_id
+ stype = "gm" if is_group else "dm"
+ sid = f"{processed.platform.value}:{stype}:{target_id}"
+ platform_name = processed.platform.value if processed.platform else adapter_instance or "unknown"
+ return sid, is_group, target_id, platform_name
+
async def _handle_respond_message_v2(self, processed, adapter_instance=None):
- is_group = processed.group_id is not None
- target_id = processed.group_id if processed.group_id else processed.sender_id
- stype = "gm" if is_group else "dm"
- sid = f"{processed.platform.value}:{stype}:{target_id}"
+ sid, is_group, target_id, platform_name = self._derive_session_key(processed, adapter_instance)Then reuse TaleCore._derive_session_key inside SessionInitStage/BuildUserInputStage, or pass the pre-derived values through PipelineContext and skip recomputation in those stages.
🧰 Tools
🪛 Ruff (0.16.0)
[warning] 1271-1271: Docstring contains ambiguous ( (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?
(RUF002)
[warning] 1271-1271: Docstring contains ambiguous , (FULLWIDTH COMMA). Did you mean , (COMMA)?
(RUF002)
[warning] 1271-1271: Docstring contains ambiguous ) (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?
(RUF002)
[warning] 1293-1293: Comment contains ambiguous ( (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?
(RUF003)
[warning] 1293-1293: Comment contains ambiguous ) (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?
(RUF003)
[warning] 1302-1302: String contains ambiguous : (FULLWIDTH COLON). Did you mean : (COLON)?
(RUF001)
🤖 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 `@core/main.py` around lines 1270 - 1310, Extract the shared derivation of
is_group, target_id, sid, and platform_name into one reusable helper, such as
TaleCore._derive_session_key. Update _handle_respond_message_v2,
BuildUserInputStage.process, and SessionInitStage.process to use that helper or
preserve the precomputed context values without recomputing them, ensuring the
lock key and pipeline session key always match.
## 问题 1(Nitpick):context_builder 未在 __init__ 声明 - 在 TaleCore.__init__ 添加 `self.context_builder: Optional[Any] = None` - 保持与其他可选组件一致的初始化模式 ## 问题 2(Major):Pipeline stages 缓存旧 LLM 引用 - 在 _on_config_reloaded() 末尾调用 `self._init_pipeline()` - 确保配置热重载时 Stage 实例获取最新的 LLM 引用 - 避免 Stage 持有过期的 chat_llm/tool_llm 导致调用失败 ## 问题 3(Major):sid/is_group/target_id 计算逻辑重复 - 提取静态方法 `_compute_session_info(processed)` - 返回 (sid, is_group, target_id, platform_name) 元组 - 在 _handle_respond_message_v2 中使用,消除重复计算 ## 测试结果 - ✅ pytest tests/unit/pipeline/ -v: 164 passed in 48.55s - ✅ 所有 Pipeline 相关测试通过
Qixuan112
left a comment
There was a problem hiding this comment.
CodeRabbit 审查问题已全部修复 ✅
问题 1(Nitpick)- context_builder 声明
✅ 已修复:在 TaleCore.__init__ (line 156-157) 添加 self.context_builder: Optional[Any] = None
问题 2(Major)- Pipeline stages 缓存旧引用
✅ 已修复:在 _on_config_reloaded() 末尾 (line 427-428) 调用 self._init_pipeline(),确保 config reload 后 Stage 获取最新 LLM 引用
问题 3(Major)- sid 计算逻辑重复
✅ 已修复:提取静态方法 _compute_session_info(processed) (line 1276-1293),消除重复计算
测试结果:164/164 Pipeline 测试通过
Commit:dfd6079
🤖 Addressed by Claude Code
## 功能说明 - 在 `behavior.yaml` 添加 `use_pipeline` 开关(默认 false) - 事件分发器根据 Feature Flag 路由到 Legacy 或 Pipeline 路径 - 添加日志标识符:`[Legacy]` / `[Pipeline]` 和详细路径日志 - 创建灰度迁移文档 `PIPELINE_MIGRATION.md` - 添加集成测试验证两条路径 ## 技术细节 1. `BotBehaviorConfig` 新增 `use_pipeline: bool` 字段 2. `_process_message_event()` 根据配置选择处理函数: - `use_pipeline=false` → `_handle_respond_message()` (Legacy) - `use_pipeline=true` → `_handle_respond_message_v2()` (Pipeline) 3. 日志监控点: - 路由决策:`[Legacy] 使用旧版流程` / `[Pipeline] 使用 StandardPipeline` - 处理开始:`[Legacy Path] 处理消息` / `[Pipeline Path] 处理消息` ## 灰度策略 - Day 1-2: 本地测试(use_pipeline=true) - Day 3-4: 生产 10% 流量 - Day 5-6: 50% 流量 - Day 7+: 全量(稳定后删除旧代码) ## 回滚方案 修改 `behavior.yaml` 设置 `use_pipeline: false` 并重启 ## 测试 新增 `tests/integration/test_pipeline_migration.py`: - 验证 Feature Flag=false 时选择 Legacy 路径 - 验证 Feature Flag=true 时选择 Pipeline 路径 - 验证日志包含正确的路径标识符 相关 Issue: Stage 4 Pipeline 激活
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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 `@PIPELINE_MIGRATION.md`:
- Around line 26-34: Update the “新版流程(Pipeline Path)” diagram to document all
nine stages registered by _init_pipeline(), changing the count from eight to
nine and adding HistorySave after ReplyDeliver in the execution path.
- Around line 87-106: Clarify the restart policy for use_pipeline in the
rollback and known-limitations sections: verify whether ConfigLoader hot-reloads
this flag while the message dispatcher reads it per response, then consistently
state either that the next message uses the new value or that a Tale process
restart is required. Remove the contradictory wording and keep both sections
aligned.
In `@tests/integration/test_pipeline_migration.py`:
- Around line 35-47: Update the _create_mock_processed fixture to construct
ProcessedMessage using only its supported fields: replace user_input with text
and remove event and sid, while preserving the existing valid values needed by
the routing assertions.
- Around line 65-74: Update both mock handler pairs in
tests/integration/test_pipeline_migration.py at lines 65-74 and 106-115: rename
the keyword parameter from adapter to adapter_instance in mock_legacy and
mock_pipeline so they match the production calls from _process_message_event().
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 157a1980-18ae-49dc-9d6b-afd0fd4d3698
📒 Files selected for processing (4)
PIPELINE_MIGRATION.mdcore/config/model.pycore/main.pytests/integration/test_pipeline_migration.py
问题 1: 文档缺少第 9 个 Stage (HistorySaveStage) - 更新 PIPELINE_MIGRATION.md 流程图,补充 HistorySave Stage 问题 2: 重启策略说法矛盾 - 验证 use_pipeline 标志在运行时动态读取 - 统一文档说法:配置修改后下一条消息立即生效,无需重启 问题 3: 测试 fixture 字段错误 - 移除 ProcessedMessage 不支持的 event/sid 字段 - 修正 user_input → text 字段名 - 修正 timestamp 参数为 datetime 对象(非 float) - 移除 channel_id 参数(PlatformEvent 不支持) 问题 4: mock 函数参数名错误 - 将测试中的 adapter 参数统一改为 adapter_instance - 与生产代码 core/main.py 保持一致
Qixuan112
left a comment
There was a problem hiding this comment.
CodeRabbit 第二轮审查问题已全部修复 ✅
问题 1 - 文档缺少第 9 个 Stage
✅ 已修复:更新 PIPELINE_MIGRATION.md 流程图,添加 HistorySaveStage(order=900, always_run)
问题 2 - 重启策略说法矛盾
✅ 已修复:验证 use_pipeline 在运行时动态读取,统一文档说明为"下一条消息立即生效,无需重启"
问题 3 - 测试 fixture 字段错误
✅ 已修复:移除 ProcessedMessage 不支持的字段(event/user_input/sid),修正为 text/timestamp
问题 4 - mock 函数参数名错误
✅ 已修复:统一 mock 函数参数为 adapter_instance(4 处修改)
Commit:b2158a0
🤖 Addressed by Claude Code
问题 1:集成测试参数错误(3 个失败) - tests/integration/test_pipeline_migration.py 行 82, 123, 150 - 错误:_process_message_event(event, adapter_instance="qq") - 原因:该方法只接受 1 个参数 (event) - 修复:移除 adapter_instance 参数 问题 2:ProcessedMessage.sid 属性缺失(6 个失败) - core/main.py 行 978 - 错误:'ProcessedMessage' object has no attribute 'sid' - 原因:ProcessedMessage 没有 sid 字段,sid 在方法内部构造 - 修复:在日志记录前从 platform/group_id/sender_id 构造 sid 测试结果: - 集成测试:3/3 通过 - 并发测试:6/6 通过 - 总计:9/9 通过
概述
实现 Issue #180 的 StandardPipeline 重构 + Issue #183 的 P0 任务(激活 Pipeline)。
完成内容
✅ 阶段4:StandardPipeline 核心实现
✅ 阶段3集成 P0:激活 Pipeline(Feature Flag)
use_pipeline: false默认(灰度开关)_handle_respond_message) ⇄ Pipeline (_handle_respond_message_v2)[Legacy Path]/[Pipeline Path]标识PIPELINE_MIGRATION.md(4 阶段灰度计划)tests/integration/test_pipeline_migration.py(3 个测试用例)use_pipeline: false立即回退使用方式
激活 Pipeline(灰度测试)
或环境变量:
export TALE_USE_PIPELINE=true灰度计划
详见
PIPELINE_MIGRATION.md:统计数据
关联 Issue
默认行为:
use_pipeline: false,保持现有流程不变。生产部署:按灰度计划逐步启用 Pipeline。
Summary by CodeRabbit
New Features
Bug Fixes
Documentation