   548→- Does it sound like a real human wrote it, not a machine?
   549→
   550→Begin writing the answer:
   551→"""
   552→
   553→            logger.info(f"Generating Zhihu answer for: {question[:50]}...")
   554→            actual_user_message = prompt
   555→
   556→        else:
   557→            # === Follow-up Turn: User gives refinement instructions ===
   558→            logger.info(f"Follow-up Zhihu instruction: {user_input}")
   559→            actual_user_message = user_input
   560→            prompt = user_input
   561→
   562→        # Call LLM — first pass: content generation
   563→        draft, _ = self.llm.generate_response(prompt, history)
   564→
   565→        # === Second pass: rule enforcement ===
   566→        # Only runs when this turn built a new rich prompt (i.e. the user sent a question,
   567→        # not a follow-up refinement instruction like "make it shorter").

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

- Does it sound like a real human wrote it, not a machine?

Begin writing the answer:
"""

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

        else:
            # === Follow-up Turn: User gives refinement instructions ===
            logger.info(f"Follow-up Zhihu 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: rule enforcement ===
        # Only runs when this turn built a new rich prompt (i.e. the user sent a question,
        # not a follow-up refinement instruction like "make it shorter").


Edit

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

        else:

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

        else:


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

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

        else:

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

        if is_first_turn:
            # === 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.)
            zhihu_bot_style = self._read_file("zhihu-bot-style.md")
            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 and anti-AI-tone guide
            methodology = self._read_file("methodology.md")
            powell_revise_guide = self._read_file("POWELL_REVISE.md")

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

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

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

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

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

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

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

{semantic_context}

=== THE QUESTION ===
{question}

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

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

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

Begin writing the answer:
"""

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

        else:
            # === Follow-up Turn: User gives refinement instructions ===
            logger.info(f"Follow-up Zhihu 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: rule enforcement ===
        # Only runs when this turn built a new rich prompt (i.e. the user sent a question,
        # not a follow-up refinement instruction like "make it shorter").
        # Detect by checking whether we built a review-able draft: always true on first turn,
        # and also triggered when follow-up looks like a new question (contains the question text).
        run_review = is_first_turn
        if run_review:
            review_prompt = f"""You are a strict editor. A draft Zhihu answer has been written. Your only job is to fix rule violations and return the corrected text. Do NOT change the content, facts, or structure unless they directly violate a rule below.

=== RULES TO ENFORCE ===

{zhihu_bot_style}

{powell_revise_guide}

=== SPECIFIC VIOLATIONS TO SCAN AND FIX ===
1. 对比式定义句式：删除或改写所有"不是X而是Y"、"不能当X用得当Y用"、"那不叫X那叫Y"、"别把X当Y要把X当Z"句式。直接陈述即可。
2. 编号分段：删除所有"一、二、三、四"序号标题，改为无标题段落自然过渡。
3. 防御性引号：删除所有套在归纳性词汇上的引号（如"数字替身"、"包工头模式"、"统一入口"、"调教"等）。
4. 伪口语：删除"说白了"、"说实话"、"折腾这玩意儿"。
5. 强行总结：删除任何"一句话建议"、"总结一句话"、"综上"式收尾段。
6. 句长均匀：如果连续三句以上长度相近，拆短其中至少一句，制造落差。
7. 嵌套数字列表：删除正文中"1. 2. 3."的子列表，改为连续段落。

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


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

        # === Second pass: rule enforcement ===
        # Only runs when this turn built a new rich prompt (i.e. the user sent a question,
        # not a follow-up refinement instruction like "make it shorter").
        # Detect by checking whether we built a review-able draft: always true on first turn,
        # and also triggered when follow-up looks like a new question (contains the question text).
        run_review = is_first_turn

        # Call LLM — first pass: content generation
        draft, _ = self.llm.generate_response(prompt, 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


        # Call LLM — first pass: content generation
        draft, _ = self.llm.generate_response(prompt, 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

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

        if is_first_turn:
            # === 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.)
            zhihu_bot_style = self._read_file("zhihu-bot-style.md")
            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 and anti-AI-tone guide
            methodology = self._read_file("methodology.md")
            powell_revise_guide = self._read_file("POWELL_REVISE.md")

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

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

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

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

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

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

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

{semantic_context}

=== THE QUESTION ===
{question}

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

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

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

Begin writing the answer:
"""

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

        else:
            # === Follow-up Turn: User gives refinement instructions ===
            logger.info(f"Follow-up Zhihu 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: rule enforcement ===
        # Only runs when this turn built a new rich prompt (i.e. the user sent a question,
        # not a follow-up refinement instruction like "make it shorter").
        # Detect by checking whether we built a review-able draft: always true on first turn,
        # and also triggered when follow-up looks like a new question (contains the question text).
        run_review = is_first_turn
        if run_review:
            review_prompt = f"""You are a strict editor. A draft Zhihu answer has been written. Your only job is to fix rule violations and return the corrected text. Do NOT change the content, facts, or structure unless they directly violate a rule below.

=== RULES TO ENFORCE ===

{zhihu_bot_style}

{powell_revise_guide}

=== SPECIFIC VIOLATIONS TO SCAN AND FIX ===
1. 对比式定义句式：删除或改写所有"不是X而是Y"、"不能当X用得当Y用"、"那不叫X那叫Y"、"别把X当Y要把X当Z"句式。直接陈述即可。
2. 编号分段：删除所有"一、二、三、四"序号标题，改为无标题段落自然过渡。
3. 防御性引号：删除所有套在归纳性词汇上的引号（如"数字替身"、"包工头模式"、"统一入口"、"调教"等）。
4. 伪口语：删除"说白了"、"说实话"、"折腾这玩意儿"。
5. 强行总结：删除任何"一句话建议"、"总结一句话"、"综上"式收尾段。
6. 句长均匀：如果连续三句以上长度相近，拆短其中至少一句，制造落差。
7. 嵌套数字列表：删除正文中"1. 2. 3."的子列表，改为连续段落。

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

-        # Only runs when this turn built a new rich prompt (i.e. the user sent a question,

-        # not a follow-up refinement instruction like "make it shorter").

-        # Detect by checking whether we built a review-able draft: always true on first turn,

-        # and also triggered when follow-up looks like a new question (contains the question text).

+        # 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").


<tool_use_error>Found 6 matches of the string to replace, but replace_all is false. To replace all occurrences, set replace_all to true. To replace only one occurrence, please provide more context to uniquely identify the instance.
String:         if is_first_turn:</tool_use_error>

Error: Found 6 matches of the string to replace, but replace_all is false. To replace all occurrences, set replace_all to true. To replace only one occurrence, please provide more context to uniquely identify the instance.
String:         if is_first_turn:


   566→        # === Second pass: rule enforcement ===
   567→        # Runs whenever a full draft was generated (built_rich_prompt=True),
   568→        # regardless of whether history was empty or not.
   569→        # Skipped on follow-up refinement turns (e.g. "make it shorter").
   570→        run_review = built_rich_prompt
   571→        if run_review:
   572→            review_prompt = f"""You are a strict editor. A draft Zhihu answer has been written. Your only job is to fix rule violations and return the corrected text. Do NOT change the content, facts, or structure unless they directly violate a rule below.
   573→
   574→=== RULES TO ENFORCE ===
   575→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

        # === 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 fix rule violations and return the corrected text. Do NOT change the content, facts, or structure unless they directly violate a rule below.

=== RULES TO ENFORCE ===


好，`if run_review:` 已经是正确的了（之前那次 edit 生效了）。但 dispatcher 那边还有个问题：每次用 `mode zhihu` 发新问题时，是否真的清空了 history？看日志里 `Follow-up Zhihu instruction` 说明 history 非空。检查 dispatcher 的 history 处理：


   233→
   234→        # Get history
   235→        history = storage.get_context()
   236→        generator = self.generators[current_mode]
   237→
   238→        try:
   239→            logger.info(f"Generating Obsidian response in mode: {current_mode}")
   240→            # The chat method handles first turn logic and returns updated history
   241→            # But wait, our generators' chat method appends to history internally.
   242→            # In our Slack setup, we want to append the result to ContextStorage.
   243→
   244→            # Note: generator.chat(text, history) returns (response, new_history_list)
   245→            # new_history_list contains [...history, {user: rich_prompt}, {assistant: response}]
   246→            response_text, updated_history = generator.chat(text, history)
   247→
   248→            # Since generator.chat already did the LLM call and returned history,
   249→            # we just need to save the ONLY NEW messages to our storage.
   250→            # However, during FIRST TURN, text is replaced by rich_prompt.
   251→
   252→            # For simplicity: completely overwrite storage with updated_history

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

        # Get history
        history = storage.get_context()
        generator = self.generators[current_mode]

        try:
            logger.info(f"Generating Obsidian response in mode: {current_mode}")
            # The chat method handles first turn logic and returns updated history
            # But wait, our generators' chat method appends to history internally.
            # In our Slack setup, we want to append the result to ContextStorage.

            # Note: generator.chat(text, history) returns (response, new_history_list)
            # new_history_list contains [...history, {user: rich_prompt}, {assistant: response}]
            response_text, updated_history = generator.chat(text, history)

            # Since generator.chat already did the LLM call and returned history,
            # we just need to save the ONLY NEW messages to our storage.
            # However, during FIRST TURN, text is replaced by rich_prompt.

            # For simplicity: completely overwrite storage with updated_history


根本原因确认：dispatcher 每次都把 `history = storage.get_context()` 传进去，只要用户没有手动 `clear`，history 永远非空，`is_first_turn` 永远 `False`，`built_rich_prompt` 永远不会被设为 `True`。

但我已经用 `built_rich_prompt` 替代 `is_first_turn` 作为触发条件了，问题在于：只有 `is_first_turn=True` 才进入构建 rich prompt 的 if 块，而 `is_first_turn=False` 时直接走 else（把用户输入当 follow-up），所以 `built_rich_prompt` 还是 `False`。

**真正的修法**：ZhihuGenerator 需要能识别"这是新问题"还是"这是对上一稿的修改指令"。最简单的做法是：**无论 history 是否为空，只要用户输入包含 `|` 分隔符或者明确是一个问题，都当新问题处理**，并自动重置 history。


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

        if is_first_turn:

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

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

        if is_new_question:


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

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

        if is_new_question:

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

        if is_first_turn:
            # === 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.)
            zhihu_bot_style = self._read_file("zhihu-bot-style.md")
            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 and anti-AI-tone guide
            methodology = self._read_file("methodology.md")
            powell_revise_guide = self._read_file("POWELL_REVISE.md")

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

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

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

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

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

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

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

{semantic_context}

=== THE QUESTION ===
{question}

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

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

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

Begin writing the answer:
"""

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

        else:
            # === Follow-up Turn: User gives refinement instructions ===
            logger.info(f"Follow-up Zhihu 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: 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 fix rule violations and return the corrected text. Do NOT change the content, facts, or structure unless they directly violate a rule below.

=== RULES TO ENFORCE ===

{zhihu_bot_style}

{powell_revise_guide}

=== SPECIFIC VIOLATIONS TO SCAN AND FIX ===
1. 对比式定义句式：删除或改写所有"不是X而是Y"、"不能当X用得当Y用"、"那不叫X那叫Y"、"别把X当Y要把X当Z"句式。直接陈述即可。
2. 编号分段：删除所有"一、二、三、四"序号标题，改为无标题段落自然过渡。
3. 防御性引号：删除所有套在归纳性词汇上的引号（如"数字替身"、"包工头模式"、"统一入口"、"调教"等）。
4. 伪口语：删除"说白了"、"说实话"、"折腾这玩意儿"。
5. 强行总结：删除任何"一句话建议"、"总结一句话"、"综上"式收尾段。
6. 句长均匀：如果连续三句以上长度相近，拆短其中至少一句，制造落差。
7. 嵌套数字列表：删除正文中"1. 2. 3."的子列表，改为连续段落。

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

+        # - 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.strip().startswith("回答这个问题")


现在找到 else 分支更新它的条件：


   557→- Is the answer direct and opinionated, not neutral or "balanced"?
   558→- Does it sound like a real human wrote it, not a machine?
   559→
   560→Begin writing the answer:
   561→"""
   562→
   563→            logger.info(f"Generating Zhihu answer for: {question[:50]}...")
   564→            actual_user_message = prompt
   565→            built_rich_prompt = True
   566→
   567→        else:
   568→            # === Follow-up Turn: User gives refinement instructions ===
   569→            logger.info(f"Follow-up Zhihu instruction: {user_input}")
   570→            actual_user_message = user_input
   571→            prompt = user_input

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

- Is the answer direct and opinionated, not neutral or "balanced"?
- Does it sound like a real human wrote it, not a machine?

Begin writing the answer:
"""

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

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


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

        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