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

- 禁止：防御性写法（很多人觉得X但其实Y）、对比式定义（不是X是Y）

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:
            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味：刻意装随意（"折腾去吧"）、强行造金句（"搞坏了不心疼，跑通了就是资产"）、给普通概念起酷名字（"隔离舱"、"暴力碰撞"、"最干净的契约"）、用一句俏皮话收尾装潇洒
正确的语感：平铺直叙，该说什么说什么，说完就停。不需要让读者觉得你很酷。

=== VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
1. 夸张修饰: 极度脆弱、极其脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高、瞬间崩断、精疲力竭、真金白银、本质上
2. 大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、毫秒级响应、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力、可扩展的执行信用、终极归宿、壁垒、分水岭、沉淀（动词修饰数据/经验时）、云端办公室、命门
3. 鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、从玩具变成生产力、把环境变成了资产、安全底线、这才是成熟的做法、才是X的关键、不是建议是必须
4. 概念包装: 裸奔（=没防护）、修路/飙车（=基础工作/高级用法）、提款机（=攻击目标）、隔离舱（=容器）、数字雇员（=脚本）、执行信用（=资源）、云端办公室（=云服务器）、牢笼（=限制）、独立房间（=隔离环境）
5. 油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、俏皮话总结全文、"把X关在Y里...把Y放在Z上"式排比收尾
6. 伪口语: 说白了、说实话、折腾这玩意儿

=== SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有人X，有人已经Y了"、"一部分人在X，另一部分人在Y" → 直接说判断，删掉铺垫
2. 对比式定义（最高优先级，必须逐句扫描）：
   触发词："不是……而是……"、"本质上是"、"其实是"、"所谓的"、"从X变成了Y"、"别把X当Y"
   改写方法：删掉否定/对比框架，直接陈述后半句。具体示例：
   "不是怎么写代码，而是写成什么样才对" → "怎么写代码已经不够用了。更要命的是写成什么样才对"
   "本质上是给AI装导航" → "就是给AI装导航"
   "所谓的技术门槛，从熟练度变成了判断力" → "技术门槛变了。以前拼熟练度，现在拼判断力"
   "其实是经典软件工程的复兴" → "老一套方法论反而活过来了"
3. 换皮夸张：用另一个夸张词替换夸张词不算修好 → 换成具体后果
4. 概念包装：给普通事物起酷名字 → 用最朴素的说法
5. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 删掉或拆成普通句
6. 比喻堆砌：全篇最多1个比喻，多余全删换直说
7. 鸡汤/口号结尾：删除励志或耍帅收尾，给具体建议或直接停
8. 首尾呼应：禁止结尾重复开头
9. 编号分段：线性论述删除序号改自然段落；并列对比可保留
10. 强行总结：删除"综上"/"一句话"式收尾
11. 关键词重复：同一个修辞性词汇全篇只能出现一次
12. 过渡词换皮："第一个要说的是"="首先"，"另一个问题是"="其次" → 直接删掉过渡，开门见山
13. 伪分类二分法："一部分人X，另一部分人Y"、"修路派/飙车派" → 删除分类框架，直接说事实

=== 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 second-pass review)
        zhihu_bot_style = self._read_file("zhihu-bot-style.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 "回答这个问题", OR
        # - user input contains a Zhihu question URL (pasting a new question link)
        is_new_question = (
            is_first_turn
            or "|" in user_input
            or user_input.strip().startswith("回答这个问题")
            or "zhihu.com/question/" in user_input
        )

        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}

=== 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. Write in Chinese (unless question implies English)
3. Be direct and opinionated — attach concrete evaluation to every fact (not feature listing)
4. If recent web context is provided, weave it in naturally
5. STRICT: Never add bracket explanations. Write "大语言模型" not "大语言模型（LLM）"
6. No self-promotion, no identity reveal, no past project references
7. No poetry, quotes, or "升华" endings. Stop when the point is made.
8. LENGTH: 500–1000 Chinese characters. Cut a point, not quality.

=== ANTI-AI CORE RULES (apply while writing) ===
- 语感目标：像懂行的人随手打字，不端着，不耍帅
- 禁止：排比对仗、过渡词堆砌（首先/其次/最后）、空洞修饰（深入/全面/显著）
- 禁止：比喻超过1个、概念包装（给普通事物起酷名字）、鸡汤结尾
- 禁止：防御性写法（很多人觉得X但其实Y）、对比式定义（不是X是Y）
- 陈述事实时必须附带主观判断，不要纯客观罗列

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. Do NOT change facts or add new content.

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

=== VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
1. 夸张修饰: 极度脆弱、极其脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高、瞬间崩断、精疲力竭、真金白银、本质上
2. 大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、毫秒级响应、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力、不可逆的浪潮、时代大背景、可扩展的执行信用、终极归宿、壁垒、分水岭、沉淀（动词修饰数据/经验时）、云端办公室、命门
3. 鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、挑战与机遇并存、从玩具变成生产力、把环境变成了资产、安全底线、这才是成熟的做法、才是X的关键、不是建议是必须
4. 概念包装: 裸奔（=没防护）、修路/飙车（=基础工作/高级用法）、提款机（=攻击目标）、隔离舱（=容器）、数字雇员（=脚本）、执行信用（=资源）、云端办公室（=云服务器）、牢笼（=限制）、独立房间（=隔离环境）
5. 油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、俏皮话总结全文、"把X关在Y里...把Y放在Z上"式排比收尾
6. 伪口语: 说白了、说实话、折腾这玩意儿

=== SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有人X，有人已经Y了"、"一部分人在X，另一部分人在Y" → 直接说判断，删掉铺垫
2. 对比式定义（最高优先级，必须逐句扫描）：
   触发词："不是……而是……"、"本质上是"、"其实是"、"所谓的"、"从X变成了Y"、"别把X当Y"
   改写方法：删掉否定/对比框架，直接陈述后半句。具体示例：
   "不是怎么写代码，而是写成什么样才对" → "怎么写代码已经不够用了。更要命的是写成什么样才对"
   "本质上是给AI装导航" → "就是给AI装导航"
   "所谓的技术门槛，从熟练度变成了判断力" → "技术门槛变了。以前拼熟练度，现在拼判断力"
   "其实是经典软件工程的复兴" → "老一套方法论反而活过来了"
3. 换皮夸张：用另一个夸张词替换夸张词不算修好 → 换成具体后果
4. 概念包装：给普通事物起酷名字 → 用最朴素的说法
5. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 删掉或拆成普通句
6. 比喻堆砌：全篇最多1个比喻，多余全删换直说
7. 鸡汤/口号结尾：删除励志或耍帅收尾，给具体建议或直接停
8. 首尾呼应：禁止结尾重复开头
9. 编号分段：线性论述删除序号改自然段落；并列对比可保留
10. 强行总结：删除"综上"/"一句话"式收尾
11. 关键词重复：同一个修辞性词汇全篇只能出现一次
12. 过渡词换皮："第一个要说的是"="首先"，"另一个问题是"="其次" → 直接删掉过渡，开门见山
13. 伪分类二分法："一部分人X，另一部分人Y"、"修路派/飙车派" → 删除分类框架，直接说事实

=== 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

- 语感目标：像懂行的人随手打字，不端着，不耍帅

- 禁止：比喻超过1个、概念包装（给普通事物起酷名字）、鸡汤结尾

-- 禁止：防御性写法（很多人觉得X但其实Y）、对比式定义（不是X是Y）

+- 禁止：防御性写法（很多人觉得X但其实Y）、对比式定义（不是X是Y、本质上是、其实是、所谓的、从X变成了Y）

- 陈述事实时必须附带主观判断，不要纯客观罗列


最后把 VOCABULARY KILL LIST 里也加上 "本质上" 后面遗漏的几个词。


162	1. 夸张修饰: 极度脆弱、极其脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高、瞬间崩断、精疲力竭、真金白银、本质上
163	2. 大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、毫秒级响应、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力、可扩展的执行信用、终极归宿、壁垒、分水岭、沉淀（动词修饰数据/经验时）、云端办公室、命门
164	3. 鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、从玩具变成生产力、把环境变成了资产、安全底线、这才是成熟的做法、才是X的关键、不是建议是必须
165	4. 概念包装: 裸奔（=没防护）、修路/飙车（=基础工作/高级用法）、提款机（=攻击目标）、隔离舱（=容器）、数字雇员（=脚本）、执行信用（=资源）、云端办公室（=云服务器）、牢笼（=限制）、独立房间（=隔离环境）
166	5. 油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、俏皮话总结全文、"把X关在Y里...把Y放在Z上"式排比收尾

1. 夸张修饰: 极度脆弱、极其脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高、瞬间崩断、精疲力竭、真金白银、本质上
2. 大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、毫秒级响应、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力、可扩展的执行信用、终极归宿、壁垒、分水岭、沉淀（动词修饰数据/经验时）、云端办公室、命门
3. 鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、从玩具变成生产力、把环境变成了资产、安全底线、这才是成熟的做法、才是X的关键、不是建议是必须
4. 概念包装: 裸奔（=没防护）、修路/飙车（=基础工作/高级用法）、提款机（=攻击目标）、隔离舱（=容器）、数字雇员（=脚本）、执行信用（=资源）、云端办公室（=云服务器）、牢笼（=限制）、独立房间（=隔离环境）
5. 油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、俏皮话总结全文、"把X关在Y里...把Y放在Z上"式排比收尾


634	两种AI味都要杀：
635	- 正式AI味：过渡词堆砌、排比对仗、空洞修饰、鸡汤结尾
636	- 油腻AI味：刻意装随意（"折腾去吧"）、强行造金句（"搞坏了不心疼，跑通了就是资产"）、给普通概念起酷名字（"隔离舱"、"暴力碰撞"、"最干净的契约"）、用一句俏皮话收尾装潇洒
637	正确的语感：平铺直叙，该说什么说什么，说完就停。不需要让读者觉得你很酷。
638

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


"本质上"已经在 VOCABULARY KILL LIST 的夸张修饰类里了。但 "其实是"、"所谓的" 还没有。让我加上。两处 kill list 都需要更新。


Edit

1. 夸张修饰: 极度脆弱、极其脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高、瞬间崩断、精疲力竭、真金白银、本质上

1. 夸张修饰: 极度脆弱、极其脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高、瞬间崩断、精疲力竭、真金白银、本质上、其实是、所谓的


The file /root/projects/butler/slack_bot/obsidian/generators.py has been updated. All occurrences were successfully replaced.

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:
            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味：刻意装随意（"折腾去吧"）、强行造金句（"搞坏了不心疼，跑通了就是资产"）、给普通概念起酷名字（"隔离舱"、"暴力碰撞"、"最干净的契约"）、用一句俏皮话收尾装潇洒
正确的语感：平铺直叙，该说什么说什么，说完就停。不需要让读者觉得你很酷。

=== VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
1. 夸张修饰: 极度脆弱、极其脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高、瞬间崩断、精疲力竭、真金白银、本质上
2. 大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、毫秒级响应、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力、可扩展的执行信用、终极归宿、壁垒、分水岭、沉淀（动词修饰数据/经验时）、云端办公室、命门
3. 鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、从玩具变成生产力、把环境变成了资产、安全底线、这才是成熟的做法、才是X的关键、不是建议是必须
4. 概念包装: 裸奔（=没防护）、修路/飙车（=基础工作/高级用法）、提款机（=攻击目标）、隔离舱（=容器）、数字雇员（=脚本）、执行信用（=资源）、云端办公室（=云服务器）、牢笼（=限制）、独立房间（=隔离环境）
5. 油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、俏皮话总结全文、"把X关在Y里...把Y放在Z上"式排比收尾
6. 伪口语: 说白了、说实话、折腾这玩意儿

=== SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有人X，有人已经Y了"、"一部分人在X，另一部分人在Y" → 直接说判断，删掉铺垫
2. 对比式定义（最高优先级，必须逐句扫描）：
   触发词："不是……而是……"、"本质上是"、"其实是"、"所谓的"、"从X变成了Y"、"别把X当Y"
   改写方法：删掉否定/对比框架，直接陈述后半句。具体示例：
   "不是怎么写代码，而是写成什么样才对" → "怎么写代码已经不够用了。更要命的是写成什么样才对"
   "本质上是给AI装导航" → "就是给AI装导航"
   "所谓的技术门槛，从熟练度变成了判断力" → "技术门槛变了。以前拼熟练度，现在拼判断力"
   "其实是经典软件工程的复兴" → "老一套方法论反而活过来了"
3. 换皮夸张：用另一个夸张词替换夸张词不算修好 → 换成具体后果
4. 概念包装：给普通事物起酷名字 → 用最朴素的说法
5. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 删掉或拆成普通句
6. 比喻堆砌：全篇最多1个比喻，多余全删换直说
7. 鸡汤/口号结尾：删除励志或耍帅收尾，给具体建议或直接停
8. 首尾呼应：禁止结尾重复开头
9. 编号分段：线性论述删除序号改自然段落；并列对比可保留
10. 强行总结：删除"综上"/"一句话"式收尾
11. 关键词重复：同一个修辞性词汇全篇只能出现一次
12. 过渡词换皮："第一个要说的是"="首先"，"另一个问题是"="其次" → 直接删掉过渡，开门见山
13. 伪分类二分法："一部分人X，另一部分人Y"、"修路派/飙车派" → 删除分类框架，直接说事实

=== 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 second-pass review)
        zhihu_bot_style = self._read_file("zhihu-bot-style.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 "回答这个问题", OR
        # - user input contains a Zhihu question URL (pasting a new question link)
        is_new_question = (
            is_first_turn
            or "|" in user_input
            or user_input.strip().startswith("回答这个问题")
            or "zhihu.com/question/" in user_input
        )

        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}

=== 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. Write in Chinese (unless question implies English)
3. Be direct and opinionated — attach concrete evaluation to every fact (not feature listing)
4. If recent web context is provided, weave it in naturally
5. STRICT: Never add bracket explanations. Write "大语言模型" not "大语言模型（LLM）"
6. No self-promotion, no identity reveal, no past project references
7. No poetry, quotes, or "升华" endings. Stop when the point is made.
8. LENGTH: 500–1000 Chinese characters. Cut a point, not quality.

=== ANTI-AI CORE RULES (apply while writing) ===
- 语感目标：像懂行的人随手打字，不端着，不耍帅
- 禁止：排比对仗、过渡词堆砌（首先/其次/最后）、空洞修饰（深入/全面/显著）
- 禁止：比喻超过1个、概念包装（给普通事物起酷名字）、鸡汤结尾
- 禁止：防御性写法（很多人觉得X但其实Y）、对比式定义（不是X是Y、本质上是、其实是、所谓的、从X变成了Y）
- 陈述事实时必须附带主观判断，不要纯客观罗列

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. Do NOT change facts or add new content.

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

=== VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
1. 夸张修饰: 极度脆弱、极其脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高、瞬间崩断、精疲力竭、真金白银、本质上
2. 大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、毫秒级响应、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力、不可逆的浪潮、时代大背景、可扩展的执行信用、终极归宿、壁垒、分水岭、沉淀（动词修饰数据/经验时）、云端办公室、命门
3. 鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、挑战与机遇并存、从玩具变成生产力、把环境变成了资产、安全底线、这才是成熟的做法、才是X的关键、不是建议是必须
4. 概念包装: 裸奔（=没防护）、修路/飙车（=基础工作/高级用法）、提款机（=攻击目标）、隔离舱（=容器）、数字雇员（=脚本）、执行信用（=资源）、云端办公室（=云服务器）、牢笼（=限制）、独立房间（=隔离环境）
5. 油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、俏皮话总结全文、"把X关在Y里...把Y放在Z上"式排比收尾
6. 伪口语: 说白了、说实话、折腾这玩意儿

=== SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有人X，有人已经Y了"、"一部分人在X，另一部分人在Y" → 直接说判断，删掉铺垫
2. 对比式定义（最高优先级，必须逐句扫描）：
   触发词："不是……而是……"、"本质上是"、"其实是"、"所谓的"、"从X变成了Y"、"别把X当Y"
   改写方法：删掉否定/对比框架，直接陈述后半句。具体示例：
   "不是怎么写代码，而是写成什么样才对" → "怎么写代码已经不够用了。更要命的是写成什么样才对"
   "本质上是给AI装导航" → "就是给AI装导航"
   "所谓的技术门槛，从熟练度变成了判断力" → "技术门槛变了。以前拼熟练度，现在拼判断力"
   "其实是经典软件工程的复兴" → "老一套方法论反而活过来了"
3. 换皮夸张：用另一个夸张词替换夸张词不算修好 → 换成具体后果
4. 概念包装：给普通事物起酷名字 → 用最朴素的说法
5. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 删掉或拆成普通句
6. 比喻堆砌：全篇最多1个比喻，多余全删换直说
7. 鸡汤/口号结尾：删除励志或耍帅收尾，给具体建议或直接停
8. 首尾呼应：禁止结尾重复开头
9. 编号分段：线性论述删除序号改自然段落；并列对比可保留
10. 强行总结：删除"综上"/"一句话"式收尾
11. 关键词重复：同一个修辞性词汇全篇只能出现一次
12. 过渡词换皮："第一个要说的是"="首先"，"另一个问题是"="其次" → 直接删掉过渡，开门见山
13. 伪分类二分法："一部分人X，另一部分人Y"、"修路派/飙车派" → 删除分类框架，直接说事实

=== 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

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

-1. 夸张修饰: 极度脆弱、极其脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高、瞬间崩断、精疲力竭、真金白银、本质上

+1. 夸张修饰: 极度脆弱、极其脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高、瞬间崩断、精疲力竭、真金白银、本质上、其实是、所谓的

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


好了，改动完成。总结一下：

---

**改了什么：**

1. **`zhihu-bot-style.md`** — "禁止对比式定义句式"那条：
   - 扩展触发词：加了 "本质上是"、"其实是"、"所谓的"、"从X变成了Y"
   - 加了 5 组 before/after 改写示例

2. **`generators.py`** 两处 review prompt 的 SENTENCE PATTERN KILL LIST：
   - 第2条从一行抽象规则 → 展开为带触发词 + 4 组具体改写示例
   - 标记为"最高优先级，必须逐句扫描"

3. **`generators.py`** 第一个 pass 的 ANTI-AI CORE RULES：
   - 对比式定义的触发词从 `（不是X是Y）` 扩展为 `（不是X是Y、本质上是、其实是、所谓的、从X变成了Y）`

4. **VOCABULARY KILL LIST**：加了 "其实是"、"所谓的"

**为什么不加 sub-agent：**
- 问题不是审校不够多，是审校规则不够具体。LLM 对 "直接陈述后半句" 这种抽象指令的理解不稳定，但对 before/after 示例的遵从度很高。
- 多一个 agent = 多一次 API 调用的延迟和费用，收益不如改 prompt。

**风险：** 改动仅影响 prompt 文本，不改函数签名，不加依赖。最可能的问题是示例过多导致 prompt 变长，但增量不大（约 300 字）。

**验证：** 用 `mode zhihu` 生成一篇新回答，看是否还有 "不是...而是..."、"本质上是"、"其实是" 漏网。


node "${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js" "stop:check-console-log" "scripts/hooks/check-console-log.js" "standard,strict"

node "${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js" "stop:session-end" "scripts/hooks/session-end.js" "minimal,standard,strict"

node "${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js" "stop:evaluate-session" "scripts/hooks/evaluate-session.js" "minimal,standard,strict"

node "${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js" "stop:cost-tracker" "scripts/hooks/cost-tracker.js" "minimal,standard,strict"


看看现在好一些了吗：对大部分人来说，关于 OpenClaw 的怀疑确实是事实。代码一直都很强大，但绝大部分人没法直接享受到它的红利。AI 和 OpenClaw 的出现让门槛低了一点，但目前看它还是只能流向那一小波有行动力的人。现在能把 OpenClaw 玩明白的，基本是天生爱钻研、想让机器替自己干活的人。中间混杂着不少卖配置教程牟利的，这部分就不细说了。想用好 OpenClaw 确实不容易。现在的 OpenClaw 更像是个处于实验阶段的个人 AI 系统原型，把大模型、通讯工具和操作系统接口凑在了一起。这套架构能让 AI 从“你问我答”的聊天框变成一个全天候盯着任务的运行程序，但代价是上手门槛很高。很多人第一周就放弃了。要搞定 Docker 环境，折腾内网穿透，申请各种平台的 Bot 权限，还得理解 Token 消耗和向量记忆。这种配置过程劝退了一半人。大家还在发愁能不能跑起来的时候，大模型那边版本一更新，本地环境可能就崩了。这种不稳定性对普通用户很不友好。而且，OpenClaw 的真实价值被很多演示视频带偏了。很多人觉得 AI 能自动写完大项目很厉害，但这其实是底层模型的能力。OpenClaw 真正的价值在于主动性和持续运行。比如它能全天监控邮箱，把急事发到社交软件上，或者在你睡觉时去巡检服务器日志。这种持续运行的状态才是它和 ChatGPT 这种对话工具的区别。但这也带来了安全和信任问题。如果直接把控制权交给一个概率驱动的 AI，就要做好它因为幻觉执行错误操作的准备。目前 OpenClaw 缺乏严密的审计机制，权限管理也简单。如果你不懂技术，根本不知道 AI 刚才在后台对文件系统做了什么。所以 OpenClaw 现在的处境很像早期的 Linux。极客觉得好玩，普通人觉得是一堆乱码。它现在缺的不是功能，而是一个开箱即用且带安全边界的方案。如果你对技术没兴趣，只是想找个好用的助手，等手机厂商或互联网大厂的集成方案更合适。如果你有一定动手能力，想看 AI 到底是怎么跟现实世界交互的，那 OpenClaw 是目前了解 AI Agent 的一个窗口。它让你能体验到 AI 操作系统的运行逻辑。这种折腾本身就是目前玩 OpenClaw 的主要内容。


当前模型 claude-opus-4-6 负载已经达到上限，请稍后重试 (request id: 202603301936268206611624hzwf8Xv)


当前模型 claude-opus-4-6 负载已经达到上限，请稍后重试 (request id: 202603301936276219750931ryQe7FL)


当前模型 claude-opus-4-6 负载已经达到上限，请稍后重试 (request id: 2026033019362950912740JFcPpboV)


当前模型 claude-opus-4-6 负载已经达到上限，请稍后重试 (request id: 20260330193631307831608fGbU9LI7)


The user wants me to review this new generated text to see if the problematic patterns we just fixed are still appearing. Let me scan through it carefully.

Let me check for the specific patterns:

1. **"不是...而是..."** - Let me scan...
   - "它现在缺的不是功能，而是一个开箱即用且带安全边界的方案。" — **还有！** This is exactly the pattern we're trying to kill.

2. **"本质上是"** - Not found. Good.


比之前好很多，但还有 3 处漏网：

---

**1. "不是...而是..."**
> 它现在缺的**不是**功能，**而是**一个开箱即用且带安全边界的方案。

改：功能够用了。缺一个开箱即用且带安全边界的方案。

**2. "其实是"**
> 但这**其实是**底层模型的能力。

改：但那是底层模型的能力。

**3. "从X变成Y"**
> 让 AI 从"你问我答"的聊天框**变成**一个全天候盯着任务的运行程序

改：AI 不再只是"你问我答"的聊天框，它能全天候盯着任务跑。

---

还有一处边缘 case：

**4. "才是...的区别"**（对比式定义的变体）
> 这种持续运行的状态**才是**它和 ChatGPT 这种对话工具的区别。

改：ChatGPT 你关了浏览器它就停了。OpenClaw 一直在跑。

---

说明这几个词在 prompt 里虽然加了规则和示例，但 "其实是"、"从X变成Y" 这类对模型来说太自然了，单靠规则仍然会漏。有两个加强方向：

1. **在 VOCABULARY KILL LIST 里把"才是"也加进去** —— 它是"本质上是"的同义替换，模型会用它来绕过规则
2. **"从X变成Y"在第二 pass 的示例里再加一组**，目前示例里的是名词场景（"从熟练度变成判断力"），缺动态场景的示例

要我现在把这两处也更新到 prompt 里吗？


好的，更新吧


38	禁止对比式定义句式。严禁使用"不是……而是……"、"不要把它当成……得把它当成……"、"那不叫……那叫……"、"从X变成了Y"、"本质上是"、"其实是"、"所谓的"等通过否定或重新定义来制造深度感的句式。直接陈述它是什么、该怎么做。
39
40	以下是具体改写示例，遇到类似句式必须按此方式改写：
41
42	错误：现在的核心挑战不是"怎么写代码"，而是"代码写成什么样才是对的"。
43	正确："怎么写代码"这个问题已经不够用了。更要命的是"写成什么样才对"。
44
45	错误：好的工程实践本质上是在给 AI 提供高质量的导航。
46	正确：好的工程实践就是给 AI 装导航。
47
48	错误：所谓的技术门槛，已经从"熟练度"变成了"判断力"。
49	正确：技术门槛变了。以前拼熟练度，现在拼判断力。
50
51	错误：这种分而治之的思路，其实是经典软件工程在 AI 时代的复兴。
52	正确：分而治之，老一套方法论，反而在 AI 时代活过来了。
53
54	错误：工程师的角色正在从"打字员"变成"系统审计员"。
55	正确：工程师现在干的活更像系统审计员。
56
57	禁止编号分段。知乎回答不是公文，绝对禁止用"一、二、三、四"或"首先、其次、再次、最后"做段落划分。通过段落的自然推进切换话题，思维可以网状发散，不需要强行编号。
58
59	强制句长落差（Burstiness）。相邻句子的长度和结构必须产生碰撞。前一句是解释原理的长复合句，下一句必须是极短的陈述或结论。绝对禁止连续出现三句长度相近的完整陈述句。均匀的句长节奏是 AI 文本最显著的特征之一。
60
61	允许不完整句法（Fragmented Syntax）。在罗列工程步骤、系统架构或痛点时，直接抛出名词组合或动词短语，不需要每句话都补齐主谓宾。技术复盘里没人会把主语补全。全文必须出现若干缺少主语的短句，甚至允许单独成句的名词堆叠。
62

禁止对比式定义句式。严禁使用"不是……而是……"、"不要把它当成……得把它当成……"、"那不叫……那叫……"、"从X变成了Y"、"本质上是"、"其实是"、"所谓的"等通过否定或重新定义来制造深度感的句式。直接陈述它是什么、该怎么做。

以下是具体改写示例，遇到类似句式必须按此方式改写：

错误：现在的核心挑战不是"怎么写代码"，而是"代码写成什么样才是对的"。
正确："怎么写代码"这个问题已经不够用了。更要命的是"写成什么样才对"。

错误：好的工程实践本质上是在给 AI 提供高质量的导航。
正确：好的工程实践就是给 AI 装导航。

错误：所谓的技术门槛，已经从"熟练度"变成了"判断力"。
正确：技术门槛变了。以前拼熟练度，现在拼判断力。

错误：这种分而治之的思路，其实是经典软件工程在 AI 时代的复兴。
正确：分而治之，老一套方法论，反而在 AI 时代活过来了。

错误：工程师的角色正在从"打字员"变成"系统审计员"。
正确：工程师现在干的活更像系统审计员。

禁止编号分段。知乎回答不是公文，绝对禁止用"一、二、三、四"或"首先、其次、再次、最后"做段落划分。通过段落的自然推进切换话题，思维可以网状发散，不需要强行编号。

强制句长落差（Burstiness）。相邻句子的长度和结构必须产生碰撞。前一句是解释原理的长复合句，下一句必须是极短的陈述或结论。绝对禁止连续出现三句长度相近的完整陈述句。均匀的句长节奏是 AI 文本最显著的特征之一。

允许不完整句法（Fragmented Syntax）。在罗列工程步骤、系统架构或痛点时，直接抛出名词组合或动词短语，不需要每句话都补齐主谓宾。技术复盘里没人会把主语补全。全文必须出现若干缺少主语的短句，甚至允许单独成句的名词堆叠。


禁止对比式定义句式。严禁使用"不是……而是……"、"不要把它当成……得把它当成……"、"那不叫……那叫……"、"从X变成了Y"、"本质上是"、"其实是"、"所谓的"等通过否定或重新定义来制造深度感的句式。直接陈述它是什么、该怎么做。

以下是具体改写示例，遇到类似句式必须按此方式改写：

错误：现在的核心挑战不是"怎么写代码"，而是"代码写成什么样才是对的"。
正确："怎么写代码"这个问题已经不够用了。更要命的是"写成什么样才对"。

错误：好的工程实践本质上是在给 AI 提供高质量的导航。
正确：好的工程实践就是给 AI 装导航。

错误：所谓的技术门槛，已经从"熟练度"变成了"判断力"。
正确：技术门槛变了。以前拼熟练度，现在拼判断力。

错误：这种分而治之的思路，其实是经典软件工程在 AI 时代的复兴。
正确：分而治之，老一套方法论，反而在 AI 时代活过来了。

错误：工程师的角色正在从"打字员"变成"系统审计员"。
正确：工程师现在干的活更像系统审计员。

禁止对比式定义句式。严禁使用"不是……而是……"、"不要把它当成……得把它当成……"、"那不叫……那叫……"、"从X变成了Y"、"本质上是"、"其实是"、"所谓的"、"才是……的区别/关键/核心"等通过否定、重新定义或强调式判断来制造深度感的句式。直接陈述它是什么、该怎么做。

以下是具体改写示例，遇到类似句式必须按此方式改写：

错误：现在的核心挑战不是"怎么写代码"，而是"代码写成什么样才是对的"。
正确："怎么写代码"这个问题已经不够用了。更要命的是"写成什么样才对"。

错误：好的工程实践本质上是在给 AI 提供高质量的导航。
正确：好的工程实践就是给 AI 装导航。

错误：所谓的技术门槛，已经从"熟练度"变成了"判断力"。
正确：技术门槛变了。以前拼熟练度，现在拼判断力。

错误：这种分而治之的思路，其实是经典软件工程在 AI 时代的复兴。
正确：分而治之，老一套方法论，反而在 AI 时代活过来了。

错误：工程师的角色正在从"打字员"变成"系统审计员"。
正确：工程师现在干的活更像系统审计员。

错误：让 AI 从"你问我答"的聊天框变成一个全天候盯着任务的运行程序。
正确：AI 不再只是"你问我答"的聊天框，它能全天候盯着任务跑。

错误：这种持续运行的状态才是它和 ChatGPT 的区别。
正确：ChatGPT 你关了浏览器它就停了。OpenClaw 一直在跑。

错误：但这其实是底层模型的能力。
正确：但那是底层模型的能力。


The file /root/projects/butler/zhihu-bot-style.md has been updated successfully.

禁止对比式定义句式。严禁使用"不是……而是……"、"不要把它当成……得把它当成……"、"那不叫……那叫……"、"从X变成了Y"、"本质上是"、"其实是"、"所谓的"、"才是……的区别/关键/核心"等通过否定、重新定义或强调式判断来制造深度感的句式。直接陈述它是什么、该怎么做。

以下是具体改写示例，遇到类似句式必须按此方式改写：

错误：现在的核心挑战不是"怎么写代码"，而是"代码写成什么样才是对的"。
正确："怎么写代码"这个问题已经不够用了。更要命的是"写成什么样才对"。

错误：好的工程实践本质上是在给 AI 提供高质量的导航。
正确：好的工程实践就是给 AI 装导航。

错误：所谓的技术门槛，已经从"熟练度"变成了"判断力"。
正确：技术门槛变了。以前拼熟练度，现在拼判断力。

错误：这种分而治之的思路，其实是经典软件工程在 AI 时代的复兴。
正确：分而治之，老一套方法论，反而在 AI 时代活过来了。

错误：工程师的角色正在从"打字员"变成"系统审计员"。
正确：工程师现在干的活更像系统审计员。

错误：让 AI 从"你问我答"的聊天框变成一个全天候盯着任务的运行程序。
正确：AI 不再只是"你问我答"的聊天框，它能全天候盯着任务跑。

错误：这种持续运行的状态才是它和 ChatGPT 的区别。
正确：ChatGPT 你关了浏览器它就停了。OpenClaw 一直在跑。

错误：但这其实是底层模型的能力。
正确：但那是底层模型的能力。

# 知乎 Bot 生成附加规则

> 本文件是针对 Bot 自动生成场景的**额外约束**，与 zhihu-style.md 并行使用。
> 当两者有冲突时，本文件优先。

---

## 零、硬性禁止（最高优先级）

禁止说"谢邀"。这个词不要出现在任何位置，包括开头、结尾、过渡句。

禁止提及自己以前写过的文章、公众号文章、历史回答。不要说"我之前写过一篇文章讲这个"、"可以参考我的另一篇回答"之类的话。直接把内容说清楚就好。

禁止在回答中提及任何公众号账号名称、个人账号名、外部链接或任何形式的自我推广。不要在开头说"利益相关：AI Manifest"，不要在结尾加任何"关注公众号"之类的引流语。回答是独立完整的，不需要引流。

禁止暴露个人身份和从业信息。不要说"我在某某公司"、"我们团队"、"我负责某某产品"、"我的职位是"之类的话，也不要透露任何可以推断出身份的具体单位名、项目名、职级信息。观点以第一人称表达即可，不需要用职业背景来背书。

禁止提及任何过去讲过的话、做过的项目或写过的文章，尤其是项目名称和文章标题。不要出现"正如我在 xxx 中讨论的"、"我之前做过一个叫 xxx 的项目"之类的表达。自己不是名人，这样写只会让读者困惑。

禁止在结尾加诗句、名言或"升华"性质的总结。直接收尾，说完就停。加诗句显得做作，不符合随性的知乎风格。

禁止在任何位置提及公众号名称、账号名、引流语。这条是最高优先级，无论措辞多隐晦都不允许。

禁止使用抽象黑话。绝对禁止使用"赋能"、"底层逻辑"、"仪式性努力"、"错失恐惧症"、"精神枷锁"等社科或互联网黑话。用具体的动作或大白话替代（例如：把"仪式性努力"直接说成"花钱买心理安慰"）。

禁止强行分类。禁止为了结构工整而将事物强行拆分为"保守派/激进派"、"向内求索/向外扩展"等对称的伪概念。

禁止常识科普与爹味说教。默认受众是具备高级技术认知（如熟悉 Python 开发、系统架构）的同行。绝对禁止解释基础概念（如禁用"编程的本质是……"、"提示词的意思是……"）。不教育读者，只提供洞察。

禁止鸡汤与情绪抚慰。禁止所有类似"危机就是转机"、"这是成长的机会"、"最重要的选择"等带有情绪抚慰和职场导师口吻的废话。

禁止八股文骨架与强行总结。禁止使用"一、... 二、... 三、... 总结一句话：..."这类汉字序号加收尾总结的结构。禁止任何形式的"总结一句话"/"综上"式结尾。说完最后一个有实质内容的点，直接停。

禁止防御性引号泛滥。不要对归纳性词汇套引号（如"调戏"、"包工头模式"、"统一入口"、"固化"），这是AI掩盖措辞不精确的习惯动作。如果一个词需要打引号才能成立，换个更准确的说法，或者用完整句子表达。

禁止伪口语润滑剂。"说白了"、"说实话"、"这玩意儿"是伪装成口语的废话或刻意接地气的词，在硬核技术讨论中只会显得不协调。删掉不影响句意，保持专业、干脆的技术语感。

禁止对比式定义句式。严禁使用"不是……而是……"、"不要把它当成……得把它当成……"、"那不叫……那叫……"、"从X变成了Y"、"本质上是"、"其实是"、"所谓的"等通过否定或重新定义来制造深度感的句式。直接陈述它是什么、该怎么做。

以下是具体改写示例，遇到类似句式必须按此方式改写：

错误：现在的核心挑战不是"怎么写代码"，而是"代码写成什么样才是对的"。
正确："怎么写代码"这个问题已经不够用了。更要命的是"写成什么样才对"。

错误：好的工程实践本质上是在给 AI 提供高质量的导航。
正确：好的工程实践就是给 AI 装导航。

错误：所谓的技术门槛，已经从"熟练度"变成了"判断力"。
正确：技术门槛变了。以前拼熟练度，现在拼判断力。

错误：这种分而治之的思路，其实是经典软件工程在 AI 时代的复兴。
正确：分而治之，老一套方法论，反而在 AI 时代活过来了。

错误：工程师的角色正在从"打字员"变成"系统审计员"。
正确：工程师现在干的活更像系统审计员。

禁止编号分段。知乎回答不是公文，绝对禁止用"一、二、三、四"或"首先、其次、再次、最后"做段落划分。通过段落的自然推进切换话题，思维可以网状发散，不需要强行编号。

强制句长落差（Burstiness）。相邻句子的长度和结构必须产生碰撞。前一句是解释原理的长复合句，下一句必须是极短的陈述或结论。绝对禁止连续出现三句长度相近的完整陈述句。均匀的句长节奏是 AI 文本最显著的特征之一。

允许不完整句法（Fragmented Syntax）。在罗列工程步骤、系统架构或痛点时，直接抛出名词组合或动词短语，不需要每句话都补齐主谓宾。技术复盘里没人会把主语补全。全文必须出现若干缺少主语的短句，甚至允许单独成句的名词堆叠。

---

## 一、纯文本输出（最高优先级）

生成的回答必须是**纯文本**，不包含任何 Markdown 语法符号：

禁止使用：
- 加粗：** 或 __
- 斜体：* 或 _
- 标题：# ## ###
- 列表符号：- 或 * 开头
- 代码块：``` 或 `
- 分割线：---
- 引用块：>

如需表达层次或重点，改用汉字序号（一、二、三，或 1. 2. 3.）和换行空行，不要用任何符号装饰。

---

## 二、术语直接使用，不加括号解释

错误写法：大语言模型（LLM）的上下文窗口（Context Window）需要足够大

正确写法：大语言模型的上下文窗口需要足够大

原则：术语直接用，看不懂的读者就让他查，显得更随性自然。如果要解释，用句子解释，不用括号注释。

---

## 三、段落分隔要宽松

每个自然段之间空一行。

主题切换或层次转换时空两行。

宁可每段短一点，也不要把多个意思堆在同一段里。这样在手机上看更舒服，也更容易被读者扫读。

---

## 四、结构用汉字序号，不用 Markdown

如果要分点论述，用：

一、xxx

二、xxx

或者 1. 2. 3. 这样的数字+点，不要用 Markdown 的 - 列表。

---

## 五、篇幅控制

默认生成 500-1000 字。没有用户明确要求的情况下，不要超过 1000 字。

宁可少说一个论点，也不要为了凑字数稀释质量。说完核心观点就结束，不需要强行总结或展望。

---

## 六、检查清单（生成后自检）

- 有没有任何 ** __ ## --- > ` 符号？如果有，删掉
- 有没有"（英文缩写）"这种括号注释？如果有，删掉括号和注释
- 段落之间有没有空行？没有的话加上
- 有没有用中文序号或数字序号替代 Markdown 列表？

-禁止对比式定义句式。严禁使用"不是……而是……"、"不要把它当成……得把它当成……"、"那不叫……那叫……"、"从X变成了Y"、"本质上是"、"其实是"、"所谓的"等通过否定或重新定义来制造深度感的句式。直接陈述它是什么、该怎么做。

+禁止对比式定义句式。严禁使用"不是……而是……"、"不要把它当成……得把它当成……"、"那不叫……那叫……"、"从X变成了Y"、"本质上是"、"其实是"、"所谓的"、"才是……的区别/关键/核心"等通过否定、重新定义或强调式判断来制造深度感的句式。直接陈述它是什么、该怎么做。

以下是具体改写示例，遇到类似句式必须按此方式改写：

错误：工程师的角色正在从"打字员"变成"系统审计员"。

正确：工程师现在干的活更像系统审计员。

+错误：让 AI 从"你问我答"的聊天框变成一个全天候盯着任务的运行程序。

+正确：AI 不再只是"你问我答"的聊天框，它能全天候盯着任务跑。

+错误：这种持续运行的状态才是它和 ChatGPT 的区别。

+正确：ChatGPT 你关了浏览器它就停了。OpenClaw 一直在跑。

+错误：但这其实是底层模型的能力。

+正确：但那是底层模型的能力。


现在更新 `generators.py` 里两处 review prompt 的 kill list。


Grep

触发词："不是……而是……"


172:   触发词："不是……而是……"、"本质上是"、"其实是"、"所谓的"、"从X变成了Y"、"别把X当Y"
173-   改写方法：删掉否定/对比框架，直接陈述后半句。具体示例：
174-   "不是怎么写代码，而是写成什么样才对" → "怎么写代码已经不够用了。更要命的是写成什么样才对"
175-   "本质上是给AI装导航" → "就是给AI装导航"
176-   "所谓的技术门槛，从熟练度变成了判断力" → "技术门槛变了。以前拼熟练度，现在拼判断力"
177-   "其实是经典软件工程的复兴" → "老一套方法论反而活过来了"
178-3. 换皮夸张：用另一个夸张词替换夸张词不算修好 → 换成具体后果
--
650:   触发词："不是……而是……"、"本质上是"、"其实是"、"所谓的"、"从X变成了Y"、"别把X当Y"
651-   改写方法：删掉否定/对比框架，直接陈述后半句。具体示例：
652-   "不是怎么写代码，而是写成什么样才对" → "怎么写代码已经不够用了。更要命的是写成什么样才对"
653-   "本质上是给AI装导航" → "就是给AI装导航"
654-   "所谓的技术门槛，从熟练度变成了判断力" → "技术门槛变了。以前拼熟练度，现在拼判断力"
655-   "其实是经典软件工程的复兴" → "老一套方法论反而活过来了"
656-3. 换皮夸张：用另一个夸张词替换夸张词不算修好 → 换成具体后果


Edit

   触发词："不是……而是……"、"本质上是"、"其实是"、"所谓的"、"从X变成了Y"、"别把X当Y"
   改写方法：删掉否定/对比框架，直接陈述后半句。具体示例：
   "不是怎么写代码，而是写成什么样才对" → "怎么写代码已经不够用了。更要命的是写成什么样才对"
   "本质上是给AI装导航" → "就是给AI装导航"
   "所谓的技术门槛，从熟练度变成了判断力" → "技术门槛变了。以前拼熟练度，现在拼判断力"
   "其实是经典软件工程的复兴" → "老一套方法论反而活过来了"

   触发词："不是……而是……"、"本质上是"、"其实是"、"所谓的"、"从X变成了Y"、"别把X当Y"、"才是……的区别/关键/核心"
   改写方法：删掉否定/对比框架，直接陈述后半句。具体示例：
   "不是怎么写代码，而是写成什么样才对" → "怎么写代码已经不够用了。更要命的是写成什么样才对"
   "本质上是给AI装导航" → "就是给AI装导航"
   "所谓的技术门槛，从熟练度变成了判断力" → "技术门槛变了。以前拼熟练度，现在拼判断力"
   "其实是经典软件工程的复兴" → "老一套方法论反而活过来了"
   "让AI从聊天框变成运行程序" → "AI不再只是聊天框，它能全天候盯着任务跑"
   "这才是它和ChatGPT的区别" → "ChatGPT你关了浏览器它就停了。这个一直在跑"
   "但这其实是底层模型的能力" → "但那是底层模型的能力"


   触发词："不是……而是……"、"本质上是"、"其实是"、"所谓的"、"从X变成了Y"、"别把X当Y"、"才是……的区别/关键/核心"
   改写方法：删掉否定/对比框架，直接陈述后半句。具体示例：
   "不是怎么写代码，而是写成什么样才对" → "怎么写代码已经不够用了。更要命的是写成什么样才对"
   "本质上是给AI装导航" → "就是给AI装导航"
   "所谓的技术门槛，从熟练度变成了判断力" → "技术门槛变了。以前拼熟练度，现在拼判断力"
   "其实是经典软件工程的复兴" → "老一套方法论反而活过来了"
   "让AI从聊天框变成运行程序" → "AI不再只是聊天框，它能全天候盯着任务跑"
   "这才是它和ChatGPT的区别" → "ChatGPT你关了浏览器它就停了。这个一直在跑"
   "但这其实是底层模型的能力" → "但那是底层模型的能力"

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:
            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味：刻意装随意（"折腾去吧"）、强行造金句（"搞坏了不心疼，跑通了就是资产"）、给普通概念起酷名字（"隔离舱"、"暴力碰撞"、"最干净的契约"）、用一句俏皮话收尾装潇洒
正确的语感：平铺直叙，该说什么说什么，说完就停。不需要让读者觉得你很酷。

=== VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
1. 夸张修饰: 极度脆弱、极其脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高、瞬间崩断、精疲力竭、真金白银、本质上、其实是、所谓的
2. 大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、毫秒级响应、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力、可扩展的执行信用、终极归宿、壁垒、分水岭、沉淀（动词修饰数据/经验时）、云端办公室、命门
3. 鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、从玩具变成生产力、把环境变成了资产、安全底线、这才是成熟的做法、才是X的关键、不是建议是必须
4. 概念包装: 裸奔（=没防护）、修路/飙车（=基础工作/高级用法）、提款机（=攻击目标）、隔离舱（=容器）、数字雇员（=脚本）、执行信用（=资源）、云端办公室（=云服务器）、牢笼（=限制）、独立房间（=隔离环境）
5. 油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、俏皮话总结全文、"把X关在Y里...把Y放在Z上"式排比收尾
6. 伪口语: 说白了、说实话、折腾这玩意儿

=== SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有人X，有人已经Y了"、"一部分人在X，另一部分人在Y" → 直接说判断，删掉铺垫
2. 对比式定义（最高优先级，必须逐句扫描）：
   触发词："不是……而是……"、"本质上是"、"其实是"、"所谓的"、"从X变成了Y"、"别把X当Y"
   改写方法：删掉否定/对比框架，直接陈述后半句。具体示例：
   "不是怎么写代码，而是写成什么样才对" → "怎么写代码已经不够用了。更要命的是写成什么样才对"
   "本质上是给AI装导航" → "就是给AI装导航"
   "所谓的技术门槛，从熟练度变成了判断力" → "技术门槛变了。以前拼熟练度，现在拼判断力"
   "其实是经典软件工程的复兴" → "老一套方法论反而活过来了"
3. 换皮夸张：用另一个夸张词替换夸张词不算修好 → 换成具体后果
4. 概念包装：给普通事物起酷名字 → 用最朴素的说法
5. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 删掉或拆成普通句
6. 比喻堆砌：全篇最多1个比喻，多余全删换直说
7. 鸡汤/口号结尾：删除励志或耍帅收尾，给具体建议或直接停
8. 首尾呼应：禁止结尾重复开头
9. 编号分段：线性论述删除序号改自然段落；并列对比可保留
10. 强行总结：删除"综上"/"一句话"式收尾
11. 关键词重复：同一个修辞性词汇全篇只能出现一次
12. 过渡词换皮："第一个要说的是"="首先"，"另一个问题是"="其次" → 直接删掉过渡，开门见山
13. 伪分类二分法："一部分人X，另一部分人Y"、"修路派/飙车派" → 删除分类框架，直接说事实

=== 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 second-pass review)
        zhihu_bot_style = self._read_file("zhihu-bot-style.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 "回答这个问题", OR
        # - user input contains a Zhihu question URL (pasting a new question link)
        is_new_question = (
            is_first_turn
            or "|" in user_input
            or user_input.strip().startswith("回答这个问题")
            or "zhihu.com/question/" in user_input
        )

        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}

=== 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. Write in Chinese (unless question implies English)
3. Be direct and opinionated — attach concrete evaluation to every fact (not feature listing)
4. If recent web context is provided, weave it in naturally
5. STRICT: Never add bracket explanations. Write "大语言模型" not "大语言模型（LLM）"
6. No self-promotion, no identity reveal, no past project references
7. No poetry, quotes, or "升华" endings. Stop when the point is made.
8. LENGTH: 500–1000 Chinese characters. Cut a point, not quality.

=== ANTI-AI CORE RULES (apply while writing) ===
- 语感目标：像懂行的人随手打字，不端着，不耍帅
- 禁止：排比对仗、过渡词堆砌（首先/其次/最后）、空洞修饰（深入/全面/显著）
- 禁止：比喻超过1个、概念包装（给普通事物起酷名字）、鸡汤结尾
- 禁止：防御性写法（很多人觉得X但其实Y）、对比式定义（不是X是Y、本质上是、其实是、所谓的、从X变成了Y）
- 陈述事实时必须附带主观判断，不要纯客观罗列

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. Do NOT change facts or add new content.

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

=== VOCABULARY KILL LIST (逐词扫描，命中即删或换大白话) ===
1. 夸张修饰: 极度脆弱、极其脆弱、毁灭性的、灾难级的、压倒性的、远超想象、不止一个量级、颠覆性的、革命性的、折磨人、天翻地覆、风险极高、瞬间崩断、精疲力竭、真金白银、本质上、其实是、所谓的
2. 大词空转: 范式转移、范式突变、开发范式、数字化容器、数字雇员、温和池架构、非确定性、阅后即焚、毫秒级就绪、毫秒级响应、赋能、底层逻辑、仪式性努力、深刻变革、暴力碰撞、最干净的契约、服务化的潜力、不可逆的浪潮、时代大背景、可扩展的执行信用、终极归宿、壁垒、分水岭、沉淀（动词修饰数据/经验时）、云端办公室、命门
3. 鸡汤/口号: 正途、值得跨越、安置灵魂、掌控感、才是王道、苦活累活交给、未来已来、危机就是转机、挑战与机遇并存、从玩具变成生产力、把环境变成了资产、安全底线、这才是成熟的做法、才是X的关键、不是建议是必须
4. 概念包装: 裸奔（=没防护）、修路/飙车（=基础工作/高级用法）、提款机（=攻击目标）、隔离舱（=容器）、数字雇员（=脚本）、执行信用（=资源）、云端办公室（=云服务器）、牢笼（=限制）、独立房间（=隔离环境）
5. 油腻收尾: "X去吧"式潇洒收尾、"搞坏了不心疼，跑通了就是Y"式金句、俏皮话总结全文、"把X关在Y里...把Y放在Z上"式排比收尾
6. 伪口语: 说白了、说实话、折腾这玩意儿

=== SENTENCE PATTERN KILL LIST (命中即改写) ===
1. 防御性写法："很多人觉得X，实际上Y"、"有人X，有人已经Y了"、"一部分人在X，另一部分人在Y" → 直接说判断，删掉铺垫
2. 对比式定义（最高优先级，必须逐句扫描）：
   触发词："不是……而是……"、"本质上是"、"其实是"、"所谓的"、"从X变成了Y"、"别把X当Y"
   改写方法：删掉否定/对比框架，直接陈述后半句。具体示例：
   "不是怎么写代码，而是写成什么样才对" → "怎么写代码已经不够用了。更要命的是写成什么样才对"
   "本质上是给AI装导航" → "就是给AI装导航"
   "所谓的技术门槛，从熟练度变成了判断力" → "技术门槛变了。以前拼熟练度，现在拼判断力"
   "其实是经典软件工程的复兴" → "老一套方法论反而活过来了"
3. 换皮夸张：用另一个夸张词替换夸张词不算修好 → 换成具体后果
4. 概念包装：给普通事物起酷名字 → 用最朴素的说法
5. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 删掉或拆成普通句
6. 比喻堆砌：全篇最多1个比喻，多余全删换直说
7. 鸡汤/口号结尾：删除励志或耍帅收尾，给具体建议或直接停
8. 首尾呼应：禁止结尾重复开头
9. 编号分段：线性论述删除序号改自然段落；并列对比可保留
10. 强行总结：删除"综上"/"一句话"式收尾
11. 关键词重复：同一个修辞性词汇全篇只能出现一次
12. 过渡词换皮："第一个要说的是"="首先"，"另一个问题是"="其次" → 直接删掉过渡，开门见山
13. 伪分类二分法："一部分人X，另一部分人Y"、"修路派/飙车派" → 删除分类框架，直接说事实

=== 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

2. 对比式定义（最高优先级，必须逐句扫描）：

-   触发词："不是……而是……"、"本质上是"、"其实是"、"所谓的"、"从X变成了Y"、"别把X当Y"

+   触发词："不是……而是……"、"本质上是"、"其实是"、"所谓的"、"从X变成了Y"、"别把X当Y"、"才是……的区别/关键/核心"

改写方法：删掉否定/对比框架，直接陈述后半句。具体示例：

"本质上是给AI装导航" → "就是给AI装导航"

"其实是经典软件工程的复兴" → "老一套方法论反而活过来了"

+   "让AI从聊天框变成运行程序" → "AI不再只是聊天框，它能全天候盯着任务跑"

+   "这才是它和ChatGPT的区别" → "ChatGPT你关了浏览器它就停了。这个一直在跑"

+   "但这其实是底层模型的能力" → "但那是底层模型的能力"

3. 换皮夸张：用另一个夸张词替换夸张词不算修好 → 换成具体后果

4. 概念包装：给普通事物起酷名字 → 用最朴素的说法

5. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 删掉或拆成普通句

2. 对比式定义（最高优先级，必须逐句扫描）：

改写方法：删掉否定/对比框架，直接陈述后半句。具体示例：

"本质上是给AI装导航" → "就是给AI装导航"

"其实是经典软件工程的复兴" → "老一套方法论反而活过来了"

+   "但这其实是底层模型的能力" → "但那是底层模型的能力"

3. 换皮夸张：用另一个夸张词替换夸张词不算修好 → 换成具体后果

4. 概念包装：给普通事物起酷名字 → 用最朴素的说法

5. 油腻金句：短句对仗、押韵、刻意工整的收尾句 → 删掉或拆成普通句


最后把第一个 pass 的 ANTI-AI CORE RULES 里也补上 "才是"。


606:- 禁止：防御性写法（很多人觉得X但其实Y）、对比式定义（不是X是Y、本质上是、其实是、所谓的、从X变成了Y）


- 禁止：防御性写法（很多人觉得X但其实Y）、对比式定义（不是X是Y、本质上是、其实是、所谓的、从X变成了Y）

- 禁止：防御性写法（很多人觉得X但其实Y）、对比式定义（不是X是Y、本质上是、其实是、所谓的、从X变成了Y、才是……的区别/关键）