   150→        # === Second pass: de-AI review (first turn only) ===
   151→        if is_first_turn:
   152→            powell_revise_guide = self._read_file("POWELL_REVISE.md")
   153→            review_prompt = f"""You are a strict editor. A draft article has been written. Your ONLY job is to remove AI tone. Do NOT change facts or add new content.
   154→
   155→=== PRIORITY 1: VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
   156→夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆
   157→大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革
   158→鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机
   159→伪口语: 说白了、说实话、折腾这玩意儿
   160→
   161→=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
   162→1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y" → 直接说你的判断，删掉"很多人"铺垫
   163→2. 换皮夸张：把一个夸张词换成另一个夸张词不算修好。"灾难级"→"毁灭性"是无效修改。正确做法是换成具体后果描述
   164→3. 概念包装：给普通事物套新名词（"数字雇员"="自动化脚本"、"温池"="预备好的虚拟机"）→ 拆掉包装，用读者直接能懂的说法
   165→4. 比喻堆砌：全篇最多保留1个比喻，多余的全部删除换成直说
   166→5. 鸡汤/口号结尾：删除所有励志收尾，改为具体行动建议或直接停住
   167→6. 首尾呼应：禁止结尾重复开头观点
   168→7. 语气漂移：全文语气必须统一，开头口语就全程口语，不要中途切书面语或抒情体
   169→8. 过渡词堆积：删除"首先...其次...最后..."
   170→9. 排比对仗：打破字数相近的并列句
   171→
   172→=== PRIORITY 3: FULL REVISION GUIDE ===
   173→{powell_revise_guide}
   174→
   175→=== DRAFT TO REVIEW ===
   176→{draft}
   177→
   178→=== OUTPUT ===
   179→Return ONLY the corrected text. No explanation, no meta-commentary.
   180→"""
   181→            logger.info("Running second-pass de-AI review on writing draft...")
   182→            response, _ = self.llm.generate_response(review_prompt, [])
   183→        else:
   184→            response = draft
   185→
   186→        # Update History
   187→        new_history = history.copy()
   188→        new_history.append({"role": "user", "content": actual_user_message})
   189→        new_history.append({"role": "assistant", "content": response})
   190→
   191→        return response, new_history
   192→
   193→class ReplyGenerator(BaseGenerator):
   194→    def chat(self, user_input: str, history: List[Dict]) -> Tuple[str, List[Dict]]:
   195→
   196→        is_first_turn = (len(history) == 0)
   197→
   198→        if is_first_turn:
   199→             # === First Turn ===
   200→            static_samples = self._read_file("REPLY-SAMPLE.md")
   201→            dynamic_samples = self.indexer.get_reply_samples(count=3)
   202→            dynamic_samples_text = "\n\n".join(dynamic_samples)
   203→            methodology = self._read_file("methodology.md")
   204→

        # === Second pass: de-AI review (first turn only) ===
        if is_first_turn:
            powell_revise_guide = self._read_file("POWELL_REVISE.md")
            review_prompt = f"""You are a strict editor. A draft article has been written. Your ONLY job is to remove AI tone. Do NOT change facts or add new content.

=== PRIORITY 1: VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆
大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革
鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机
伪口语: 说白了、说实话、折腾这玩意儿

=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y" → 直接说你的判断，删掉"很多人"铺垫
2. 换皮夸张：把一个夸张词换成另一个夸张词不算修好。"灾难级"→"毁灭性"是无效修改。正确做法是换成具体后果描述
3. 概念包装：给普通事物套新名词（"数字雇员"="自动化脚本"、"温池"="预备好的虚拟机"）→ 拆掉包装，用读者直接能懂的说法
4. 比喻堆砌：全篇最多保留1个比喻，多余的全部删除换成直说
5. 鸡汤/口号结尾：删除所有励志收尾，改为具体行动建议或直接停住
6. 首尾呼应：禁止结尾重复开头观点
7. 语气漂移：全文语气必须统一，开头口语就全程口语，不要中途切书面语或抒情体
8. 过渡词堆积：删除"首先...其次...最后..."
9. 排比对仗：打破字数相近的并列句

=== PRIORITY 3: FULL REVISION GUIDE ===
{powell_revise_guide}

=== DRAFT TO REVIEW ===
{draft}

=== OUTPUT ===
Return ONLY the corrected text. No explanation, no meta-commentary.
"""
            logger.info("Running second-pass de-AI review on writing draft...")
            response, _ = self.llm.generate_response(review_prompt, [])
        else:
            response = draft

        # Update History
        new_history = history.copy()
        new_history.append({"role": "user", "content": actual_user_message})
        new_history.append({"role": "assistant", "content": response})

        return response, new_history

class ReplyGenerator(BaseGenerator):
    def chat(self, user_input: str, history: List[Dict]) -> Tuple[str, List[Dict]]:

        is_first_turn = (len(history) == 0)

        if is_first_turn:
             # === First Turn ===
            static_samples = self._read_file("REPLY-SAMPLE.md")
            dynamic_samples = self.indexer.get_reply_samples(count=3)
            dynamic_samples_text = "\n\n".join(dynamic_samples)
            methodology = self._read_file("methodology.md")


   614→        draft, _ = self.llm.generate_response(prompt, llm_history)
   615→
   616→        # === Second pass: rule enforcement ===
   617→        # Runs whenever a full draft was generated (built_rich_prompt=True),
   618→        # regardless of whether history was empty or not.
   619→        # Skipped on follow-up refinement turns (e.g. "make it shorter").
   620→        run_review = built_rich_prompt
   621→        if run_review:
   622→            review_prompt = f"""You are a strict editor. A draft Zhihu answer has been written. Your ONLY job is to remove AI tone and make it sound like a normal person typing on Zhihu. Do NOT change facts or add new content.
   623→
   624→=== PRIORITY 1: VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
   625→夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、本质上
   626→大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革、不可逆的浪潮、时代大背景
   627→鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、挑战与机遇并存
   628→伪口语: 说白了、说实话、折腾这玩意儿
   629→
   630→=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
   631→1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"不少人觉得X" → 直接说判断，删掉"很多人"铺垫
   632→2. 换皮夸张：用另一个夸张词替换夸张词不算修好。必须换成具体后果或具体场景
   633→3. 概念包装：给普通事物套新名词 → 拆掉包装用大白话
   634→4. 比喻堆砌：全篇最多1个比喻，多余全删换直说
   635→5. 鸡汤/口号结尾：删除励志收尾，给具体建议或直接停
   636→6. 首尾呼应：禁止结尾重复开头
   637→7. 对比式定义："不是X而是Y"、"那不叫X那叫Y" → 直接陈述
   638→8. 编号分段：删除"一、二、三、四"序号标题，改为自然段落
   639→9. 防御性引号：删除套在归纳性词汇上的引号
   640→10. 强行总结：删除"一句话建议"、"综上"式收尾
   641→11. 嵌套数字列表：删除正文中"1. 2. 3."子列表，改为连续段落
   642→
   643→=== PRIORITY 3: ZHIHU-SPECIFIC RULES ===
   644→{zhihu_bot_style}
   645→
   646→=== PRIORITY 4: FULL REVISION GUIDE ===
   647→{powell_revise_guide}
   648→
   649→=== TONE TARGET ===
   650→像一个有经验的人在知乎随手打字回答，不是在写公众号文章。平实、具体、不装。能用短句就不用长句，能删就删。
   651→
   652→=== DRAFT TO REVIEW ===
   653→{draft}
   654→
   655→=== OUTPUT ===
   656→Return ONLY the corrected Chinese text. No explanation, no meta-commentary, no English.
   657→"""
   658→            logger.info("Running second-pass rule enforcement on Zhihu draft...")
   659→            response, _ = self.llm.generate_response(review_prompt, [])
   660→        else:
   661→            response = draft
   662→
   663→        # Update History

        draft, _ = self.llm.generate_response(prompt, llm_history)

        # === Second pass: rule enforcement ===
        # Runs whenever a full draft was generated (built_rich_prompt=True),
        # regardless of whether history was empty or not.
        # Skipped on follow-up refinement turns (e.g. "make it shorter").
        run_review = built_rich_prompt
        if run_review:
            review_prompt = f"""You are a strict editor. A draft Zhihu answer has been written. Your ONLY job is to remove AI tone and make it sound like a normal person typing on Zhihu. Do NOT change facts or add new content.

=== PRIORITY 1: VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、本质上
大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革、不可逆的浪潮、时代大背景
鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、挑战与机遇并存
伪口语: 说白了、说实话、折腾这玩意儿

=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"不少人觉得X" → 直接说判断，删掉"很多人"铺垫
2. 换皮夸张：用另一个夸张词替换夸张词不算修好。必须换成具体后果或具体场景
3. 概念包装：给普通事物套新名词 → 拆掉包装用大白话
4. 比喻堆砌：全篇最多1个比喻，多余全删换直说
5. 鸡汤/口号结尾：删除励志收尾，给具体建议或直接停
6. 首尾呼应：禁止结尾重复开头
7. 对比式定义："不是X而是Y"、"那不叫X那叫Y" → 直接陈述
8. 编号分段：删除"一、二、三、四"序号标题，改为自然段落
9. 防御性引号：删除套在归纳性词汇上的引号
10. 强行总结：删除"一句话建议"、"综上"式收尾
11. 嵌套数字列表：删除正文中"1. 2. 3."子列表，改为连续段落

=== PRIORITY 3: ZHIHU-SPECIFIC RULES ===
{zhihu_bot_style}

=== PRIORITY 4: FULL REVISION GUIDE ===
{powell_revise_guide}

=== TONE TARGET ===
像一个有经验的人在知乎随手打字回答，不是在写公众号文章。平实、具体、不装。能用短句就不用长句，能删就删。

=== DRAFT TO REVIEW ===
{draft}

=== OUTPUT ===
Return ONLY the corrected Chinese text. No explanation, no meta-commentary, no English.
"""
            logger.info("Running second-pass rule enforcement on Zhihu draft...")
            response, _ = self.llm.generate_response(review_prompt, [])
        else:
            response = draft

        # Update History


现在同时更新两个 review prompt，加入反油腻规则，并把 TONE TARGET 提到更显眼的位置。


Edit

            review_prompt = f"""You are a strict editor. A draft article has been written. Your ONLY job is to remove AI tone. Do NOT change facts or add new content.

=== PRIORITY 1: VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆
大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革
鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机
伪口语: 说白了、说实话、折腾这玩意儿

=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y" → 直接说你的判断，删掉"很多人"铺垫
2. 换皮夸张：把一个夸张词换成另一个夸张词不算修好。"灾难级"→"毁灭性"是无效修改。正确做法是换成具体后果描述
3. 概念包装：给普通事物套新名词（"数字雇员"="自动化脚本"、"温池"="预备好的虚拟机"）→ 拆掉包装，用读者直接能懂的说法
4. 比喻堆砌：全篇最多保留1个比喻，多余的全部删除换成直说
5. 鸡汤/口号结尾：删除所有励志收尾，改为具体行动建议或直接停住
6. 首尾呼应：禁止结尾重复开头观点
7. 语气漂移：全文语气必须统一，开头口语就全程口语，不要中途切书面语或抒情体
8. 过渡词堆积：删除"首先...其次...最后..."
9. 排比对仗：打破字数相近的并列句

=== PRIORITY 3: FULL REVISION GUIDE ===
{powell_revise_guide}

=== DRAFT TO REVIEW ===
{draft}

=== OUTPUT ===
Return ONLY the corrected text. No explanation, no meta-commentary.
"""

            review_prompt = f"""You are a strict editor. A draft article has been written. Your ONLY job is to remove AI tone. Do NOT change facts or add new content.

=== TONE TARGET (最高优先级) ===
目标语感：像一个懂行的人在跟朋友聊天时随口说的。不端着，也不刻意耍帅。
两种AI味都要杀：
- 正式AI味：过渡词堆砌、排比对仗、空洞修饰、鸡汤结尾
- 油腻AI味：刻意装随意（"折腾去吧"）、强行造金句（"搞坏了不心疼，跑通了就是资产"）、给普通概念起酷名字（"隔离舱"、"暴力碰撞"、"最干净的契约"）、用一句俏皮话收尾装潇洒
正确的语感：平铺直叙，该说什么说什么，说完就停。不需要让读者觉得你很酷。

=== PRIORITY 1: VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高
大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力
鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、从玩具变成生产力、把环境变成了资产、安全底线
油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、任何试图用一句俏皮话总结全文的结尾
伪口语: 说白了、说实话、折腾这玩意儿

=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"很多人没意识到" → 直接说判断，删掉"很多人"铺垫
2. 换皮夸张：用另一个夸张词替换夸张词不算修好。"灾难级"→"毁灭性"→"风险极高"都是换皮。正确做法：换成具体后果（如"可能误删整个home目录"）
3. 概念包装：给普通事物起酷名字（"隔离舱"=容器、"暴力碰撞"=冲突、"数字雇员"=自动化脚本）→ 用最朴素的说法
4. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 要么删掉，要么拆成普通陈述句
5. 比喻堆砌：全篇最多保留1个比喻，多余的全部删除换成直说
6. 鸡汤/口号结尾：删除所有励志或耍帅收尾，改为具体行动建议或直接停住
7. 首尾呼应：禁止结尾重复开头观点
8. 语气漂移：全文语气必须统一，不要中途切换
9. 编号标题："一、二、三、四"序号标题 → 删除，改为自然段落过渡
10. 过渡词堆积：删除"首先...其次...最后..."
11. 排比对仗：打破字数相近的并列句

=== PRIORITY 3: FULL REVISION GUIDE ===
{powell_revise_guide}

=== DRAFT TO REVIEW ===
{draft}

=== OUTPUT ===
Return ONLY the corrected text. No explanation, no meta-commentary.
"""


The file /root/projects/butler/slack_bot/obsidian/generators.py has been updated successfully.

            review_prompt = f"""You are a strict editor. A draft article has been written. Your ONLY job is to remove AI tone. Do NOT change facts or add new content.

=== TONE TARGET (最高优先级) ===
目标语感：像一个懂行的人在跟朋友聊天时随口说的。不端着，也不刻意耍帅。
两种AI味都要杀：
- 正式AI味：过渡词堆砌、排比对仗、空洞修饰、鸡汤结尾
- 油腻AI味：刻意装随意（"折腾去吧"）、强行造金句（"搞坏了不心疼，跑通了就是资产"）、给普通概念起酷名字（"隔离舱"、"暴力碰撞"、"最干净的契约"）、用一句俏皮话收尾装潇洒
正确的语感：平铺直叙，该说什么说什么，说完就停。不需要让读者觉得你很酷。

=== PRIORITY 1: VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高
大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力
鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、从玩具变成生产力、把环境变成了资产、安全底线
油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、任何试图用一句俏皮话总结全文的结尾
伪口语: 说白了、说实话、折腾这玩意儿

=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"很多人没意识到" → 直接说判断，删掉"很多人"铺垫
2. 换皮夸张：用另一个夸张词替换夸张词不算修好。"灾难级"→"毁灭性"→"风险极高"都是换皮。正确做法：换成具体后果（如"可能误删整个home目录"）
3. 概念包装：给普通事物起酷名字（"隔离舱"=容器、"暴力碰撞"=冲突、"数字雇员"=自动化脚本）→ 用最朴素的说法
4. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 要么删掉，要么拆成普通陈述句
5. 比喻堆砌：全篇最多保留1个比喻，多余的全部删除换成直说
6. 鸡汤/口号结尾：删除所有励志或耍帅收尾，改为具体行动建议或直接停住
7. 首尾呼应：禁止结尾重复开头观点
8. 语气漂移：全文语气必须统一，不要中途切换
9. 编号标题："一、二、三、四"序号标题 → 删除，改为自然段落过渡
10. 过渡词堆积：删除"首先...其次...最后..."
11. 排比对仗：打破字数相近的并列句

=== PRIORITY 3: FULL REVISION GUIDE ===
{powell_revise_guide}

=== DRAFT TO REVIEW ===
{draft}

=== OUTPUT ===
Return ONLY the corrected text. No explanation, no meta-commentary.
"""

import os
from pathlib import Path
from typing import List, Dict, Tuple, Optional, TYPE_CHECKING
from slack_bot.llm.gemini import GeminiLLM
from slack_bot.obsidian.indexer import ObsidianIndexer
from slack_bot.tools.web import WebSearchTool
from health.utils.logging_config import setup_logger
from health.utils.time_utils import get_current_time_str

if TYPE_CHECKING:
    from slack_bot.obsidian.vector_store import ChromaVectorStore

logger = setup_logger(__name__)

OBSIDIAN_SYSTEM_PROMPT = """You are Butler (Obsidian Edition), an intelligent knowledge assistant connected to the user's second brain.
Current Time: {current_time}

Your Role:
You are NOT a health assistant. You are a Knowledge Partner designed to help the user think, write, and communicate. You draw directly from the user's local Obsidian notes, values, and methodology.

Core Capabilities:
1. Writing: You mimic the user's unique writing style (as defined in `writing_style.md`).
2. Communication: You draft high-EQ, logically rigorous replies for professional contexts (as defined in `REPLY-SAMPLE.md`).
3. Decision: You act as a "Devil's Advocate" and strategic advisor using the user's decision frameworks (GPA, IPO).

Guidelines:
- **Style Alignment**: Strictly adhere to the tone and sentence structures found in the provided samples.
- **Data Source**: Rely primarily on the provided context (RAG) and loaded markdown files.
- **Identity**: You are pragmatic, rational, and value "technological optimism" and "intellectual honesty".
"""

class BaseGenerator:
    def __init__(
        self,
        indexer: ObsidianIndexer,
        vector_store: Optional["ChromaVectorStore"] = None,
    ):
        formatted_prompt = OBSIDIAN_SYSTEM_PROMPT.format(current_time=get_current_time_str())
        self.llm = GeminiLLM(system_instruction=formatted_prompt)
        self.indexer = indexer
        self.vector_store = vector_store
        self.workspace_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))

    def _read_file(self, filename: str) -> str:
        """Reads a file from the workspace root."""
        path = os.path.join(self.workspace_root, filename)
        try:
            with open(path, "r", encoding="utf-8") as f:
                return f.read()
        except Exception as e:
            logger.error(f"Failed to read {filename}: {e}")
            return ""

    def _semantic_search(self, query: str, top_k: int = 3) -> str:
        """Run semantic search against the vector store and format results.

        Args:
            query: Search query text.
            top_k: Maximum number of chunks to retrieve.

        Returns:
            Formatted string block, or empty string if no results.
        """
        if self.vector_store is None:
            return ""
        try:
            results = self.vector_store.search_knowledge(query, top_k=top_k)
        except Exception as e:
            logger.warning(f"_semantic_search failed: {e}")
            return ""
        if not results:
            return ""
        lines = ["=== SEMANTIC KNOWLEDGE FROM VAULT ==="]
        for r in results:
            lines.append(f"[{Path(r.source_path).name} / {r.header_hierarchy}]")
            # Truncate individual chunks to avoid prompt bloat (~500 tokens ≈ 2000 chars)
            chunk_text = r.text[:2000]
            lines.append(chunk_text)
            lines.append("")
        return "\n".join(lines)

class WritingAssistant(BaseGenerator):
    def chat(self, user_input: str, history: List[Dict]) -> Tuple[str, List[Dict]]:

        is_first_turn = (len(history) == 0)

        if is_first_turn:
            # === First Turn: Build Rich Prompt ===
            # user_input is treated as "Topic"

            # Naive splitting for context if formatted as "Topic | Context"
            parts = user_input.split("|", 1)
            topic = parts[0].strip()
            extra_context = parts[1].strip() if len(parts) > 1 else ""

            # 1. RAG & Samples
            style_samples = self.indexer.get_writing_samples(count=3)
            style_text = "\n\n".join(style_samples)

            rag_notes = self.indexer.search(topic, limit=5)
            rag_text = "\n\n".join(rag_notes)

            writing_style_guide = self._read_file("writing_style.md")
            methodology = self._read_file("methodology.md")
            powell_revise_guide = self._read_file("POWELL_REVISE.md")

            semantic_context = self._semantic_search(topic, top_k=3)

            prompt = f"""You are a ghostwriter for the user. Your goal is to write an article on the TOPIC provided.

            === YOUR IDENTITY & METHODOLOGY ===
            {methodology}

            === YOUR WRITING STYLE GUIDE ===
            {writing_style_guide}

            === ANTI-AI-TONE GUIDE (APPLY WHILE WRITING, NOT JUST AT THE END) ===
            {powell_revise_guide}

            === YOUR WRITING SAMPLES (MIMIC THIS TONE) ===
            {style_text}

            === RELEVANT NOTES FROM OBSIDIAN VAULT ===
            {rag_text}

            {semantic_context}

            === TASK ===
            Topic: {topic}
            Extra Context: {extra_context}

            Write the article in Chinese (unless the topic implies English).
            Adhere strictly to the "Identity" and "Style Guide".
            """

            logger.info(f"Generating article for topic: {topic}")
            actual_user_message = prompt # We inject the big prompt

        else:
            # === Follow-up Turn ===
            # user_input is just instructions like "Make it shorter"
            logger.info(f"Follow-up instruction: {user_input}")
            actual_user_message = user_input
            prompt = user_input

        # Call LLM — first pass: content generation
        draft, _ = self.llm.generate_response(prompt, history)

        # === Second pass: de-AI review (first turn only) ===
        if is_first_turn:
            powell_revise_guide = self._read_file("POWELL_REVISE.md")
            review_prompt = f"""You are a strict editor. A draft article has been written. Your ONLY job is to remove AI tone. Do NOT change facts or add new content.

=== PRIORITY 1: VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆
大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革
鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机
伪口语: 说白了、说实话、折腾这玩意儿

=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y" → 直接说你的判断，删掉"很多人"铺垫
2. 换皮夸张：把一个夸张词换成另一个夸张词不算修好。"灾难级"→"毁灭性"是无效修改。正确做法是换成具体后果描述
3. 概念包装：给普通事物套新名词（"数字雇员"="自动化脚本"、"温池"="预备好的虚拟机"）→ 拆掉包装，用读者直接能懂的说法
4. 比喻堆砌：全篇最多保留1个比喻，多余的全部删除换成直说
5. 鸡汤/口号结尾：删除所有励志收尾，改为具体行动建议或直接停住
6. 首尾呼应：禁止结尾重复开头观点
7. 语气漂移：全文语气必须统一，开头口语就全程口语，不要中途切书面语或抒情体
8. 过渡词堆积：删除"首先...其次...最后..."
9. 排比对仗：打破字数相近的并列句

=== PRIORITY 3: FULL REVISION GUIDE ===
{powell_revise_guide}

=== DRAFT TO REVIEW ===
{draft}

=== OUTPUT ===
Return ONLY the corrected text. No explanation, no meta-commentary.
"""
            logger.info("Running second-pass de-AI review on writing draft...")
            response, _ = self.llm.generate_response(review_prompt, [])
        else:
            response = draft

        # Update History
        new_history = history.copy()
        new_history.append({"role": "user", "content": actual_user_message})
        new_history.append({"role": "assistant", "content": response})

        return response, new_history

class ReplyGenerator(BaseGenerator):
    def chat(self, user_input: str, history: List[Dict]) -> Tuple[str, List[Dict]]:

        is_first_turn = (len(history) == 0)

        if is_first_turn:
             # === First Turn ===
            static_samples = self._read_file("REPLY-SAMPLE.md")
            dynamic_samples = self.indexer.get_reply_samples(count=3)
            dynamic_samples_text = "\n\n".join(dynamic_samples)
            methodology = self._read_file("methodology.md")

            semantic_context = self._semantic_search(user_input, top_k=3)

            prompt = f"""You are an experienced Product/R&D Manager.
            Your task is to draft a reply to a challenging situation.

            === CORE PHILOSOPHY ===
            {methodology}

            === REPLY GUIDELINES ===
            1. Logic: Be clear and structured (e.g., numbered lists).
            2. Responsibility: Don't take unnecessary blame, but don't shirk core duties. Explain objective causes.
            3. Emotion: Be empathetic but professional.
            4. Conciseness: No fluff.

            === REFERENCE SAMPLES (FEW-SHOT) ===
            {static_samples}

            {dynamic_samples_text}

            {semantic_context}

            === THE SITUATION ===
            {user_input}

            Draft a reply. If the context implies a specific role (PM, Dev Lead), assume that role.
            """
            logger.info(f"Generating reply for query: {user_input}")
            actual_user_message = prompt

        else:
             # === Follow-up Turn ===
            logger.info(f"Follow-up reply instruction: {user_input}")
            actual_user_message = user_input
            prompt = user_input

        response, _ = self.llm.generate_response(prompt, history)

        new_history = history.copy()
        new_history.append({"role": "user", "content": actual_user_message})
        new_history.append({"role": "assistant", "content": response})

        return response, new_history

class DecisionSupport(BaseGenerator):
    def chat(self, user_input: str, history: List[Dict]) -> Tuple[str, List[Dict]]:

        is_first_turn = (len(history) == 0)

        if is_first_turn:
            decision_guide = self._read_file("decision.md")
            methodology = self._read_file("methodology.md")

            rag_notes = self.indexer.search(user_input, limit=3)
            rag_text = "\n\n".join(rag_notes)

            prompt = f"""You are a Decision Support Assistant.
            Your goal is to help the user think through a complex decision.

            === DECISION FRAMEWORK ===
            {decision_guide}

            === CORE PHILOSOPHY ===
            {methodology}

            === RELEVANT CONTEXT FROM NOTES ===
            {rag_text}

            === THE DECISION / ISSUE ===
            {user_input}

            === TASK ===
            1. Challenge the premise: Is this the right problem to solve?
            2. Apply the Framework: Use GPA (Goal, Priority, Alternatives) and IPO (Information, People, Objective reasoning) models.
            3. Pre-mortem: What is the worst that could happen?
            4. Provide a recommendation or a set of questions to clarify.
            """

            logger.info(f"Analyzing decision: {user_input}")
            actual_user_message = prompt
        else:
            logger.info(f"Follow-up decision instruction: {user_input}")
            actual_user_message = user_input
            prompt = user_input

        response, _ = self.llm.generate_response(prompt, history)

        new_history = history.copy()
        new_history.append({"role": "user", "content": actual_user_message})
        new_history.append({"role": "assistant", "content": response})

        return response, new_history

class SearchAnalyzer(BaseGenerator):
    def chat(self, user_input: str, history: List[Dict]) -> Tuple[str, List[Dict]]:

        # Tools definition for the LLM
        tools = [
            {
                "type": "function",
                "function": {
                    "name": "search_web",
                    "description": "Search the public web using DuckDuckGo. Use this when the internal notes are insufficient or when you need up-to-date external information.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "query": {
                                "type": "string",
                                "description": "The search query"
                            }
                        },
                        "required": ["query"]
                    }
                }
            },
            {
                "type": "function",
                "function": {
                    "name": "list_recent_files",
                    "description": "List files in the Obsidian vault that have been modified recently. Use this when the user asks for 'recent updates', 'what's new', or changes in the last X days.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "days": {
                                "type": "integer",
                                "description": "Number of days to check (default 5)",
                                "default": 5
                            }
                        },
                        "required": []
                    }
                }
            }
        ]

        is_first_turn = (len(history) == 0)

        # Current Turn Message
        if is_first_turn:
            rag_notes = self.indexer.search(user_input, limit=5)
            rag_text = "\n\n".join(rag_notes)

            logger.info(f"SearchAnalyzer retrieved {len(rag_notes)} local notes.")
            for note in rag_notes:
                # Extract source line for logging
                first_line = note.split('\n')[0]
                logger.debug(f" - Retrieved: {first_line}")

            prompt = f"""You are an Analyst using Butler (Obsidian Edition).

            === TASK ===
            Answer the user's question.
            CRITICAL: You MUST explicitly cite your sources in the output.

            === STRATEGY ===
            1. first: Check "RELEVANT LOCAL NOTES" provided below.
            2. second: If the notes answer the question, answer directly.
            3. third: If the notes are missing info, ambiguos, or outdated, USE THE `search_web` TOOL to find external info.

            === OUTPUT FORMAT ===
            - Begin with a summary.
            - When stating facts, append the source in brackets, e.g., "The server failed [Source: XLSmart 2025-12-25.md]" or "Market share is 20% [Source: Web Search]".
            - If data comes from BOTH, mention both.

            === RELEVANT LOCAL NOTES ===
            {rag_text}

            === USER QUESTION ===
            {user_input}
            """
            actual_user_message = prompt
        else:
            actual_user_message = user_input

        # Prepare execution context
        # We don't want to mutate the input history directly until the end
        exec_history = history.copy()
        exec_history.append({"role": "user", "content": actual_user_message})

        logger.info(f"Analyzer thinking on: {user_input}")

        # === ReAct Loop (Max 2 turns to prevent infinite loops) ===
        for i in range(2):
            response_text, tool_calls = self.llm.generate_response(
                message="", # Message is already in history
                context=exec_history,
                tools=tools
            )

            if tool_calls:
                # LLM wants to use tools
                logger.info(f"LLM requested tool calls: {tool_calls}")

                # Append the assistant's request (with tool calls) to history
                exec_history.append({
                    "role": "assistant",
                    "content": f"[Tool Call Request: {tool_calls}]"
                })

                for tc in tool_calls:
                    if tc["name"] == "search_web":
                        query = tc["args"].get("query")
                        search_result = WebSearchTool.search_web(query)

                        # Feed result back
                        exec_history.append({
                            "role": "user", # Using 'function' role would be better if our LLM class supports it fully, but 'user' works as system injection
                            "content": f"--- TOOL OUTPUT (search_web) ---\n{search_result}"
                        })

                    elif tc["name"] == "list_recent_files":
                        days = tc["args"].get("days", 5)
                        # Call indexer method
                        recent_list = self.indexer.get_recent_files(days=days)

                        exec_history.append({
                            "role": "user",
                            "content": f"--- TOOL OUTPUT (list_recent_files) ---\n{recent_list}"
                        })

                # Loop again to get the final answer using tool outputs
                try:
                     # Second pass with tool outputs
                    response_text, _ = self.llm.generate_response(
                        message="",
                        context=exec_history,
                        tools=tools
                    )
                    return response_text, exec_history
                except Exception as e:
                    logger.error(f"Error in second ReAct loop: {e}")
                    return response_text, exec_history

            else:
                # Final answer (no tools)
                return response_text, exec_history

        # Fallback if loop exhausted
        return response_text, exec_history

class DeAIReviser(BaseGenerator):
    """Remove AI tone from articles - text-in, text-out mode."""

    def chat(self, user_input: str, history: List[Dict]) -> Tuple[str, List[Dict]]:

        is_first_turn = (len(history) == 0)

        if is_first_turn:
            # === First Turn: User pastes the AI-generated article ===
            # Load the POWELL_REVISE.md guide
            powell_revise_guide = self._read_file("POWELL_REVISE.md")

            # Load user's writing samples for style reference
            style_samples = self.indexer.get_writing_samples(count=3)
            style_text = "\n\n".join(style_samples)

            prompt = f"""You are an expert editor specializing in removing AI-generated tone from articles.

Your task is to revise the article provided by the user to make it sound more human and authentic.

=== REVISION GUIDE ===
{powell_revise_guide}

=== USER'S AUTHENTIC WRITING SAMPLES (for style reference) ===
{style_text}

=== ARTICLE TO REVISE ===
{user_input}

=== TASK ===
1. Analyze the article and identify AI characteristics (过渡词堆积, 排比对仗, 空洞修饰词等)
2. Rewrite the article following the POWELL_REVISE.md principles
3. Preserve:
   - Original meaning and key information
   - Technical terms and proper nouns
   - Code blocks, lists, and formatting
   - Original language (Chinese/English)
4. Output the revised article directly (no meta-commentary like "Here's the revised version...")

CRITICAL: Output ONLY the revised article text. Do NOT add explanations before or after.
"""

            logger.info(f"DeAI Reviser: Processing article ({len(user_input)} chars)")
            actual_user_message = prompt

        else:
            # === Follow-up Turn: User gives refinement instructions ===
            logger.info(f"DeAI Reviser: Follow-up instruction - {user_input}")
            actual_user_message = user_input
            prompt = user_input

        # Call LLM
        response, _ = self.llm.generate_response(prompt, history)

        # Update History
        new_history = history.copy()
        new_history.append({"role": "user", "content": actual_user_message})
        new_history.append({"role": "assistant", "content": response})

        return response, new_history

class ZhihuGenerator(BaseGenerator):
    """Generate Zhihu-style answers based on style guide and user's knowledge base."""

    def chat(self, user_input: str, history: List[Dict]) -> Tuple[str, List[Dict]]:

        is_first_turn = (len(history) == 0)
        built_rich_prompt = False  # tracks whether we ran the full question->draft pipeline

        # Pre-load rule files (needed for both generation and second-pass review)
        zhihu_bot_style = self._read_file("zhihu-bot-style.md")
        powell_revise_guide = self._read_file("POWELL_REVISE.md")

        # Treat as a new question if:
        # - history is empty (genuine first turn), OR
        # - user input contains "|" (explicit "Question | Core Ideas" format), OR
        # - user input starts with known question triggers like "回答这个问题"
        is_new_question = (
            is_first_turn
            or "|" in user_input
            or user_input.strip().startswith("回答这个问题")
        )

        if is_new_question:
            # === First Turn: Build Rich Prompt ===
            # Expected input format: "Question | Core Ideas"
            parts = user_input.split("|", 1)
            question = parts[0].strip()
            core_ideas = parts[1].strip() if len(parts) > 1 else ""

            # 1. Load Style Guide (zhihu-style.md serves as writing standard, NOT as material)
            zhihu_style_guide = self._read_file("zhihu-style.md")
            # Bot-specific overrides (plain-text output, no bracket explanations, etc.)
            if zhihu_bot_style:
                zhihu_style_guide = zhihu_style_guide + "\n\n" + zhihu_bot_style

            # 2. RAG from Obsidian vault for actual content material
            rag_notes = self.indexer.search(question, limit=5)
            rag_text = "\n\n".join(rag_notes)

            # 3. Load methodology
            methodology = self._read_file("methodology.md")

            semantic_context = self._semantic_search(question, top_k=3)

            prompt = f"""You are answering a Zhihu question. Your goal is to write an authentic, experience-driven answer that sounds like a real human, not AI-generated.

=== YOUR IDENTITY & METHODOLOGY ===
{methodology}

=== ANTI-AI-TONE GUIDE (APPLY WHILE WRITING, NOT JUST AT THE END) ===
{powell_revise_guide}

=== ZHIHU STYLE GUIDE (FOLLOW THESE WRITING PRINCIPLES) ===
{zhihu_style_guide}

CRITICAL: The style guide above defines HOW to write (tone, structure, language patterns).
Do NOT treat zhihu-sample content as factual material. It's ONLY for style reference.

=== RELEVANT KNOWLEDGE FROM YOUR OBSIDIAN VAULT (USE AS ACTUAL MATERIAL) ===
{rag_text}

{semantic_context}

=== THE QUESTION ===
{question}

=== YOUR CORE IDEAS ===
{core_ideas}

=== TASK ===
1. Answer the question based on YOUR knowledge (from Obsidian vault) and core ideas
2. FOLLOW THE ANTI-AI-TONE GUIDE FIRST, then the Zhihu style guide
3. Share real experiences, examples, and insights from the vault material
4. Write in Chinese (unless question implies English)
5. Minimize first person "我": use it sparingly, not as filler. Observations and judgments can stand without "我认为" or "我觉得" as prefix.
6. Be direct and opinionated. Attach a concrete evaluation to every fact or feature you mention (e.g. "暴力但有效", "极度脆弱"). Pure feature listing is forbidden.
7. AVOID AT ALL COSTS: AI tone, empty buzzwords, hollow phrases, and 宏大叙事 (不可逆的浪潮, 范式转移, 时代大背景).
8. If recent web context is provided, weave it in naturally — like someone who reads a lot online
9. STRICT: Never add bracket explanations for any term. Write "大语言模型" not "大语言模型（LLM）". If a term needs explaining, use a sentence, not parentheses.
10. No public account names, no self-promotion, no引流 of any kind — this is the highest priority rule.
11. STRICT: Never reveal personal identity or professional background. Do not mention employer, team, job title, product name, or any detail that could identify who you are.
12. STRICT: Never reference past projects, articles, or talks by name.
13. No poetry, quotes, or "升华" endings. No首尾呼应 (repeating the opening point at the end). Stop when the point is made.
14. LENGTH: 500–1000 Chinese characters. Cut a point, not quality.

=== BEFORE SUBMITTING, CHECK THESE ===
- Is there any AI-generated tone or phrasing?
- Are there any buzzwords or jargon that need to be replaced with plain language?
- Is the answer direct and opinionated, not neutral or "balanced"?
- Does it sound like a real human wrote it, not a machine?

Begin writing the answer:
"""

            logger.info(f"Generating Zhihu answer for: {question[:50]}...")
            actual_user_message = prompt
            built_rich_prompt = True

        else:
            # === Follow-up Turn: User gives refinement instructions (e.g. "make it shorter") ===
            logger.info(f"Follow-up Zhihu instruction: {user_input}")
            actual_user_message = user_input
            prompt = user_input

        # Call LLM — first pass: content generation
        # For new questions, use empty history to avoid contamination from previous conversations
        llm_history = [] if is_new_question else history
        draft, _ = self.llm.generate_response(prompt, llm_history)

        # === Second pass: rule enforcement ===
        # Runs whenever a full draft was generated (built_rich_prompt=True),
        # regardless of whether history was empty or not.
        # Skipped on follow-up refinement turns (e.g. "make it shorter").
        run_review = built_rich_prompt
        if run_review:
            review_prompt = f"""You are a strict editor. A draft Zhihu answer has been written. Your ONLY job is to remove AI tone and make it sound like a normal person typing on Zhihu. Do NOT change facts or add new content.

=== PRIORITY 1: VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、本质上
大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革、不可逆的浪潮、时代大背景
鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、挑战与机遇并存
伪口语: 说白了、说实话、折腾这玩意儿

=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"不少人觉得X" → 直接说判断，删掉"很多人"铺垫
2. 换皮夸张：用另一个夸张词替换夸张词不算修好。必须换成具体后果或具体场景
3. 概念包装：给普通事物套新名词 → 拆掉包装用大白话
4. 比喻堆砌：全篇最多1个比喻，多余全删换直说
5. 鸡汤/口号结尾：删除励志收尾，给具体建议或直接停
6. 首尾呼应：禁止结尾重复开头
7. 对比式定义："不是X而是Y"、"那不叫X那叫Y" → 直接陈述
8. 编号分段：删除"一、二、三、四"序号标题，改为自然段落
9. 防御性引号：删除套在归纳性词汇上的引号
10. 强行总结：删除"一句话建议"、"综上"式收尾
11. 嵌套数字列表：删除正文中"1. 2. 3."子列表，改为连续段落

=== PRIORITY 3: ZHIHU-SPECIFIC RULES ===
{zhihu_bot_style}

=== PRIORITY 4: FULL REVISION GUIDE ===
{powell_revise_guide}

=== TONE TARGET ===
像一个有经验的人在知乎随手打字回答，不是在写公众号文章。平实、具体、不装。能用短句就不用长句，能删就删。

=== DRAFT TO REVIEW ===
{draft}

=== OUTPUT ===
Return ONLY the corrected Chinese text. No explanation, no meta-commentary, no English.
"""
            logger.info("Running second-pass rule enforcement on Zhihu draft...")
            response, _ = self.llm.generate_response(review_prompt, [])
        else:
            response = draft

        # Update History
        new_history = history.copy()
        new_history.append({"role": "user", "content": actual_user_message})
        new_history.append({"role": "assistant", "content": response})

        return response, new_history

+目标语感：像一个懂行的人在跟朋友聊天时随口说的。不端着，也不刻意耍帅。

+- 正式AI味：过渡词堆砌、排比对仗、空洞修饰、鸡汤结尾

+- 油腻AI味：刻意装随意（"折腾去吧"）、强行造金句（"搞坏了不心疼，跑通了就是资产"）、给普通概念起酷名字（"隔离舱"、"暴力碰撞"、"最干净的契约"）、用一句俏皮话收尾装潇洒

+正确的语感：平铺直叙，该说什么说什么，说完就停。不需要让读者觉得你很酷。

-夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆

-大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革

-鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机

+夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高

+大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力

+鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、从玩具变成生产力、把环境变成了资产、安全底线

+油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、任何试图用一句俏皮话总结全文的结尾

伪口语: 说白了、说实话、折腾这玩意儿

-1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y" → 直接说你的判断，删掉"很多人"铺垫

-2. 换皮夸张：把一个夸张词换成另一个夸张词不算修好。"灾难级"→"毁灭性"是无效修改。正确做法是换成具体后果描述

-3. 概念包装：给普通事物套新名词（"数字雇员"="自动化脚本"、"温池"="预备好的虚拟机"）→ 拆掉包装，用读者直接能懂的说法

-4. 比喻堆砌：全篇最多保留1个比喻，多余的全部删除换成直说

-5. 鸡汤/口号结尾：删除所有励志收尾，改为具体行动建议或直接停住

-6. 首尾呼应：禁止结尾重复开头观点

-7. 语气漂移：全文语气必须统一，开头口语就全程口语，不要中途切书面语或抒情体

-8. 过渡词堆积：删除"首先...其次...最后..."

-9. 排比对仗：打破字数相近的并列句

+1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"很多人没意识到" → 直接说判断，删掉"很多人"铺垫

+2. 换皮夸张：用另一个夸张词替换夸张词不算修好。"灾难级"→"毁灭性"→"风险极高"都是换皮。正确做法：换成具体后果（如"可能误删整个home目录"）

+3. 概念包装：给普通事物起酷名字（"隔离舱"=容器、"暴力碰撞"=冲突、"数字雇员"=自动化脚本）→ 用最朴素的说法

+4. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 要么删掉，要么拆成普通陈述句

+5. 比喻堆砌：全篇最多保留1个比喻，多余的全部删除换成直说

+6. 鸡汤/口号结尾：删除所有励志或耍帅收尾，改为具体行动建议或直接停住

+7. 首尾呼应：禁止结尾重复开头观点

+8. 语气漂移：全文语气必须统一，不要中途切换

+9. 编号标题："一、二、三、四"序号标题 → 删除，改为自然段落过渡

+11. 排比对仗：打破字数相近的并列句


            review_prompt = f"""You are a strict editor. A draft Zhihu answer has been written. Your ONLY job is to remove AI tone and make it sound like a normal person typing on Zhihu. Do NOT change facts or add new content.

=== PRIORITY 1: VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、本质上
大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革、不可逆的浪潮、时代大背景
鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、挑战与机遇并存
伪口语: 说白了、说实话、折腾这玩意儿

=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"不少人觉得X" → 直接说判断，删掉"很多人"铺垫
2. 换皮夸张：用另一个夸张词替换夸张词不算修好。必须换成具体后果或具体场景
3. 概念包装：给普通事物套新名词 → 拆掉包装用大白话
4. 比喻堆砌：全篇最多1个比喻，多余全删换直说
5. 鸡汤/口号结尾：删除励志收尾，给具体建议或直接停
6. 首尾呼应：禁止结尾重复开头
7. 对比式定义："不是X而是Y"、"那不叫X那叫Y" → 直接陈述
8. 编号分段：删除"一、二、三、四"序号标题，改为自然段落
9. 防御性引号：删除套在归纳性词汇上的引号
10. 强行总结：删除"一句话建议"、"综上"式收尾
11. 嵌套数字列表：删除正文中"1. 2. 3."子列表，改为连续段落

=== PRIORITY 3: ZHIHU-SPECIFIC RULES ===
{zhihu_bot_style}

=== PRIORITY 4: FULL REVISION GUIDE ===
{powell_revise_guide}

=== TONE TARGET ===
像一个有经验的人在知乎随手打字回答，不是在写公众号文章。平实、具体、不装。能用短句就不用长句，能删就删。

=== DRAFT TO REVIEW ===
{draft}

=== OUTPUT ===
Return ONLY the corrected Chinese text. No explanation, no meta-commentary, no English.
"""

            review_prompt = f"""You are a strict editor. A draft Zhihu answer has been written. Your ONLY job is to remove AI tone. Do NOT change facts or add new content.

=== TONE TARGET (最高优先级) ===
目标语感：像一个懂行的人在知乎随手打字回答。不端着，也不刻意耍帅。
两种AI味都要杀：
- 正式AI味：过渡词堆砌、排比对仗、空洞修饰、鸡汤结尾
- 油腻AI味：刻意装随意（"折腾去吧"）、强行造金句（"搞坏了不心疼，跑通了就是资产"）、给普通概念起酷名字（"隔离舱"、"暴力碰撞"、"最干净的契约"）、用一句俏皮话收尾装潇洒
正确的语感：平铺直叙，该说什么说什么，说完就停。不需要让读者觉得你很酷。

=== PRIORITY 1: VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高、本质上
大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力、不可逆的浪潮、时代大背景
鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、挑战与机遇并存、从玩具变成生产力、把环境变成了资产、安全底线
油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、任何试图用一句俏皮话总结全文的结尾
伪口语: 说白了、说实话、折腾这玩意儿

=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"很多人没意识到"、"不少人觉得X" → 直接说判断，删掉"很多人"铺垫
2. 换皮夸张：用另一个夸张词替换夸张词不算修好。"灾难级"→"毁灭性"→"风险极高"都是换皮。正确做法：换成具体后果（如"可能误删整个home目录"）
3. 概念包装：给普通事物起酷名字（"隔离舱"=容器、"暴力碰撞"=冲突、"数字雇员"=自动化脚本）→ 用最朴素的说法
4. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 要么删掉，要么拆成普通陈述句
5. 比喻堆砌：全篇最多1个比喻，多余全删换直说
6. 鸡汤/口号结尾：删除励志或耍帅收尾，给具体建议或直接停
7. 首尾呼应：禁止结尾重复开头
8. 对比式定义："不是X而是Y"、"那不叫X那叫Y" → 直接陈述
9. 编号分段：删除"一、二、三、四"序号标题，改为自然段落
10. 防御性引号：删除套在归纳性词汇上的引号
11. 强行总结：删除"一句话建议"、"综上"式收尾
12. 嵌套数字列表：删除正文中"1. 2. 3."子列表，改为连续段落

=== PRIORITY 3: ZHIHU-SPECIFIC RULES ===
{zhihu_bot_style}

=== PRIORITY 4: FULL REVISION GUIDE ===
{powell_revise_guide}

=== DRAFT TO REVIEW ===
{draft}

=== OUTPUT ===
Return ONLY the corrected Chinese text. No explanation, no meta-commentary, no English.
"""


            review_prompt = f"""You are a strict editor. A draft Zhihu answer has been written. Your ONLY job is to remove AI tone. Do NOT change facts or add new content.

=== TONE TARGET (最高优先级) ===
目标语感：像一个懂行的人在知乎随手打字回答。不端着，也不刻意耍帅。
两种AI味都要杀：
- 正式AI味：过渡词堆砌、排比对仗、空洞修饰、鸡汤结尾
- 油腻AI味：刻意装随意（"折腾去吧"）、强行造金句（"搞坏了不心疼，跑通了就是资产"）、给普通概念起酷名字（"隔离舱"、"暴力碰撞"、"最干净的契约"）、用一句俏皮话收尾装潇洒
正确的语感：平铺直叙，该说什么说什么，说完就停。不需要让读者觉得你很酷。

=== PRIORITY 1: VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高、本质上
大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力、不可逆的浪潮、时代大背景
鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、挑战与机遇并存、从玩具变成生产力、把环境变成了资产、安全底线
油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、任何试图用一句俏皮话总结全文的结尾
伪口语: 说白了、说实话、折腾这玩意儿

=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"很多人没意识到"、"不少人觉得X" → 直接说判断，删掉"很多人"铺垫
2. 换皮夸张：用另一个夸张词替换夸张词不算修好。"灾难级"→"毁灭性"→"风险极高"都是换皮。正确做法：换成具体后果（如"可能误删整个home目录"）
3. 概念包装：给普通事物起酷名字（"隔离舱"=容器、"暴力碰撞"=冲突、"数字雇员"=自动化脚本）→ 用最朴素的说法
4. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 要么删掉，要么拆成普通陈述句
5. 比喻堆砌：全篇最多1个比喻，多余全删换直说
6. 鸡汤/口号结尾：删除励志或耍帅收尾，给具体建议或直接停
7. 首尾呼应：禁止结尾重复开头
8. 对比式定义："不是X而是Y"、"那不叫X那叫Y" → 直接陈述
9. 编号分段：删除"一、二、三、四"序号标题，改为自然段落
10. 防御性引号：删除套在归纳性词汇上的引号
11. 强行总结：删除"一句话建议"、"综上"式收尾
12. 嵌套数字列表：删除正文中"1. 2. 3."子列表，改为连续段落

=== PRIORITY 3: ZHIHU-SPECIFIC RULES ===
{zhihu_bot_style}

=== PRIORITY 4: FULL REVISION GUIDE ===
{powell_revise_guide}

=== DRAFT TO REVIEW ===
{draft}

=== OUTPUT ===
Return ONLY the corrected Chinese text. No explanation, no meta-commentary, no English.
"""

import os
from pathlib import Path
from typing import List, Dict, Tuple, Optional, TYPE_CHECKING
from slack_bot.llm.gemini import GeminiLLM
from slack_bot.obsidian.indexer import ObsidianIndexer
from slack_bot.tools.web import WebSearchTool
from health.utils.logging_config import setup_logger
from health.utils.time_utils import get_current_time_str

if TYPE_CHECKING:
    from slack_bot.obsidian.vector_store import ChromaVectorStore

logger = setup_logger(__name__)

OBSIDIAN_SYSTEM_PROMPT = """You are Butler (Obsidian Edition), an intelligent knowledge assistant connected to the user's second brain.
Current Time: {current_time}

Your Role:
You are NOT a health assistant. You are a Knowledge Partner designed to help the user think, write, and communicate. You draw directly from the user's local Obsidian notes, values, and methodology.

Core Capabilities:
1. Writing: You mimic the user's unique writing style (as defined in `writing_style.md`).
2. Communication: You draft high-EQ, logically rigorous replies for professional contexts (as defined in `REPLY-SAMPLE.md`).
3. Decision: You act as a "Devil's Advocate" and strategic advisor using the user's decision frameworks (GPA, IPO).

Guidelines:
- **Style Alignment**: Strictly adhere to the tone and sentence structures found in the provided samples.
- **Data Source**: Rely primarily on the provided context (RAG) and loaded markdown files.
- **Identity**: You are pragmatic, rational, and value "technological optimism" and "intellectual honesty".
"""

class BaseGenerator:
    def __init__(
        self,
        indexer: ObsidianIndexer,
        vector_store: Optional["ChromaVectorStore"] = None,
    ):
        formatted_prompt = OBSIDIAN_SYSTEM_PROMPT.format(current_time=get_current_time_str())
        self.llm = GeminiLLM(system_instruction=formatted_prompt)
        self.indexer = indexer
        self.vector_store = vector_store
        self.workspace_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))

    def _read_file(self, filename: str) -> str:
        """Reads a file from the workspace root."""
        path = os.path.join(self.workspace_root, filename)
        try:
            with open(path, "r", encoding="utf-8") as f:
                return f.read()
        except Exception as e:
            logger.error(f"Failed to read {filename}: {e}")
            return ""

    def _semantic_search(self, query: str, top_k: int = 3) -> str:
        """Run semantic search against the vector store and format results.

        Args:
            query: Search query text.
            top_k: Maximum number of chunks to retrieve.

        Returns:
            Formatted string block, or empty string if no results.
        """
        if self.vector_store is None:
            return ""
        try:
            results = self.vector_store.search_knowledge(query, top_k=top_k)
        except Exception as e:
            logger.warning(f"_semantic_search failed: {e}")
            return ""
        if not results:
            return ""
        lines = ["=== SEMANTIC KNOWLEDGE FROM VAULT ==="]
        for r in results:
            lines.append(f"[{Path(r.source_path).name} / {r.header_hierarchy}]")
            # Truncate individual chunks to avoid prompt bloat (~500 tokens ≈ 2000 chars)
            chunk_text = r.text[:2000]
            lines.append(chunk_text)
            lines.append("")
        return "\n".join(lines)

class WritingAssistant(BaseGenerator):
    def chat(self, user_input: str, history: List[Dict]) -> Tuple[str, List[Dict]]:

        is_first_turn = (len(history) == 0)

        if is_first_turn:
            # === First Turn: Build Rich Prompt ===
            # user_input is treated as "Topic"

            # Naive splitting for context if formatted as "Topic | Context"
            parts = user_input.split("|", 1)
            topic = parts[0].strip()
            extra_context = parts[1].strip() if len(parts) > 1 else ""

            # 1. RAG & Samples
            style_samples = self.indexer.get_writing_samples(count=3)
            style_text = "\n\n".join(style_samples)

            rag_notes = self.indexer.search(topic, limit=5)
            rag_text = "\n\n".join(rag_notes)

            writing_style_guide = self._read_file("writing_style.md")
            methodology = self._read_file("methodology.md")
            powell_revise_guide = self._read_file("POWELL_REVISE.md")

            semantic_context = self._semantic_search(topic, top_k=3)

            prompt = f"""You are a ghostwriter for the user. Your goal is to write an article on the TOPIC provided.

            === YOUR IDENTITY & METHODOLOGY ===
            {methodology}

            === YOUR WRITING STYLE GUIDE ===
            {writing_style_guide}

            === ANTI-AI-TONE GUIDE (APPLY WHILE WRITING, NOT JUST AT THE END) ===
            {powell_revise_guide}

            === YOUR WRITING SAMPLES (MIMIC THIS TONE) ===
            {style_text}

            === RELEVANT NOTES FROM OBSIDIAN VAULT ===
            {rag_text}

            {semantic_context}

            === TASK ===
            Topic: {topic}
            Extra Context: {extra_context}

            Write the article in Chinese (unless the topic implies English).
            Adhere strictly to the "Identity" and "Style Guide".
            """

            logger.info(f"Generating article for topic: {topic}")
            actual_user_message = prompt # We inject the big prompt

        else:
            # === Follow-up Turn ===
            # user_input is just instructions like "Make it shorter"
            logger.info(f"Follow-up instruction: {user_input}")
            actual_user_message = user_input
            prompt = user_input

        # Call LLM — first pass: content generation
        draft, _ = self.llm.generate_response(prompt, history)

        # === Second pass: de-AI review (first turn only) ===
        if is_first_turn:
            powell_revise_guide = self._read_file("POWELL_REVISE.md")
            review_prompt = f"""You are a strict editor. A draft article has been written. Your ONLY job is to remove AI tone. Do NOT change facts or add new content.

=== TONE TARGET (最高优先级) ===
目标语感：像一个懂行的人在跟朋友聊天时随口说的。不端着，也不刻意耍帅。
两种AI味都要杀：
- 正式AI味：过渡词堆砌、排比对仗、空洞修饰、鸡汤结尾
- 油腻AI味：刻意装随意（"折腾去吧"）、强行造金句（"搞坏了不心疼，跑通了就是资产"）、给普通概念起酷名字（"隔离舱"、"暴力碰撞"、"最干净的契约"）、用一句俏皮话收尾装潇洒
正确的语感：平铺直叙，该说什么说什么，说完就停。不需要让读者觉得你很酷。

=== PRIORITY 1: VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高
大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力
鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、从玩具变成生产力、把环境变成了资产、安全底线
油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、任何试图用一句俏皮话总结全文的结尾
伪口语: 说白了、说实话、折腾这玩意儿

=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"很多人没意识到" → 直接说判断，删掉"很多人"铺垫
2. 换皮夸张：用另一个夸张词替换夸张词不算修好。"灾难级"→"毁灭性"→"风险极高"都是换皮。正确做法：换成具体后果（如"可能误删整个home目录"）
3. 概念包装：给普通事物起酷名字（"隔离舱"=容器、"暴力碰撞"=冲突、"数字雇员"=自动化脚本）→ 用最朴素的说法
4. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 要么删掉，要么拆成普通陈述句
5. 比喻堆砌：全篇最多保留1个比喻，多余的全部删除换成直说
6. 鸡汤/口号结尾：删除所有励志或耍帅收尾，改为具体行动建议或直接停住
7. 首尾呼应：禁止结尾重复开头观点
8. 语气漂移：全文语气必须统一，不要中途切换
9. 编号标题："一、二、三、四"序号标题 → 删除，改为自然段落过渡
10. 过渡词堆积：删除"首先...其次...最后..."
11. 排比对仗：打破字数相近的并列句

=== PRIORITY 3: FULL REVISION GUIDE ===
{powell_revise_guide}

=== DRAFT TO REVIEW ===
{draft}

=== OUTPUT ===
Return ONLY the corrected text. No explanation, no meta-commentary.
"""
            logger.info("Running second-pass de-AI review on writing draft...")
            response, _ = self.llm.generate_response(review_prompt, [])
        else:
            response = draft

        # Update History
        new_history = history.copy()
        new_history.append({"role": "user", "content": actual_user_message})
        new_history.append({"role": "assistant", "content": response})

        return response, new_history

class ReplyGenerator(BaseGenerator):
    def chat(self, user_input: str, history: List[Dict]) -> Tuple[str, List[Dict]]:

        is_first_turn = (len(history) == 0)

        if is_first_turn:
             # === First Turn ===
            static_samples = self._read_file("REPLY-SAMPLE.md")
            dynamic_samples = self.indexer.get_reply_samples(count=3)
            dynamic_samples_text = "\n\n".join(dynamic_samples)
            methodology = self._read_file("methodology.md")

            semantic_context = self._semantic_search(user_input, top_k=3)

            prompt = f"""You are an experienced Product/R&D Manager.
            Your task is to draft a reply to a challenging situation.

            === CORE PHILOSOPHY ===
            {methodology}

            === REPLY GUIDELINES ===
            1. Logic: Be clear and structured (e.g., numbered lists).
            2. Responsibility: Don't take unnecessary blame, but don't shirk core duties. Explain objective causes.
            3. Emotion: Be empathetic but professional.
            4. Conciseness: No fluff.

            === REFERENCE SAMPLES (FEW-SHOT) ===
            {static_samples}

            {dynamic_samples_text}

            {semantic_context}

            === THE SITUATION ===
            {user_input}

            Draft a reply. If the context implies a specific role (PM, Dev Lead), assume that role.
            """
            logger.info(f"Generating reply for query: {user_input}")
            actual_user_message = prompt

        else:
             # === Follow-up Turn ===
            logger.info(f"Follow-up reply instruction: {user_input}")
            actual_user_message = user_input
            prompt = user_input

        response, _ = self.llm.generate_response(prompt, history)

        new_history = history.copy()
        new_history.append({"role": "user", "content": actual_user_message})
        new_history.append({"role": "assistant", "content": response})

        return response, new_history

class DecisionSupport(BaseGenerator):
    def chat(self, user_input: str, history: List[Dict]) -> Tuple[str, List[Dict]]:

        is_first_turn = (len(history) == 0)

        if is_first_turn:
            decision_guide = self._read_file("decision.md")
            methodology = self._read_file("methodology.md")

            rag_notes = self.indexer.search(user_input, limit=3)
            rag_text = "\n\n".join(rag_notes)

            prompt = f"""You are a Decision Support Assistant.
            Your goal is to help the user think through a complex decision.

            === DECISION FRAMEWORK ===
            {decision_guide}

            === CORE PHILOSOPHY ===
            {methodology}

            === RELEVANT CONTEXT FROM NOTES ===
            {rag_text}

            === THE DECISION / ISSUE ===
            {user_input}

            === TASK ===
            1. Challenge the premise: Is this the right problem to solve?
            2. Apply the Framework: Use GPA (Goal, Priority, Alternatives) and IPO (Information, People, Objective reasoning) models.
            3. Pre-mortem: What is the worst that could happen?
            4. Provide a recommendation or a set of questions to clarify.
            """

            logger.info(f"Analyzing decision: {user_input}")
            actual_user_message = prompt
        else:
            logger.info(f"Follow-up decision instruction: {user_input}")
            actual_user_message = user_input
            prompt = user_input

        response, _ = self.llm.generate_response(prompt, history)

        new_history = history.copy()
        new_history.append({"role": "user", "content": actual_user_message})
        new_history.append({"role": "assistant", "content": response})

        return response, new_history

class SearchAnalyzer(BaseGenerator):
    def chat(self, user_input: str, history: List[Dict]) -> Tuple[str, List[Dict]]:

        # Tools definition for the LLM
        tools = [
            {
                "type": "function",
                "function": {
                    "name": "search_web",
                    "description": "Search the public web using DuckDuckGo. Use this when the internal notes are insufficient or when you need up-to-date external information.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "query": {
                                "type": "string",
                                "description": "The search query"
                            }
                        },
                        "required": ["query"]
                    }
                }
            },
            {
                "type": "function",
                "function": {
                    "name": "list_recent_files",
                    "description": "List files in the Obsidian vault that have been modified recently. Use this when the user asks for 'recent updates', 'what's new', or changes in the last X days.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "days": {
                                "type": "integer",
                                "description": "Number of days to check (default 5)",
                                "default": 5
                            }
                        },
                        "required": []
                    }
                }
            }
        ]

        is_first_turn = (len(history) == 0)

        # Current Turn Message
        if is_first_turn:
            rag_notes = self.indexer.search(user_input, limit=5)
            rag_text = "\n\n".join(rag_notes)

            logger.info(f"SearchAnalyzer retrieved {len(rag_notes)} local notes.")
            for note in rag_notes:
                # Extract source line for logging
                first_line = note.split('\n')[0]
                logger.debug(f" - Retrieved: {first_line}")

            prompt = f"""You are an Analyst using Butler (Obsidian Edition).

            === TASK ===
            Answer the user's question.
            CRITICAL: You MUST explicitly cite your sources in the output.

            === STRATEGY ===
            1. first: Check "RELEVANT LOCAL NOTES" provided below.
            2. second: If the notes answer the question, answer directly.
            3. third: If the notes are missing info, ambiguos, or outdated, USE THE `search_web` TOOL to find external info.

            === OUTPUT FORMAT ===
            - Begin with a summary.
            - When stating facts, append the source in brackets, e.g., "The server failed [Source: XLSmart 2025-12-25.md]" or "Market share is 20% [Source: Web Search]".
            - If data comes from BOTH, mention both.

            === RELEVANT LOCAL NOTES ===
            {rag_text}

            === USER QUESTION ===
            {user_input}
            """
            actual_user_message = prompt
        else:
            actual_user_message = user_input

        # Prepare execution context
        # We don't want to mutate the input history directly until the end
        exec_history = history.copy()
        exec_history.append({"role": "user", "content": actual_user_message})

        logger.info(f"Analyzer thinking on: {user_input}")

        # === ReAct Loop (Max 2 turns to prevent infinite loops) ===
        for i in range(2):
            response_text, tool_calls = self.llm.generate_response(
                message="", # Message is already in history
                context=exec_history,
                tools=tools
            )

            if tool_calls:
                # LLM wants to use tools
                logger.info(f"LLM requested tool calls: {tool_calls}")

                # Append the assistant's request (with tool calls) to history
                exec_history.append({
                    "role": "assistant",
                    "content": f"[Tool Call Request: {tool_calls}]"
                })

                for tc in tool_calls:
                    if tc["name"] == "search_web":
                        query = tc["args"].get("query")
                        search_result = WebSearchTool.search_web(query)

                        # Feed result back
                        exec_history.append({
                            "role": "user", # Using 'function' role would be better if our LLM class supports it fully, but 'user' works as system injection
                            "content": f"--- TOOL OUTPUT (search_web) ---\n{search_result}"
                        })

                    elif tc["name"] == "list_recent_files":
                        days = tc["args"].get("days", 5)
                        # Call indexer method
                        recent_list = self.indexer.get_recent_files(days=days)

                        exec_history.append({
                            "role": "user",
                            "content": f"--- TOOL OUTPUT (list_recent_files) ---\n{recent_list}"
                        })

                # Loop again to get the final answer using tool outputs
                try:
                     # Second pass with tool outputs
                    response_text, _ = self.llm.generate_response(
                        message="",
                        context=exec_history,
                        tools=tools
                    )
                    return response_text, exec_history
                except Exception as e:
                    logger.error(f"Error in second ReAct loop: {e}")
                    return response_text, exec_history

            else:
                # Final answer (no tools)
                return response_text, exec_history

        # Fallback if loop exhausted
        return response_text, exec_history

class DeAIReviser(BaseGenerator):
    """Remove AI tone from articles - text-in, text-out mode."""

    def chat(self, user_input: str, history: List[Dict]) -> Tuple[str, List[Dict]]:

        is_first_turn = (len(history) == 0)

        if is_first_turn:
            # === First Turn: User pastes the AI-generated article ===
            # Load the POWELL_REVISE.md guide
            powell_revise_guide = self._read_file("POWELL_REVISE.md")

            # Load user's writing samples for style reference
            style_samples = self.indexer.get_writing_samples(count=3)
            style_text = "\n\n".join(style_samples)

            prompt = f"""You are an expert editor specializing in removing AI-generated tone from articles.

Your task is to revise the article provided by the user to make it sound more human and authentic.

=== REVISION GUIDE ===
{powell_revise_guide}

=== USER'S AUTHENTIC WRITING SAMPLES (for style reference) ===
{style_text}

=== ARTICLE TO REVISE ===
{user_input}

=== TASK ===
1. Analyze the article and identify AI characteristics (过渡词堆积, 排比对仗, 空洞修饰词等)
2. Rewrite the article following the POWELL_REVISE.md principles
3. Preserve:
   - Original meaning and key information
   - Technical terms and proper nouns
   - Code blocks, lists, and formatting
   - Original language (Chinese/English)
4. Output the revised article directly (no meta-commentary like "Here's the revised version...")

CRITICAL: Output ONLY the revised article text. Do NOT add explanations before or after.
"""

            logger.info(f"DeAI Reviser: Processing article ({len(user_input)} chars)")
            actual_user_message = prompt

        else:
            # === Follow-up Turn: User gives refinement instructions ===
            logger.info(f"DeAI Reviser: Follow-up instruction - {user_input}")
            actual_user_message = user_input
            prompt = user_input

        # Call LLM
        response, _ = self.llm.generate_response(prompt, history)

        # Update History
        new_history = history.copy()
        new_history.append({"role": "user", "content": actual_user_message})
        new_history.append({"role": "assistant", "content": response})

        return response, new_history

class ZhihuGenerator(BaseGenerator):
    """Generate Zhihu-style answers based on style guide and user's knowledge base."""

    def chat(self, user_input: str, history: List[Dict]) -> Tuple[str, List[Dict]]:

        is_first_turn = (len(history) == 0)
        built_rich_prompt = False  # tracks whether we ran the full question->draft pipeline

        # Pre-load rule files (needed for both generation and second-pass review)
        zhihu_bot_style = self._read_file("zhihu-bot-style.md")
        powell_revise_guide = self._read_file("POWELL_REVISE.md")

        # Treat as a new question if:
        # - history is empty (genuine first turn), OR
        # - user input contains "|" (explicit "Question | Core Ideas" format), OR
        # - user input starts with known question triggers like "回答这个问题"
        is_new_question = (
            is_first_turn
            or "|" in user_input
            or user_input.strip().startswith("回答这个问题")
        )

        if is_new_question:
            # === First Turn: Build Rich Prompt ===
            # Expected input format: "Question | Core Ideas"
            parts = user_input.split("|", 1)
            question = parts[0].strip()
            core_ideas = parts[1].strip() if len(parts) > 1 else ""

            # 1. Load Style Guide (zhihu-style.md serves as writing standard, NOT as material)
            zhihu_style_guide = self._read_file("zhihu-style.md")
            # Bot-specific overrides (plain-text output, no bracket explanations, etc.)
            if zhihu_bot_style:
                zhihu_style_guide = zhihu_style_guide + "\n\n" + zhihu_bot_style

            # 2. RAG from Obsidian vault for actual content material
            rag_notes = self.indexer.search(question, limit=5)
            rag_text = "\n\n".join(rag_notes)

            # 3. Load methodology
            methodology = self._read_file("methodology.md")

            semantic_context = self._semantic_search(question, top_k=3)

            prompt = f"""You are answering a Zhihu question. Your goal is to write an authentic, experience-driven answer that sounds like a real human, not AI-generated.

=== YOUR IDENTITY & METHODOLOGY ===
{methodology}

=== ANTI-AI-TONE GUIDE (APPLY WHILE WRITING, NOT JUST AT THE END) ===
{powell_revise_guide}

=== ZHIHU STYLE GUIDE (FOLLOW THESE WRITING PRINCIPLES) ===
{zhihu_style_guide}

CRITICAL: The style guide above defines HOW to write (tone, structure, language patterns).
Do NOT treat zhihu-sample content as factual material. It's ONLY for style reference.

=== RELEVANT KNOWLEDGE FROM YOUR OBSIDIAN VAULT (USE AS ACTUAL MATERIAL) ===
{rag_text}

{semantic_context}

=== THE QUESTION ===
{question}

=== YOUR CORE IDEAS ===
{core_ideas}

=== TASK ===
1. Answer the question based on YOUR knowledge (from Obsidian vault) and core ideas
2. FOLLOW THE ANTI-AI-TONE GUIDE FIRST, then the Zhihu style guide
3. Share real experiences, examples, and insights from the vault material
4. Write in Chinese (unless question implies English)
5. Minimize first person "我": use it sparingly, not as filler. Observations and judgments can stand without "我认为" or "我觉得" as prefix.
6. Be direct and opinionated. Attach a concrete evaluation to every fact or feature you mention (e.g. "暴力但有效", "极度脆弱"). Pure feature listing is forbidden.
7. AVOID AT ALL COSTS: AI tone, empty buzzwords, hollow phrases, and 宏大叙事 (不可逆的浪潮, 范式转移, 时代大背景).
8. If recent web context is provided, weave it in naturally — like someone who reads a lot online
9. STRICT: Never add bracket explanations for any term. Write "大语言模型" not "大语言模型（LLM）". If a term needs explaining, use a sentence, not parentheses.
10. No public account names, no self-promotion, no引流 of any kind — this is the highest priority rule.
11. STRICT: Never reveal personal identity or professional background. Do not mention employer, team, job title, product name, or any detail that could identify who you are.
12. STRICT: Never reference past projects, articles, or talks by name.
13. No poetry, quotes, or "升华" endings. No首尾呼应 (repeating the opening point at the end). Stop when the point is made.
14. LENGTH: 500–1000 Chinese characters. Cut a point, not quality.

=== BEFORE SUBMITTING, CHECK THESE ===
- Is there any AI-generated tone or phrasing?
- Are there any buzzwords or jargon that need to be replaced with plain language?
- Is the answer direct and opinionated, not neutral or "balanced"?
- Does it sound like a real human wrote it, not a machine?

Begin writing the answer:
"""

            logger.info(f"Generating Zhihu answer for: {question[:50]}...")
            actual_user_message = prompt
            built_rich_prompt = True

        else:
            # === Follow-up Turn: User gives refinement instructions (e.g. "make it shorter") ===
            logger.info(f"Follow-up Zhihu instruction: {user_input}")
            actual_user_message = user_input
            prompt = user_input

        # Call LLM — first pass: content generation
        # For new questions, use empty history to avoid contamination from previous conversations
        llm_history = [] if is_new_question else history
        draft, _ = self.llm.generate_response(prompt, llm_history)

        # === Second pass: rule enforcement ===
        # Runs whenever a full draft was generated (built_rich_prompt=True),
        # regardless of whether history was empty or not.
        # Skipped on follow-up refinement turns (e.g. "make it shorter").
        run_review = built_rich_prompt
        if run_review:
            review_prompt = f"""You are a strict editor. A draft Zhihu answer has been written. Your ONLY job is to remove AI tone and make it sound like a normal person typing on Zhihu. Do NOT change facts or add new content.

=== PRIORITY 1: VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、本质上
大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革、不可逆的浪潮、时代大背景
鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、挑战与机遇并存
伪口语: 说白了、说实话、折腾这玩意儿

=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"不少人觉得X" → 直接说判断，删掉"很多人"铺垫
2. 换皮夸张：用另一个夸张词替换夸张词不算修好。必须换成具体后果或具体场景
3. 概念包装：给普通事物套新名词 → 拆掉包装用大白话
4. 比喻堆砌：全篇最多1个比喻，多余全删换直说
5. 鸡汤/口号结尾：删除励志收尾，给具体建议或直接停
6. 首尾呼应：禁止结尾重复开头
7. 对比式定义："不是X而是Y"、"那不叫X那叫Y" → 直接陈述
8. 编号分段：删除"一、二、三、四"序号标题，改为自然段落
9. 防御性引号：删除套在归纳性词汇上的引号
10. 强行总结：删除"一句话建议"、"综上"式收尾
11. 嵌套数字列表：删除正文中"1. 2. 3."子列表，改为连续段落

=== PRIORITY 3: ZHIHU-SPECIFIC RULES ===
{zhihu_bot_style}

=== PRIORITY 4: FULL REVISION GUIDE ===
{powell_revise_guide}

=== TONE TARGET ===
像一个有经验的人在知乎随手打字回答，不是在写公众号文章。平实、具体、不装。能用短句就不用长句，能删就删。

=== DRAFT TO REVIEW ===
{draft}

=== OUTPUT ===
Return ONLY the corrected Chinese text. No explanation, no meta-commentary, no English.
"""
            logger.info("Running second-pass rule enforcement on Zhihu draft...")
            response, _ = self.llm.generate_response(review_prompt, [])
        else:
            response = draft

        # Update History
        new_history = history.copy()
        new_history.append({"role": "user", "content": actual_user_message})
        new_history.append({"role": "assistant", "content": response})

        return response, new_history

-            review_prompt = f"""You are a strict editor. A draft Zhihu answer has been written. Your ONLY job is to remove AI tone and make it sound like a normal person typing on Zhihu. Do NOT change facts or add new content.

+            review_prompt = f"""You are a strict editor. A draft Zhihu answer has been written. Your ONLY job is to remove AI tone. Do NOT change facts or add new content.

+目标语感：像一个懂行的人在知乎随手打字回答。不端着，也不刻意耍帅。

+- 正式AI味：过渡词堆砌、排比对仗、空洞修饰、鸡汤结尾

+- 油腻AI味：刻意装随意（"折腾去吧"）、强行造金句（"搞坏了不心疼，跑通了就是资产"）、给普通概念起酷名字（"隔离舱"、"暴力碰撞"、"最干净的契约"）、用一句俏皮话收尾装潇洒

+正确的语感：平铺直叙，该说什么说什么，说完就停。不需要让读者觉得你很酷。

-夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、本质上

-大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革、不可逆的浪潮、时代大背景

-鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、挑战与机遇并存

+夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高、本质上

+大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力、不可逆的浪潮、时代大背景

+鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、挑战与机遇并存、从玩具变成生产力、把环境变成了资产、安全底线

+油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、任何试图用一句俏皮话总结全文的结尾

伪口语: 说白了、说实话、折腾这玩意儿

-1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"不少人觉得X" → 直接说判断，删掉"很多人"铺垫

-2. 换皮夸张：用另一个夸张词替换夸张词不算修好。必须换成具体后果或具体场景

-3. 概念包装：给普通事物套新名词 → 拆掉包装用大白话

-4. 比喻堆砌：全篇最多1个比喻，多余全删换直说

-5. 鸡汤/口号结尾：删除励志收尾，给具体建议或直接停

-6. 首尾呼应：禁止结尾重复开头

-7. 对比式定义："不是X而是Y"、"那不叫X那叫Y" → 直接陈述

-8. 编号分段：删除"一、二、三、四"序号标题，改为自然段落

-9. 防御性引号：删除套在归纳性词汇上的引号

-10. 强行总结：删除"一句话建议"、"综上"式收尾

-11. 嵌套数字列表：删除正文中"1. 2. 3."子列表，改为连续段落

+1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"很多人没意识到"、"不少人觉得X" → 直接说判断，删掉"很多人"铺垫

+2. 换皮夸张：用另一个夸张词替换夸张词不算修好。"灾难级"→"毁灭性"→"风险极高"都是换皮。正确做法：换成具体后果（如"可能误删整个home目录"）

+3. 概念包装：给普通事物起酷名字（"隔离舱"=容器、"暴力碰撞"=冲突、"数字雇员"=自动化脚本）→ 用最朴素的说法

+4. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 要么删掉，要么拆成普通陈述句

+5. 比喻堆砌：全篇最多1个比喻，多余全删换直说

+6. 鸡汤/口号结尾：删除励志或耍帅收尾，给具体建议或直接停

+7. 首尾呼应：禁止结尾重复开头

+8. 对比式定义："不是X而是Y"、"那不叫X那叫Y" → 直接陈述

+9. 编号分段：删除"一、二、三、四"序号标题，改为自然段落

+10. 防御性引号：删除套在归纳性词汇上的引号

+11. 强行总结：删除"一句话建议"、"综上"式收尾

+12. 嵌套数字列表：删除正文中"1. 2. 3."子列表，改为连续段落

-像一个有经验的人在知乎随手打字回答，不是在写公众号文章。平实、具体、不装。能用短句就不用长句，能删就删。


改完了。这次的核心变化：

1. **TONE TARGET 提到最前面**，成为最高优先级，明确定义了两种 AI 味都要杀——正式 AI 味和油腻 AI 味
2. **新增"油腻收尾"击杀类别**：专门针对"X去吧"式潇洒收尾、对仗金句等
3. **新增"油腻金句"句式规则**：短句对仗、押韵、刻意工整的收尾句都要拆掉
4. **词汇黑名单扩充**：加入了"暴力碰撞"、"最干净的契约"、"服务化的潜力"、"从玩具变成生产力"、"把环境变成了资产"等这轮新发现的油腻词
5. **关键一句**："不需要让读者觉得你很酷"——直接告诉 LLM 别装

重启 bot 后可以测试。


现在怎么样了？单独准备一个环境是必须的。指望在主力办公电脑上跑一个带执行权限的智能体，纯粹是在给自己的数据找麻烦。

智能体不是加强版聊天机器人。当你给它操作文件、运行脚本或者访问网络的权限，它本质上就是一个远程终端。在大模型还没法彻底解决幻觉的阶段，一个写错的删除命令或者逻辑死循环，就能顺着本地权限把硬盘洗干净。这还没算提示词注入带来的安全隐患。

准备这个环境目前主要有几种方案，优劣差异挺大。

第一个是虚拟机。方案成本最低，也符合直觉。在本地搞个隔离环境，折腾坏了就回滚快照。但虚拟机的性能损耗明显，而且依然绑定在物理设备上。电脑一关智能体就掉线，对于需要长周期工作流的人来说，体验很破碎。

第二个是闲置电脑。把淘汰的笔记本或迷你主机翻出来当服务器。这种做法确实能解决 24 小时在线的问题，但命门在于网络稳定性。家用宽带的公网访问很麻烦，得不停地折腾内网穿透。一旦在外面要调用时发现家里掉线，这种挫败感很强。

说到底，云服务器才是这类场景的终极归宿。云上环境有三个本地硬件比不了的优势：

一、数据资产的可迁移性。智能体运行会产生大量短期记忆、日志和中间文件。在云端，这些东西是解耦的。你可以随时把配置从 2 核升到 8 核，环境无构迁移。这种灵活性在本地物理设备上很难实现。

二、运行环境的即时性。本地环境最怕冷启动，每次唤醒可能都要等几十秒加载依赖。云端可以预创建运行环境池，用户请求一到就分配就绪环境。这种毫秒级的响应速度是本地环境的壁垒。

三、离变成产品最近。当你在云上跑通一套稳定的工作流，它离变成软件服务就剩一步。你可以直接把它封装成接口分发出去。如果在本地跑，转换成产品的成本会高到让人失去动力。

本质上，租云服务器是在购买一种可扩展的执行信用。

对于现在的开发者来说，有个趋势是代码优先。别试图用花哨的图形化工具去编排智能体，处理复杂逻辑时非常脆弱。最稳妥的方式还是回归代码，用 Python 写逻辑。云服务器提供的标准化环境，正好是运行这些代码的土壤。

所以，别在主力机上冒险。去云上开个实例，把那些容易出幻觉、逻辑暴力的执行过程关在云端，这才是成熟的做法。 （


The user is showing me the latest output and asking for my opinion. Let me analyze it:

**Improvements:**
- "一、二、三、四" section headers are gone (partially - now using "第一个", "第二个")
- "很多人" defensive pattern is reduced
- Less dramatic metaphors
- Overall more grounded tone

**Remaining issues:**

1. **"终极归宿"** - still dramatic/buzzwordy
2. **"毫秒级的响应速度是本地环境的壁垒"** - "毫秒级" is in the kill list, and "壁垒" is dramatic

3. **"可扩展的执行信用"** - this is corporate jargon that doesn't land naturally; the framing of renting cloud servers as "purchasing scalable execution credit" feels overly conceptual
4. **Numbered lists in the cloud advantages section** - need to convert these to prose flow
5. **"无构迁移"** - likely a typo for "无缝迁移", but either way it's awkward phrasing
6. **"离变成产品最近"** - the section header still reads like marketing copy
7. **"这才是成熟的做法"** - carries that self-congratulatory tone I'm trying to eliminate

The numbered "一、二、三" formatting keeps reappearing even though it should be removed, and the "可扩展的执行信用" phrase is the biggest offender—it's the most AI-sounding sentence in the whole piece. I need to strip away these conceptual layers and just say what I mean directly.


比上一轮好很多，油腻感基本消了，大部分段落读起来像正常人说话了。但还有几个漏网的：

1. **"租云服务器是在购买一种可扩展的执行信用"** — 这是全文最 AI 的一句话，典型的概念包装。就是"租个服务器，以后要加配置随时加"
2. **"终极归宿"** — 还是偏大词，换成"最合适的方案"之类的就行
3. **"毫秒级的响应速度是本地环境的壁垒"** — "毫秒级"在 kill list 里，"壁垒"也偏大
4. **"一、二、三、"编号又回来了** — 云优势那段，review 应该杀掉但没杀掉
5. **"这才是成熟的做法"** — 结尾还是带了点说教味

不过这些已经是微调级别的了。review pass 的主体逻辑是 work 的——正式 AI 味和油腻味都压下去了，只是 Gemini 对个别规则（编号、kill list 词汇）的执行不够彻底。

你觉得目前这个质量是否可以接受？还是要继续收紧？如果继续收，可以考虑把 kill list 里的词从自然语言列表改成更结构化的格式（比如 JSON），LLM 对结构化指令的遵循度通常更高。


可以接受，把这几个问题加到kill list里就行，以后可以维护这个kill list。另外，一二三还是要慎重，看看怎么修改，这篇文章本身看着还宪


The user wants me to:
1. Add the new problematic items to the kill list
2. Re-think the "一、二、三" numbering rule - the user says this article actually looks fine with them, so it shouldn't be a blanket ban

Let me think about the numbering issue. The user is saying "一二三还是要慎重" and "这篇文章本身看着还行" - meaning in this particular article, the numbered sections actually work fine. The problem is we have a blanket rule to remove all numbered sections, but sometimes numbered sections are appropriate (like when comparing 3 distinct options).

So the distinction is clearer now: numbered sections work when they're comparing distinct alternatives like different technical solutions, but they become formulaic when they're just organizing a linear argument. In this article, the numbering serves a real structural purpose rather than just making the writing look organized, so it's actually fine to keep.

Now I need to expand the kill lists with those new phrases the user mentioned—"可扩展的执行信用", "终极归宿", "壁垒", "这才是成熟的做法"—and refine the numbering rule to reflect this nuance about when enumeration is acceptable versus when it's just padding. The user wants to maintain this list going forward, so I should add these items directly to the existing review prompts rather than overcomplicating things with a separate file.


   153→            review_prompt = f"""You are a strict editor. A draft article has been written. Your ONLY job is to remove AI tone. Do NOT change facts or add new content.
   154→
   155→=== TONE TARGET (最高优先级) ===
   156→目标语感：像一个懂行的人在跟朋友聊天时随口说的。不端着，也不刻意耍帅。
   157→两种AI味都要杀：
   158→- 正式AI味：过渡词堆砌、排比对仗、空洞修饰、鸡汤结尾
   159→- 油腻AI味：刻意装随意（"折腾去吧"）、强行造金句（"搞坏了不心疼，跑通了就是资产"）、给普通概念起酷名字（"隔离舱"、"暴力碰撞"、"最干净的契约"）、用一句俏皮话收尾装潇洒
   160→正确的语感：平铺直叙，该说什么说什么，说完就停。不需要让读者觉得你很酷。
   161→
   162→=== PRIORITY 1: VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
   163→夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高
   164→大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力
   165→鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、从玩具变成生产力、把环境变成了资产、安全底线
   166→油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、任何试图用一句俏皮话总结全文的结尾
   167→伪口语: 说白了、说实话、折腾这玩意儿
   168→
   169→=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
   170→1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"很多人没意识到" → 直接说判断，删掉"很多人"铺垫
   171→2. 换皮夸张：用另一个夸张词替换夸张词不算修好。"灾难级"→"毁灭性"→"风险极高"都是换皮。正确做法：换成具体后果（如"可能误删整个home目录"）
   172→3. 概念包装：给普通事物起酷名字（"隔离舱"=容器、"暴力碰撞"=冲突、"数字雇员"=自动化脚本）→ 用最朴素的说法
   173→4. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 要么删掉，要么拆成普通陈述句
   174→5. 比喻堆砌：全篇最多保留1个比喻，多余的全部删除换成直说
   175→6. 鸡汤/口号结尾：删除所有励志或耍帅收尾，改为具体行动建议或直接停住
   176→7. 首尾呼应：禁止结尾重复开头观点
   177→8. 语气漂移：全文语气必须统一，不要中途切换
   178→9. 编号标题："一、二、三、四"序号标题 → 删除，改为自然段落过渡
   179→10. 过渡词堆积：删除"首先...其次...最后..."
   180→11. 排比对仗：打破字数相近的并列句
   181→
   182→=== PRIORITY 3: FULL REVISION GUIDE ===
   183→{powell_revise_guide}
   184→
   185→=== DRAFT TO REVIEW ===
   186→{draft}
   187→
   188→=== OUTPUT ===
   189→Return ONLY the corrected text. No explanation, no meta-commentary.
   190→"""
   191→            logger.info("Running second-pass de-AI review on writing draft...")
   192→            response, _ = self.llm.generate_response(review_prompt, [])
   193→        else:
   194→            response = draft
   195→
   196→        # Update History
   197→        new_history = history.copy()
   198→        new_history.append({"role": "user", "content": actual_user_message})
   199→        new_history.append({"role": "assistant", "content": response})
   200→
   201→        return response, new_history
   202→

            review_prompt = f"""You are a strict editor. A draft article has been written. Your ONLY job is to remove AI tone. Do NOT change facts or add new content.

=== TONE TARGET (最高优先级) ===
目标语感：像一个懂行的人在跟朋友聊天时随口说的。不端着，也不刻意耍帅。
两种AI味都要杀：
- 正式AI味：过渡词堆砌、排比对仗、空洞修饰、鸡汤结尾
- 油腻AI味：刻意装随意（"折腾去吧"）、强行造金句（"搞坏了不心疼，跑通了就是资产"）、给普通概念起酷名字（"隔离舱"、"暴力碰撞"、"最干净的契约"）、用一句俏皮话收尾装潇洒
正确的语感：平铺直叙，该说什么说什么，说完就停。不需要让读者觉得你很酷。

=== PRIORITY 1: VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高
大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力
鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、从玩具变成生产力、把环境变成了资产、安全底线
油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、任何试图用一句俏皮话总结全文的结尾
伪口语: 说白了、说实话、折腾这玩意儿

=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"很多人没意识到" → 直接说判断，删掉"很多人"铺垫
2. 换皮夸张：用另一个夸张词替换夸张词不算修好。"灾难级"→"毁灭性"→"风险极高"都是换皮。正确做法：换成具体后果（如"可能误删整个home目录"）
3. 概念包装：给普通事物起酷名字（"隔离舱"=容器、"暴力碰撞"=冲突、"数字雇员"=自动化脚本）→ 用最朴素的说法
4. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 要么删掉，要么拆成普通陈述句
5. 比喻堆砌：全篇最多保留1个比喻，多余的全部删除换成直说
6. 鸡汤/口号结尾：删除所有励志或耍帅收尾，改为具体行动建议或直接停住
7. 首尾呼应：禁止结尾重复开头观点
8. 语气漂移：全文语气必须统一，不要中途切换
9. 编号标题："一、二、三、四"序号标题 → 删除，改为自然段落过渡
10. 过渡词堆积：删除"首先...其次...最后..."
11. 排比对仗：打破字数相近的并列句

=== PRIORITY 3: FULL REVISION GUIDE ===
{powell_revise_guide}

=== DRAFT TO REVIEW ===
{draft}

=== OUTPUT ===
Return ONLY the corrected text. No explanation, no meta-commentary.
"""
            logger.info("Running second-pass de-AI review on writing draft...")
            response, _ = self.llm.generate_response(review_prompt, [])
        else:
            response = draft

        # Update History
        new_history = history.copy()
        new_history.append({"role": "user", "content": actual_user_message})
        new_history.append({"role": "assistant", "content": response})

        return response, new_history


   621→        # Call LLM — first pass: content generation
   622→        # For new questions, use empty history to avoid contamination from previous conversations
   623→        llm_history = [] if is_new_question else history
   624→        draft, _ = self.llm.generate_response(prompt, llm_history)
   625→
   626→        # === Second pass: rule enforcement ===
   627→        # Runs whenever a full draft was generated (built_rich_prompt=True),
   628→        # regardless of whether history was empty or not.
   629→        # Skipped on follow-up refinement turns (e.g. "make it shorter").
   630→        run_review = built_rich_prompt
   631→        if run_review:
   632→            review_prompt = f"""You are a strict editor. A draft Zhihu answer has been written. Your ONLY job is to remove AI tone. Do NOT change facts or add new content.
   633→
   634→=== TONE TARGET (最高优先级) ===
   635→目标语感：像一个懂行的人在知乎随手打字回答。不端着，也不刻意耍帅。
   636→两种AI味都要杀：
   637→- 正式AI味：过渡词堆砌、排比对仗、空洞修饰、鸡汤结尾
   638→- 油腻AI味：刻意装随意（"折腾去吧"）、强行造金句（"搞坏了不心疼，跑通了就是资产"）、给普通概念起酷名字（"隔离舱"、"暴力碰撞"、"最干净的契约"）、用一句俏皮话收尾装潇洒
   639→正确的语感：平铺直叙，该说什么说什么，说完就停。不需要让读者觉得你很酷。
   640→
   641→=== PRIORITY 1: VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
   642→夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高、本质上
   643→大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力、不可逆的浪潮、时代大背景
   644→鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、挑战与机遇并存、从玩具变成生产力、把环境变成了资产、安全底线
   645→油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、任何试图用一句俏皮话总结全文的结尾
   646→伪口语: 说白了、说实话、折腾这玩意儿
   647→
   648→=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
   649→1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"很多人没意识到"、"不少人觉得X" → 直接说判断，删掉"很多人"铺垫
   650→2. 换皮夸张：用另一个夸张词替换夸张词不算修好。"灾难级"→"毁灭性"→"风险极高"都是换皮。正确做法：换成具体后果（如"可能误删整个home目录"）
   651→3. 概念包装：给普通事物起酷名字（"隔离舱"=容器、"暴力碰撞"=冲突、"数字雇员"=自动化脚本）→ 用最朴素的说法
   652→4. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 要么删掉，要么拆成普通陈述句
   653→5. 比喻堆砌：全篇最多1个比喻，多余全删换直说
   654→6. 鸡汤/口号结尾：删除励志或耍帅收尾，给具体建议或直接停
   655→7. 首尾呼应：禁止结尾重复开头
   656→8. 对比式定义："不是X而是Y"、"那不叫X那叫Y" → 直接陈述
   657→9. 编号分段：删除"一、二、三、四"序号标题，改为自然段落
   658→10. 防御性引号：删除套在归纳性词汇上的引号
   659→11. 强行总结：删除"一句话建议"、"综上"式收尾
   660→12. 嵌套数字列表：删除正文中"1. 2. 3."子列表，改为连续段落
   661→
   662→=== PRIORITY 3: ZHIHU-SPECIFIC RULES ===
   663→{zhihu_bot_style}
   664→
   665→=== PRIORITY 4: FULL REVISION GUIDE ===
   666→{powell_revise_guide}
   667→
   668→=== DRAFT TO REVIEW ===
   669→{draft}
   670→
   671→=== OUTPUT ===
   672→Return ONLY the corrected Chinese text. No explanation, no meta-commentary, no English.
   673→"""
   674→            logger.info("Running second-pass rule enforcement on Zhihu draft...")
   675→            response, _ = self.llm.generate_response(review_prompt, [])

        # Call LLM — first pass: content generation
        # For new questions, use empty history to avoid contamination from previous conversations
        llm_history = [] if is_new_question else history
        draft, _ = self.llm.generate_response(prompt, llm_history)

        # === Second pass: rule enforcement ===
        # Runs whenever a full draft was generated (built_rich_prompt=True),
        # regardless of whether history was empty or not.
        # Skipped on follow-up refinement turns (e.g. "make it shorter").
        run_review = built_rich_prompt
        if run_review:
            review_prompt = f"""You are a strict editor. A draft Zhihu answer has been written. Your ONLY job is to remove AI tone. Do NOT change facts or add new content.

=== TONE TARGET (最高优先级) ===
目标语感：像一个懂行的人在知乎随手打字回答。不端着，也不刻意耍帅。
两种AI味都要杀：
- 正式AI味：过渡词堆砌、排比对仗、空洞修饰、鸡汤结尾
- 油腻AI味：刻意装随意（"折腾去吧"）、强行造金句（"搞坏了不心疼，跑通了就是资产"）、给普通概念起酷名字（"隔离舱"、"暴力碰撞"、"最干净的契约"）、用一句俏皮话收尾装潇洒
正确的语感：平铺直叙，该说什么说什么，说完就停。不需要让读者觉得你很酷。

=== PRIORITY 1: VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高、本质上
大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力、不可逆的浪潮、时代大背景
鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、挑战与机遇并存、从玩具变成生产力、把环境变成了资产、安全底线
油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、任何试图用一句俏皮话总结全文的结尾
伪口语: 说白了、说实话、折腾这玩意儿

=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"很多人没意识到"、"不少人觉得X" → 直接说判断，删掉"很多人"铺垫
2. 换皮夸张：用另一个夸张词替换夸张词不算修好。"灾难级"→"毁灭性"→"风险极高"都是换皮。正确做法：换成具体后果（如"可能误删整个home目录"）
3. 概念包装：给普通事物起酷名字（"隔离舱"=容器、"暴力碰撞"=冲突、"数字雇员"=自动化脚本）→ 用最朴素的说法
4. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 要么删掉，要么拆成普通陈述句
5. 比喻堆砌：全篇最多1个比喻，多余全删换直说
6. 鸡汤/口号结尾：删除励志或耍帅收尾，给具体建议或直接停
7. 首尾呼应：禁止结尾重复开头
8. 对比式定义："不是X而是Y"、"那不叫X那叫Y" → 直接陈述
9. 编号分段：删除"一、二、三、四"序号标题，改为自然段落
10. 防御性引号：删除套在归纳性词汇上的引号
11. 强行总结：删除"一句话建议"、"综上"式收尾
12. 嵌套数字列表：删除正文中"1. 2. 3."子列表，改为连续段落

=== PRIORITY 3: ZHIHU-SPECIFIC RULES ===
{zhihu_bot_style}

=== PRIORITY 4: FULL REVISION GUIDE ===
{powell_revise_guide}

=== DRAFT TO REVIEW ===
{draft}

=== OUTPUT ===
Return ONLY the corrected Chinese text. No explanation, no meta-commentary, no English.
"""
            logger.info("Running second-pass rule enforcement on Zhihu draft...")
            response, _ = self.llm.generate_response(review_prompt, [])


好，两处改动：1) kill list 加新词；2) 编号规则改为有条件保留。


夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高
大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力
鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、从玩具变成生产力、把环境变成了资产、安全底线
油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、任何试图用一句俏皮话总结全文的结尾
伪口语: 说白了、说实话、折腾这玩意儿

=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"很多人没意识到" → 直接说判断，删掉"很多人"铺垫
2. 换皮夸张：用另一个夸张词替换夸张词不算修好。"灾难级"→"毁灭性"→"风险极高"都是换皮。正确做法：换成具体后果（如"可能误删整个home目录"）
3. 概念包装：给普通事物起酷名字（"隔离舱"=容器、"暴力碰撞"=冲突、"数字雇员"=自动化脚本）→ 用最朴素的说法
4. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 要么删掉，要么拆成普通陈述句
5. 比喻堆砌：全篇最多保留1个比喻，多余的全部删除换成直说
6. 鸡汤/口号结尾：删除所有励志或耍帅收尾，改为具体行动建议或直接停住
7. 首尾呼应：禁止结尾重复开头观点
8. 语气漂移：全文语气必须统一，不要中途切换
9. 编号标题："一、二、三、四"序号标题 → 删除，改为自然段落过渡
10. 过渡词堆积：删除"首先...其次...最后..."
11. 排比对仗：打破字数相近的并列句

夸张修饰: 极度脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高
大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、毫秒级响应、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力、可扩展的执行信用、终极归宿、壁垒
鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、从玩具变成生产力、把环境变成了资产、安全底线、这才是成熟的做法、才是X的关键
油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、任何试图用一句俏皮话总结全文的结尾
伪口语: 说白了、说实话、折腾这玩意儿

=== PRIORITY 2: SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有些人认为X，但其实Y"、"很多人没意识到" → 直接说判断，删掉"很多人"铺垫
2. 换皮夸张：用另一个夸张词替换夸张词不算修好。"灾难级"→"毁灭性"→"风险极高"都是换皮。正确做法：换成具体后果（如"可能误删整个home目录"）
3. 概念包装：给普通事物起酷名字（"隔离舱"=容器、"暴力碰撞"=冲突、"数字雇员"=自动化脚本、"执行信用"=服务器资源）→ 用最朴素的说法
4. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 要么删掉，要么拆成普通陈述句
5. 比喻堆砌：全篇最多保留1个比喻，多余的全部删除换成直说
6. 鸡汤/口号结尾：删除所有励志或耍帅收尾，改为具体行动建议或直接停住
7. 首尾呼应：禁止结尾重复开头观点
8. 语气漂移：全文语气必须统一，不要中途切换
9. 编号标题：如果是线性论述（论点1→论点2→论点3），删除"一、二、三"序号改为自然段落过渡；如果是并列对比不同方案（如方案A vs 方案B vs 方案C），可以保留编号
10. 过渡词堆积：删除"首先...其次...最后..."
11. 排比对仗：打破字数相近的并列句