Please thoroughly read and analyze all files in the deep_dive module at /root/projects/ai_usecases_explorer/src/ai_usecases_explorer/deep_dive/. I need to understand:

1. The data models (models.py) - what fields exist, how pros/cons/perspectives are structured
2. The analyzer (analyzer.py) - how it extracts argument structure from content
3. The researcher (researcher.py) - how it searches for evidence (what queries, how many results, how it organizes findings)
4. The evaluator (evaluator.py) - how it generates multi-perspective evaluation, what the prompts look like, how credibility/quality is assessed
5. The reporter (reporter.py) - how the final markdown report is structured and what sections it includes
6. The CLI orchestrator (cli.py) - the overall pipeline flow

Please read ALL files completely and report the full content of each file, especially the LLM prompts used in analyzer.py and evaluator.py.


I'll thoroughly read and analyze all files in the deep_dive module for you.


     1→"""Pydantic data models for the Deep Dive skill."""
     2→from __future__ import annotations
     3→
     4→from datetime import datetime
     5→from pathlib import Path
     6→
     7→from pydantic import BaseModel
     8→
     9→
    10→class ArticleContent(BaseModel):
    11→    """Represents the raw content of an article to be analysed."""
    12→
    13→    title: str
    14→    url: str | None
    15→    raw_text: str
    16→
    17→
    18→class SearchQueries(BaseModel):
    19→    """Search queries generated by the structural analyser for web research."""
    20→
    21→    supporting: list[str]
    22→    opposing: list[str]
    23→    expert: list[str]
    24→    fact_checks: list[str]
    25→
    26→
    27→class StructuralAnalysis(BaseModel):
    28→    """Structural breakdown of an article's argument and logic."""
    29→
    30→    main_thesis: str
    31→    sub_claims: list[str]
    32→    logical_structure: str
    33→    key_entities: list[str]
    34→    verifiable_facts: list[str]
    35→    search_queries: SearchQueries
    36→
    37→
    38→class WebSource(BaseModel):
    39→    """A single web search result used as evidence."""
    40→
    41→    title: str
    42→    url: str
    43→    snippet: str
    44→
    45→
    46→class FactCheck(BaseModel):
    47→    """Result of fact-checking a single verifiable claim."""
    48→
    49→    claim: str
    50→    status: str  # "verified" | "disputed" | "unverifiable" | "false"
    51→    evidence: str
    52→    source_url: str | None
    53→
    54→
    55→class ResearchResults(BaseModel):
    56→    """Aggregated web research from all query categories."""
    57→
    58→    supporting: list[WebSource]
    59→    opposing: list[WebSource]
    60→    expert: list[WebSource]
    61→    fact_checks: list[FactCheck]
    62→
    63→
    64→class MultiPerspectiveEvaluation(BaseModel):
    65→    """Multi-perspective evaluation of an article's claims."""
    66→
    67→    pro_arguments: list[str]
    68→    con_arguments: list[str]
    69→    neutral_context: list[str]
    70→    bias_assessment: str
    71→    credibility_score: float  # 0.0–1.0
    72→    overall_verdict: str
    73→    further_exploration: list[str]
    74→
    75→
    76→class DeepDiveReport(BaseModel):
    77→    """Complete deep-dive analysis report."""
    78→
    79→    article: ArticleContent
    80→    structure: StructuralAnalysis
    81→    research: ResearchResults
    82→    evaluation: MultiPerspectiveEvaluation
    83→    generated_at: datetime
    84→    report_path: Path | None = None
    85→

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


     1→"""Step 1 — Structural analyser: extracts argument structure from an article."""
     2→from __future__ import annotations
     3→
     4→import anthropic
     5→
     6→from ai_usecases_explorer.deep_dive.models import ArticleContent, SearchQueries, StructuralAnalysis
     7→from ai_usecases_explorer.processors.utils import parse_llm_json
     8→
     9→_SYSTEM = """\
    10→你是一位批判性思维分析专家。请分析给定文章，提取其论证结构。
    11→
    12→【语言要求】除 search_queries 字段外，所有文本字段必须使用中文输出，无论原文是何种语言。
    13→
    14→请严格以 JSON 格式返回（无 markdown 代码块），包含以下字段：
    15→{
    16→  "main_thesis": "核心主张（中文一句话总结）",
    17→  "sub_claims": ["中文支撑论点1", "中文支撑论点2"],
    18→  "logical_structure": "论证方式（如：因果论证、比较分析、叙事论证、演绎推理）",
    19→  "key_entities": ["关键人物/机构/事件（中文注释）1", "关键人物/机构/事件（中文注释）2"],
    20→  "verifiable_facts": ["中文可查证的具体事实声明1", "中文可查证的具体事实声明2"],
    21→  "search_queries": {
    22→    "supporting": ["英文搜索词（查找支持论点的互联网资料）"],
    23→    "opposing": ["英文搜索词（查找反对或质疑该论点的资料）"],
    24→    "expert": ["英文搜索词（查找权威/学术来源）"],
    25→    "fact_checks": ["英文搜索词（核查具体事实声明）"]
    26→  }
    27→}
    28→
    29→要求：
    30→- main_thesis、sub_claims、key_entities、verifiable_facts、logical_structure 必须全部用中文
    31→- main_thesis 必须是一句话，清晰表达文章的核心主张
    32→- sub_claims 列举 2-5 个支撑论点
    33→- verifiable_facts 只包含可以通过外部来源核实的具体声明（含数字、日期、事件）
    34→- search_queries 保持英文以获得更好的互联网搜索结果，每类最多 2 个搜索词
    35→"""
    36→
    37→
    38→class StructuralAnalyzer:
    39→    """Uses Claude to extract argument structure from an article."""
    40→
    41→    def __init__(self, client: anthropic.Anthropic, model: str) -> None:
    42→        self._client = client
    43→        self._model = model
    44→
    45→    def run(self, article: ArticleContent) -> StructuralAnalysis:
    46→        """Analyse an article and return its structural breakdown.
    47→
    48→        Args:
    49→            article: The article content to analyse.
    50→
    51→        Returns:
    52→            A StructuralAnalysis with thesis, claims, and search queries.
    53→        """
    54→        raw = self._call_llm(article)
    55→        return StructuralAnalysis(
    56→            main_thesis=raw["main_thesis"],
    57→            sub_claims=raw["sub_claims"],
    58→            logical_structure=raw["logical_structure"],
    59→            key_entities=raw["key_entities"],
    60→            verifiable_facts=raw["verifiable_facts"],
    61→            search_queries=SearchQueries(**raw["search_queries"]),
    62→        )
    63→
    64→    def _call_llm(self, article: ArticleContent) -> dict:
    65→        content = f"标题：{article.title}\n\n正文：\n{article.raw_text[:6000]}"
    66→        message = self._client.messages.create(
    67→            model=self._model,
    68→            max_tokens=1024,
    69→            system=_SYSTEM,
    70→            messages=[
    71→                {"role": "user", "content": content},
    72→                {"role": "assistant", "content": "{"},
    73→            ],
    74→        )
    75→        return parse_llm_json("{" + message.content[0].text)
    76→

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


     1→"""Step 2 — Web researcher: multi-perspective Exa.ai search."""
     2→from __future__ import annotations
     3→
     4→import logging
     5→
     6→from exa_py import Exa  # type: ignore[import-untyped]
     7→
     8→from ai_usecases_explorer.deep_dive.models import (
     9→    FactCheck,
    10→    ResearchResults,
    11→    SearchQueries,
    12→    WebSource,
    13→)
    14→
    15→logger = logging.getLogger(__name__)
    16→
    17→_RESULTS_PER_QUERY = 3
    18→_MAX_QUERIES_PER_CATEGORY = 2
    19→_FACT_CHECK_RESULTS = 2
    20→
    21→
    22→class WebResearcher:
    23→    """Performs multi-perspective web research using Exa.ai."""
    24→
    25→    def __init__(self, exa_client: Exa) -> None:
    26→        self._exa = exa_client
    27→
    28→    def run(self, queries: SearchQueries) -> ResearchResults:
    29→        """Execute web searches for all query categories.
    30→
    31→        Args:
    32→            queries: SearchQueries with supporting, opposing, expert, and fact-check terms.
    33→
    34→        Returns:
    35→            ResearchResults with deduplicated sources for each category.
    36→        """
    37→        seen_urls: set[str] = set()
    38→
    39→        supporting = self._search_category(
    40→            queries.supporting[:_MAX_QUERIES_PER_CATEGORY],
    41→            seen_urls,
    42→            num_results=_RESULTS_PER_QUERY,
    43→        )
    44→        opposing = self._search_category(
    45→            queries.opposing[:_MAX_QUERIES_PER_CATEGORY],
    46→            seen_urls,
    47→            num_results=_RESULTS_PER_QUERY,
    48→        )
    49→        expert = self._search_category(
    50→            queries.expert[:_MAX_QUERIES_PER_CATEGORY],
    51→            seen_urls,
    52→            num_results=_RESULTS_PER_QUERY,
    53→            category="research paper",
    54→        )
    55→        fact_checks = self._search_fact_checks(
    56→            queries.fact_checks[:_MAX_QUERIES_PER_CATEGORY],
    57→        )
    58→
    59→        return ResearchResults(
    60→            supporting=supporting,
    61→            opposing=opposing,
    62→            expert=expert,
    63→            fact_checks=fact_checks,
    64→        )
    65→
    66→    def _search_category(
    67→        self,
    68→        queries: list[str],
    69→        seen_urls: set[str],
    70→        num_results: int,
    71→        category: str | None = None,
    72→    ) -> list[WebSource]:
    73→        """Search Exa for a list of queries, deduplicating by URL.
    74→
    75→        Args:
    76→            queries: Search terms to query.
    77→            seen_urls: Mutable set of already-seen URLs for deduplication.
    78→            num_results: Number of results to request per query.
    79→            category: Optional Exa result category filter.
    80→
    81→        Returns:
    82→            List of WebSource objects with unique URLs.
    83→        """
    84→        sources: list[WebSource] = []
    85→        for query in queries:
    86→            try:
    87→                kwargs: dict = {
    88→                    "text": True,
    89→                    "num_results": num_results,
    90→                }
    91→                if category:
    92→                    kwargs["category"] = category
    93→                response = self._exa.search_and_contents(query, **kwargs)
    94→            except Exception as exc:
    95→                logger.warning("Exa search failed for query '%s': %s", query, exc)
    96→                continue
    97→
    98→            for result in response.results:
    99→                url = result.url or ""
   100→                if not url or url in seen_urls:
   101→                    continue
   102→                seen_urls.add(url)
   103→                sources.append(
   104→                    WebSource(
   105→                        title=result.title or url,
   106→                        url=url,
   107→                        snippet=(result.text or "")[:300],
   108→                    )
   109→                )
   110→        return sources
   111→
   112→    def _search_fact_checks(self, queries: list[str]) -> list[FactCheck]:
   113→        """Search Exa for fact-checking evidence on specific claims.
   114→
   115→        Args:
   116→            queries: Search terms targeting specific verifiable facts.
   117→
   118→        Returns:
   119→            List of FactCheck objects (status defaults to "unverifiable" — Claude
   120→            will assess them in the evaluation step).
   121→        """
   122→        fact_checks: list[FactCheck] = []
   123→        seen_claims: set[str] = set()
   124→
   125→        for query in queries:
   126→            if query in seen_claims:
   127→                continue
   128→            seen_claims.add(query)
   129→            try:
   130→                response = self._exa.search_and_contents(
   131→                    query,
   132→                    text=True,
   133→                    num_results=_FACT_CHECK_RESULTS,
   134→                )
   135→            except Exception as exc:
   136→                logger.warning("Exa fact-check search failed for '%s': %s", query, exc)
   137→                continue
   138→
   139→            for result in response.results:
   140→                url = result.url or ""
   141→                snippet = (result.text or "")[:300]
   142→                fact_checks.append(
   143→                    FactCheck(
   144→                        claim=query,
   145→                        status="unverifiable",
   146→                        evidence=snippet,
   147→                        source_url=url or None,
   148→                    )
   149→                )
   150→                break  # One best result per claim
   151→
   152→        return fact_checks
   153→

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


     1→"""Step 3 — Multi-perspective evaluator: synthesises research into a verdict."""
     2→from __future__ import annotations
     3→
     4→import anthropic
     5→
     6→from ai_usecases_explorer.deep_dive.models import (
     7→    MultiPerspectiveEvaluation,
     8→    ResearchResults,
     9→    StructuralAnalysis,
    10→)
    11→from ai_usecases_explorer.processors.utils import parse_llm_json
    12→
    13→_SYSTEM = """\
    14→你是一位多角度批判性评估专家。请基于提供的文章结构分析和网络研究结果，从正方、反方和中性角度综合评价该文章。
    15→
    16→【语言要求】所有输出字段必须使用中文，无论输入材料是何种语言。
    17→
    18→请严格以 JSON 格式返回（无 markdown 代码块），包含以下字段：
    19→{
    20→  "pro_arguments": ["支持该文章核心主张的中文论据1", "中文论据2"],
    21→  "con_arguments": ["质疑或反驳该文章核心主张的中文论据1", "中文论据2"],
    22→  "neutral_context": ["重要的中文背景信息或上下文1", "中文背景2"],
    23→  "bias_assessment": "对文章潜在偏见的中文识别与评估",
    24→  "credibility_score": 0.0到1.0之间的浮点数（基于证据质量、来源多样性、论证严密性）,
    25→  "overall_verdict": "中文综合评估结论（2-3句话）",
    26→  "further_exploration": ["建议进一步探索的中文方向或问题1", "中文方向2"]
    27→}
    28→
    29→评分标准：
    30→- 0.8-1.0：高度可信，证据充分，论证严密
    31→- 0.6-0.8：基本可信，有一定支撑，但存在一些不足
    32→- 0.4-0.6：部分可信，混合证据，需要更多核实
    33→- 0.2-0.4：低可信度，证据薄弱或存在明显偏见
    34→- 0.0-0.2：基本不可信，缺乏证据或含有错误信息
    35→"""
    36→
    37→
    38→class MultiPerspectiveEvaluator:
    39→    """Uses Claude to evaluate an article from multiple perspectives."""
    40→
    41→    def __init__(self, client: anthropic.Anthropic, model: str) -> None:
    42→        self._client = client
    43→        self._model = model
    44→
    45→    def run(
    46→        self, structure: StructuralAnalysis, research: ResearchResults
    47→    ) -> MultiPerspectiveEvaluation:
    48→        """Generate a multi-perspective evaluation.
    49→
    50→        Args:
    51→            structure: The structural analysis of the article.
    52→            research: Web research results from all categories.
    53→
    54→        Returns:
    55→            A MultiPerspectiveEvaluation with arguments, score, and verdict.
    56→        """
    57→        raw = self._call_llm(structure, research)
    58→        return MultiPerspectiveEvaluation(
    59→            pro_arguments=raw["pro_arguments"],
    60→            con_arguments=raw["con_arguments"],
    61→            neutral_context=raw["neutral_context"],
    62→            bias_assessment=raw["bias_assessment"],
    63→            credibility_score=float(raw["credibility_score"]),
    64→            overall_verdict=raw["overall_verdict"],
    65→            further_exploration=raw["further_exploration"],
    66→        )
    67→
    68→    def _call_llm(self, structure: StructuralAnalysis, research: ResearchResults) -> dict:
    69→        structure_summary = (
    70→            f"核心主张：{structure.main_thesis}\n"
    71→            f"主要论点：{'; '.join(structure.sub_claims)}\n"
    72→            f"论证方式：{structure.logical_structure}\n"
    73→            f"关键实体：{', '.join(structure.key_entities)}\n"
    74→            f"可核实事实：{'; '.join(structure.verifiable_facts)}"
    75→        )
    76→
    77→        def _fmt_sources(sources: list) -> str:
    78→            return "\n".join(f"- {s.title}: {s.snippet[:150]}" for s in sources[:4])
    79→
    80→        research_summary = (
    81→            f"支持性来源：\n{_fmt_sources(research.supporting)}\n\n"
    82→            f"反对性来源：\n{_fmt_sources(research.opposing)}\n\n"
    83→            f"权威来源：\n{_fmt_sources(research.expert)}"
    84→        )
    85→
    86→        content = (
    87→            f"=== 文章结构分析 ===\n{structure_summary}\n\n"
    88→            f"=== 网络研究结果 ===\n{research_summary}"
    89→        )[:6000]
    90→
    91→        message = self._client.messages.create(
    92→            model=self._model,
    93→            max_tokens=2048,
    94→            system=_SYSTEM,
    95→            messages=[
    96→                {"role": "user", "content": content},
    97→                {"role": "assistant", "content": "{"},
    98→            ],
    99→        )
   100→        return parse_llm_json("{" + message.content[0].text)
   101→

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


     1→"""Step 4 — Report generator: writes Deep Dive analysis to Obsidian Markdown."""
     2→from __future__ import annotations
     3→
     4→import re
     5→from pathlib import Path
     6→
     7→from ai_usecases_explorer.deep_dive.models import DeepDiveReport
     8→
     9→_STATUS_EMOJI = {
    10→    "verified": "✅",
    11→    "disputed": "⚠",
    12→    "false": "❌",
    13→    "unverifiable": "❓",
    14→}
    15→
    16→
    17→def _slug(text: str, max_len: int = 40) -> str:
    18→    """Convert text to a URL-safe slug."""
    19→    text = text.lower()
    20→    text = re.sub(r"[^\w\s-]", "", text)
    21→    text = re.sub(r"[\s_]+", "-", text.strip())
    22→    return text[:max_len].rstrip("-")
    23→
    24→
    25→class DeepDiveReporter:
    26→    """Writes a Deep Dive analysis report to an Obsidian vault directory."""
    27→
    28→    def __init__(self, report_dir: Path) -> None:
    29→        self._report_dir = report_dir
    30→
    31→    def write(self, report: DeepDiveReport) -> Path:
    32→        """Render the report and write it to disk.
    33→
    34→        Args:
    35→            report: The completed DeepDiveReport.
    36→
    37→        Returns:
    38→            The Path of the written Markdown file.
    39→        """
    40→        self._report_dir.mkdir(parents=True, exist_ok=True)
    41→        timestamp = report.generated_at.strftime("%Y-%m-%d-%H%M")
    42→        slug = _slug(report.article.title)
    43→        filename = f"{timestamp}-{slug}.md"
    44→        path = self._report_dir / filename
    45→        path.write_text(self._render_markdown(report), encoding="utf-8")
    46→        return path
    47→
    48→    def _render_markdown(self, report: DeepDiveReport) -> str:
    49→        """Render the full Markdown report string.
    50→
    51→        Args:
    52→            report: The completed DeepDiveReport.
    53→
    54→        Returns:
    55→            A Markdown-formatted string ready to be written to disk.
    56→        """
    57→        r = report
    58→        st = r.structure
    59→        ev = r.evaluation
    60→        rs = r.research
    61→
    62→        source_label = r.article.url or "手动输入"
    63→        timestamp_str = r.generated_at.strftime("%Y-%m-%d %H:%M")
    64→
    65→        lines: list[str] = []
    66→
    67→        # Header
    68→        lines += [
    69→            f"# 深度探索：{r.article.title}",
    70→            "",
    71→            f"> 分析时间：{timestamp_str} ｜ 来源：{source_label}",
    72→            "",
    73→            "## 核心主张",
    74→            st.main_thesis,
    75→            "",
    76→            "## 论证结构",
    77→            f"**论证方式**：{st.logical_structure}",
    78→            "",
    79→            "### 主要论点",
    80→        ]
    81→        for i, claim in enumerate(st.sub_claims, 1):
    82→            lines.append(f"{i}. {claim}")
    83→        lines += [
    84→            "",
    85→            "### 关键实体",
    86→            "，".join(st.key_entities),
    87→            "",
    88→            "## 互联网多维检索",
    89→            "",
    90→            "### 📗 支持性证据",
    91→        ]
    92→        if rs.supporting:
    93→            for src in rs.supporting:
    94→                lines.append(f"- **{src.title}**：{src.snippet} — {src.url}")
    95→        else:
    96→            lines.append("*未找到相关支持性来源*")
    97→
    98→        lines += ["", "### 📕 反对与质疑"]
    99→        if rs.opposing:
   100→            for src in rs.opposing:
   101→                lines.append(f"- **{src.title}**：{src.snippet} — {src.url}")
   102→        else:
   103→            lines.append("*未找到相关反对性来源*")
   104→
   105→        lines += ["", "### 🎓 权威观点"]
   106→        if rs.expert:
   107→            for src in rs.expert:
   108→                lines.append(f"- **{src.title}**：{src.snippet} — {src.url}")
   109→        else:
   110→            lines.append("*未找到相关权威来源*")
   111→
   112→        # Fact checks table
   113→        lines += ["", "## 事实核查", ""]
   114→        if rs.fact_checks:
   115→            lines += ["| 声明 | 状态 | 依据 | 来源 |", "|------|------|------|------|"]
   116→            for fc in rs.fact_checks:
   117→                emoji = _STATUS_EMOJI.get(fc.status, "❓")
   118→                url_cell = fc.source_url or "-"
   119→                evidence_short = fc.evidence[:80].replace("|", "｜")
   120→                lines.append(
   121→                    f"| {fc.claim} | {emoji} {fc.status} | {evidence_short} | {url_cell} |"
   122→                )
   123→        else:
   124→            lines.append("*未执行事实核查*")
   125→
   126→        # Evaluation
   127→        lines += [
   128→            "",
   129→            "## 多角度评价",
   130→            "",
   131→            "### 正方：支持论点",
   132→        ]
   133→        for arg in ev.pro_arguments:
   134→            lines.append(f"- {arg}")
   135→
   136→        lines += ["", "### 反方：质疑与挑战"]
   137→        for arg in ev.con_arguments:
   138→            lines.append(f"- {arg}")
   139→
   140→        lines += ["", "### 中性背景"]
   141→        for ctx in ev.neutral_context:
   142→            lines.append(f"- {ctx}")
   143→
   144→        # Summary table
   145→        lines += [
   146→            "",
   147→            "## 综合评估",
   148→            "",
   149→            "| 维度 | 评分/结论 |",
   150→            "|------|----------|",
   151→            f"| 可信度 | {ev.credibility_score:.2f} / 1.0 |",
   152→            f"| 偏见识别 | {ev.bias_assessment} |",
   153→            f"| 总体结论 | {ev.overall_verdict} |",
   154→            "",
   155→            "### 延伸探索建议",
   156→        ]
   157→        for item in ev.further_exploration:
   158→            lines.append(f"- {item}")
   159→
   160→        lines += [
   161→            "",
   162→            "---",
   163→            f"*Deep Dive Skill · Exa.ai + Claude Sonnet 4.6 · 生成时间：{timestamp_str}*",
   164→        ]
   165→
   166→        return "\n".join(lines)
   167→

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


     1→"""CLI entry point and orchestrator for the Deep Dive skill."""
     2→from __future__ import annotations
     3→
     4→import argparse
     5→from datetime import UTC, datetime
     6→from pathlib import Path
     7→
     8→import anthropic
     9→from exa_py import Exa  # type: ignore[import-untyped]
    10→
    11→from ai_usecases_explorer.deep_dive.analyzer import StructuralAnalyzer
    12→from ai_usecases_explorer.deep_dive.evaluator import MultiPerspectiveEvaluator
    13→from ai_usecases_explorer.deep_dive.fetcher import ContentFetcher
    14→from ai_usecases_explorer.deep_dive.models import DeepDiveReport
    15→from ai_usecases_explorer.deep_dive.reporter import DeepDiveReporter
    16→from ai_usecases_explorer.deep_dive.researcher import WebResearcher
    17→from ai_usecases_explorer.settings import Settings
    18→
    19→_DEEP_DIVE_MODEL = "claude-sonnet-4-6"
    20→
    21→
    22→class DeepDiveOrchestrator:
    23→    """Orchestrates the four-step deep-dive analysis pipeline."""
    24→
    25→    def __init__(
    26→        self,
    27→        fetcher: ContentFetcher,
    28→        analyzer: StructuralAnalyzer,
    29→        researcher: WebResearcher,
    30→        evaluator: MultiPerspectiveEvaluator,
    31→        reporter: DeepDiveReporter,
    32→    ) -> None:
    33→        self._fetcher = fetcher
    34→        self._analyzer = analyzer
    35→        self._researcher = researcher
    36→        self._evaluator = evaluator
    37→        self._reporter = reporter
    38→
    39→    def run(
    40→        self,
    41→        url: str | None = None,
    42→        text: str | None = None,
    43→        file: Path | None = None,
    44→        dry_run: bool = False,
    45→    ) -> DeepDiveReport:
    46→        """Run the full deep-dive pipeline.
    47→
    48→        Args:
    49→            url: URL of the article to analyse.
    50→            text: Raw article text (alternative to url).
    51→            file: Path to a text file (alternative to url/text).
    52→            dry_run: If True, skip writing the report to disk.
    53→
    54→        Returns:
    55→            A complete DeepDiveReport.
    56→
    57→        Raises:
    58→            ValueError: If none of url, text, or file is provided.
    59→        """
    60→        # Step 0: Acquire content
    61→        if url:
    62→            article = self._fetcher.fetch_url(url)
    63→        elif text:
    64→            article = self._fetcher.from_text(text)
    65→        elif file:
    66→            article = self._fetcher.from_text(file.read_text(encoding="utf-8"), title=file.name)
    67→        else:
    68→            raise ValueError("At least one of --url, --text, or --file must be provided.")
    69→
    70→        # Step 1: Structural analysis
    71→        structure = self._analyzer.run(article)
    72→
    73→        # Step 2: Web research
    74→        research = self._researcher.run(structure.search_queries)
    75→
    76→        # Step 3: Multi-perspective evaluation
    77→        evaluation = self._evaluator.run(structure, research)
    78→
    79→        report = DeepDiveReport(
    80→            article=article,
    81→            structure=structure,
    82→            research=research,
    83→            evaluation=evaluation,
    84→            generated_at=datetime.now(tz=UTC),
    85→        )
    86→
    87→        # Step 4: Write report (unless dry-run)
    88→        if not dry_run:
    89→            report_path = self._reporter.write(report)
    90→            report = report.model_copy(update={"report_path": report_path})
    91→
    92→        return report
    93→
    94→
    95→def _build_orchestrator(settings: Settings) -> DeepDiveOrchestrator:
    96→    """Instantiate all pipeline components from settings."""
    97→    anthropic_kwargs: dict = {"api_key": settings.anthropic_api_key}
    98→    if settings.anthropic_base_url:
    99→        anthropic_kwargs["base_url"] = settings.anthropic_base_url
   100→    client = anthropic.Anthropic(**anthropic_kwargs)
   101→
   102→    exa = Exa(api_key=settings.exa_api_key)
   103→
   104→    return DeepDiveOrchestrator(
   105→        fetcher=ContentFetcher(exa_client=exa),
   106→        analyzer=StructuralAnalyzer(client=client, model=settings.deep_dive_model),
   107→        researcher=WebResearcher(exa_client=exa),
   108→        evaluator=MultiPerspectiveEvaluator(client=client, model=settings.deep_dive_model),
   109→        reporter=DeepDiveReporter(report_dir=settings.deep_dive_report_dir),
   110→    )
   111→
   112→
   113→def main() -> None:
   114→    """Parse arguments and run the deep-dive pipeline."""
   115→    parser = argparse.ArgumentParser(
   116→        description="Deep Dive: multi-perspective article analysis via Exa.ai + Claude."
   117→    )
   118→    group = parser.add_mutually_exclusive_group()
   119→    group.add_argument("--url", help="URL of the article to analyse")
   120→    group.add_argument("--text", help="Raw article text (quoted string)")
   121→    group.add_argument("--file", type=Path, help="Path to a plain-text article file")
   122→    parser.add_argument(
   123→        "--dry-run",
   124→        action="store_true",
   125→        help="Print the report without writing to disk",
   126→    )
   127→    args = parser.parse_args()
   128→
   129→    settings = Settings()
   130→    orchestrator = _build_orchestrator(settings)
   131→
   132→    report = orchestrator.run(
   133→        url=args.url,
   134→        text=args.text,
   135→        file=args.file,
   136→        dry_run=args.dry_run,
   137→    )
   138→
   139→    if args.dry_run:
   140→        from ai_usecases_explorer.deep_dive.reporter import DeepDiveReporter
   141→
   142→        reporter = DeepDiveReporter(report_dir=settings.deep_dive_report_dir)
   143→        print(reporter._render_markdown(report))
   144→    else:
   145→        print(f"REPORT_PATH:{report.report_path}")
   146→        print(f"✅ 报告已保存至 Obsidian：{report.report_path}")
   147→        print(f"   可信度评分：{report.evaluation.credibility_score:.2f} / 1.0")
   148→        print(f"   总体结论：{report.evaluation.overall_verdict}")
   149→
   150→
   151→if __name__ == "__main__":
   152→    main()
   153→

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


     1→"""Fetches article content from a URL or accepts raw text input."""
     2→from __future__ import annotations
     3→
     4→import html.parser
     5→import logging
     6→from typing import TYPE_CHECKING
     7→
     8→import httpx
     9→
    10→from ai_usecases_explorer.deep_dive.models import ArticleContent
    11→
    12→if TYPE_CHECKING:
    13→    from exa_py import Exa  # type: ignore[import-untyped]
    14→
    15→logger = logging.getLogger(__name__)
    16→
    17→_MAX_CHARS = 8000
    18→_BROWSER_HEADERS = {
    19→    "User-Agent": (
    20→        "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
    21→        "(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
    22→    ),
    23→    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    24→    "Accept-Language": "en-US,en;q=0.5",
    25→}
    26→
    27→
    28→class _TextExtractor(html.parser.HTMLParser):
    29→    """Minimal HTML parser that collects visible text, skipping scripts/styles."""
    30→
    31→    _SKIP_TAGS = {"script", "style", "head", "noscript", "nav", "footer", "header"}
    32→
    33→    def __init__(self) -> None:
    34→        super().__init__()
    35→        self._chunks: list[str] = []
    36→        self._skip: int = 0
    37→        self.title: str = ""
    38→        self._in_title: bool = False
    39→
    40→    def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
    41→        if tag in self._SKIP_TAGS:
    42→            self._skip += 1
    43→        if tag == "title":
    44→            self._in_title = True
    45→
    46→    def handle_endtag(self, tag: str) -> None:
    47→        if tag in self._SKIP_TAGS:
    48→            self._skip = max(0, self._skip - 1)
    49→        if tag == "title":
    50→            self._in_title = False
    51→
    52→    def handle_data(self, data: str) -> None:
    53→        text = data.strip()
    54→        if not text:
    55→            return
    56→        if self._in_title and not self.title:
    57→            self.title = text
    58→        elif self._skip == 0:
    59→            self._chunks.append(text)
    60→
    61→    @property
    62→    def text(self) -> str:
    63→        return " ".join(self._chunks)
    64→
    65→
    66→class ContentFetcher:
    67→    """Fetches article content from a URL or wraps raw text.
    68→
    69→    Falls back to Exa.ai's get_contents() for URLs that block direct HTTP access
    70→    (e.g. Cloudflare-protected or paywalled sites).
    71→    """
    72→
    73→    def __init__(self, exa_client: Exa | None = None) -> None:
    74→        self._exa = exa_client
    75→
    76→    def fetch_url(self, url: str) -> ArticleContent:
    77→        """Fetch a web page and extract its text content.
    78→
    79→        Tries a direct httpx GET first; falls back to Exa.ai get_contents() on
    80→        4xx/5xx errors if an Exa client is available.
    81→
    82→        Args:
    83→            url: The URL to fetch.
    84→
    85→        Returns:
    86→            An ArticleContent with extracted title and text.
    87→
    88→        Raises:
    89→            RuntimeError: If both direct fetch and Exa fallback fail.
    90→        """
    91→        try:
    92→            response = httpx.get(
    93→                url, follow_redirects=True, timeout=15, headers=_BROWSER_HEADERS
    94→            )
    95→            response.raise_for_status()
    96→            return self._parse_html(response.text, url)
    97→        except httpx.HTTPStatusError as exc:
    98→            logger.warning(
    99→                "Direct fetch failed (%s) for %s — trying Exa fallback",
   100→                exc.response.status_code,
   101→                url,
   102→            )
   103→            return self._fetch_via_exa(url)
   104→
   105→    def from_text(self, text: str, title: str = "手动输入") -> ArticleContent:
   106→        """Wrap raw text as an ArticleContent without network access.
   107→
   108→        Args:
   109→            text: The article body text.
   110→            title: Optional display title (defaults to "手动输入").
   111→
   112→        Returns:
   113→            An ArticleContent with url=None.
   114→        """
   115→        return ArticleContent(title=title, url=None, raw_text=text[:_MAX_CHARS])
   116→
   117→    # ------------------------------------------------------------------
   118→    # Internal helpers
   119→    # ------------------------------------------------------------------
   120→
   121→    def _parse_html(self, html_text: str, url: str) -> ArticleContent:
   122→        parser = _TextExtractor()
   123→        parser.feed(html_text)
   124→        return ArticleContent(
   125→            title=parser.title or url,
   126→            url=url,
   127→            raw_text=parser.text[:_MAX_CHARS],
   128→        )
   129→
   130→    def _fetch_via_exa(self, url: str) -> ArticleContent:
   131→        if self._exa is None:
   132→            raise RuntimeError(
   133→                f"Direct HTTP fetch failed and no Exa client is available to fetch {url}. "
   134→                "Pass the article text with --text or provide an EXA_API_KEY."
   135→            )
   136→        try:
   137→            result = self._exa.get_contents([url], text=True)
   138→            if not result.results:
   139→                raise RuntimeError(f"Exa returned no content for {url}")
   140→            item = result.results[0]
   141→            return ArticleContent(
   142→                title=item.title or url,
   143→                url=url,
   144→                raw_text=(item.text or "")[:_MAX_CHARS],
   145→            )
   146→        except Exception as exc:
   147→            raise RuntimeError(f"Exa fallback also failed for {url}: {exc}") from exc
   148→

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


Excellent! I've successfully read all seven files in the deep_dive module. Let me provide you with a comprehensive analysis of the entire system.

---

## Deep Dive Module: Complete Analysis

### 1. DATA MODELS (models.py)

The module uses 7 Pydantic data classes for type-safe data flow:

**ArticleContent**
- `title: str` — article headline
- `url: str | None` — source URL (None for manual text input)
- `raw_text: str` — the article body (up to 8000 chars)

**SearchQueries** (generated by Step 1)
- `supporting: list[str]` — 1-2 queries to find evidence supporting the thesis
- `opposing: list[str]` — 1-2 queries to find counterarguments
- `expert: list[str]` — 1-2 queries for academic/authoritative sources
- `fact_checks: list[str]` — 1-2 queries to verify specific factual claims

**StructuralAnalysis** (output of Step 1)
- `main_thesis: str` — core claim (1 sentence, Chinese)
- `sub_claims: list[str]` — 2-5 supporting arguments (Chinese)
- `logical_structure: str` — type of reasoning used (e.g., "因果论证", "比较分析")
- `key_entities: list[str]` — people/organizations/events involved (Chinese)
- `verifiable_facts: list[str]` — specific, factual statements that can be checked (Chinese)
- `search_queries: SearchQueries` — the queries for web research

**WebSource** (research evidence)
- `title: str` — article/page title
- `url: str` — source link
- `snippet: str` — up to 300 chars of extracted text

**FactCheck** (individual fact-check result)
- `claim: str` — the verifiable fact being checked
- `status: str` — "verified" | "disputed" | "unverifiable" | "false"
- `evidence: str` — up to 300 chars supporting the status
- `source_url: str | None` — where the evidence came from

**ResearchResults** (aggregated research from Step 2)
- `supporting: list[WebSource]` — evidence backing the thesis (up to 3 sources × 2 queries = max 6)
- `opposing: list[WebSource]` — counter-evidence
- `expert: list[WebSource]` — research papers/academic sources
- `fact_checks: list[FactCheck]` — verification of specific claims (up to 2 per query × 2 queries = 4 max)

**MultiPerspectiveEvaluation** (output of Step 3)
- `pro_arguments: list[str]` — reasons supporting the article (Chinese)
- `con_arguments: list[str]` — reasons questioning/opposing it (Chinese)
- `neutral_context: list[str]` — important background/context (Chinese)
- `bias_assessment: str` — identified biases in the article (Chinese)
- `credibility_score: float` — 0.0–1.0 score based on evidence quality
- `overall_verdict: str` — 2-3 sentence summary conclusion (Chinese)
- `further_exploration: list[str]` — suggested areas for deeper investigation (Chinese)

**DeepDiveReport** (complete output)
- `article: ArticleContent`
- `structure: StructuralAnalysis`
- `research: ResearchResults`
- `evaluation: MultiPerspectiveEvaluation`
- `generated_at: datetime` — UTC timestamp
- `report_path: Path | None` — where markdown was saved (None for dry-run)

---

### 2. ANALYZER (analyzer.py) - Structural Analysis

**Purpose:** Extract argument structure from raw article text using Claude.

**Model:** Configurable (default: claude-sonnet-4-6)

**Input:** ArticleContent (title + first 6000 chars of raw_text)

**LLM System Prompt (full):**

```
你是一位批判性思维分析专家。请分析给定文章，提取其论证结构。

【语言要求】除 search_queries 字段外，所有文本字段必须使用中文输出，无论原文是何种语言。

请严格以 JSON 格式返回（无 markdown 代码块），包含以下字段：
{
  "main_thesis": "核心主张（中文一句话总结）",
  "sub_claims": ["中文支撑论点1", "中文支撑论点2"],
  "logical_structure": "论证方式（如：因果论证、比较分析、叙事论证、演绎推理）",
  "key_entities": ["关键人物/机构/事件（中文注释）1", "关键人物/机构/事件（中文注释）2"],
  "verifiable_facts": ["中文可查证的具体事实声明1", "中文可查证的具体事实声明2"],
  "search_queries": {
    "supporting": ["英文搜索词（查找支持论点的互联网资料）"],
    "opposing": ["英文搜索词（查找反对或质疑该论点的资料）"],
    "expert": ["英文搜索词（查找权威/学术来源）"],
    "fact_checks": ["英文搜索词（核查具体事实声明）"]
  }
}

要求：
- main_thesis、sub_claims、key_entities、verifiable_facts、logical_structure 必须全部用中文
- main_thesis 必须是一句话，清晰表达文章的核心主张
- sub_claims 列举 2-5 个支撑论点
- verifiable_facts 只包含可以通过外部来源核实的具体声明（含数字、日期、事件）
- search_queries 保持英文以获得更好的互联网搜索结果，每类最多 2 个搜索词
```

**Key Details:**
- Max tokens: 1024
- Uses a clever trick: sends `{"role": "assistant", "content": "{"}` in the system message to make Claude complete JSON (avoids markdown fence wrapper)
- Processes JSON response via `parse_llm_json()` which strips markdown fences if Claude wraps them
- Generates 4 categories of search queries (2 queries each max) for Step 2

---

### 3. RESEARCHER (researcher.py) - Multi-Perspective Web Search

**Purpose:** Execute 4-category web search using Exa.ai to gather evidence.

**Client:** Exa.ai (`exa_py` SDK)

**Search Constants:**
- `_RESULTS_PER_QUERY = 3` — fetch 3 results per query
- `_MAX_QUERIES_PER_CATEGORY = 2` — only use first 2 queries per category
- `_FACT_CHECK_RESULTS = 2` — fetch 2 results for fact-checking

**Pipeline:**

1. **Supporting Evidence** (`_search_category`)
   - Queries: up to 2 from `queries.supporting`
   - Results: 3 per query, deduplicated by URL
   - Max total: 6 unique sources (after dedup across all categories)

2. **Opposing Evidence** (`_search_category`)
   - Queries: up to 2 from `queries.opposing`
   - Results: 3 per query, deduplicated
   - Max total: 6 unique sources

3. **Expert/Research Sources** (`_search_category`)
   - Queries: up to 2 from `queries.expert`
   - Results: 3 per query with `category="research paper"` filter
   - Max total: 6 unique sources

4. **Fact-Check Queries** (`_search_fact_checks`)
   - Queries: up to 2 from `queries.fact_checks`
   - Results: only 1 best result per fact (via `break` after first result)
   - Returns `FactCheck` objects with `status="unverifiable"` (will be assessed later)

**Key Implementation Details:**
- Uses `self._exa.search_and_contents(query, text=True, num_results=N)` to get both metadata and snippet
- Deduplicates across categories using a mutable `seen_urls` set passed through all category searches
- Truncates snippets to 300 chars
- Silently logs and continues on search failures (exception handling)
- For fact checks, breaks after first result per claim

**ResearchResults Structure:**
```python
ResearchResults(
    supporting=[WebSource, ...],      # 0-6 sources
    opposing=[WebSource, ...],         # 0-6 sources
    expert=[WebSource, ...],          # 0-6 sources
    fact_checks=[FactCheck, ...]      # 0-2 fact-checks (one per query)
)
```

---

### 4. EVALUATOR (evaluator.py) - Multi-Perspective Analysis

**Purpose:** Synthesize research into a credibility verdict using Claude.

**Model:** Configurable (default: claude-sonnet-4-6)

**Input:** StructuralAnalysis + ResearchResults (from Steps 1 & 2)

**LLM System Prompt (full):**

```
你是一位多角度批判性评估专家。请基于提供的文章结构分析和网络研究结果，从正方、反方和中性角度综合评价该文章。

【语言要求】所有输出字段必须使用中文，无论输入材料是何种语言。

请严格以 JSON 格式返回（无 markdown 代码块），包含以下字段：
{
  "pro_arguments": ["支持该文章核心主张的中文论据1", "中文论据2"],
  "con_arguments": ["质疑或反驳该文章核心主张的中文论据1", "中文论据2"],
  "neutral_context": ["重要的中文背景信息或上下文1", "中文背景2"],
  "bias_assessment": "对文章潜在偏见的中文识别与评估",
  "credibility_score": 0.0到1.0之间的浮点数（基于证据质量、来源多样性、论证严密性）,
  "overall_verdict": "中文综合评估结论（2-3句话）",
  "further_exploration": ["建议进一步探索的中文方向或问题1", "中文方向2"]
}

评分标准：
- 0.8-1.0：高度可信，证据充分，论证严密
- 0.6-0.8：基本可信，有一定支撑，但存在一些不足
- 0.4-0.6：部分可信，混合证据，需要更多核实
- 0.2-0.4：低可信度，证据薄弱或存在明显偏见
- 0.0-0.2：基本不可信，缺乏证据或含有错误信息
```

**Input Content Assembly:**
```python
structure_summary = f"""
核心主张：{main_thesis}
主要论点：{'; '.join(sub_claims)}
论证方式：{logical_structure}
关键实体：{', '.join(key_entities)}
可核实事实：{'; '.join(verifiable_facts)}
"""

research_summary = f"""
支持性来源：
- title1: snippet[:150]
- title2: snippet[:150]
...（up to 4 sources per category）

反对性来源：
...

权威来源：
...
"""

full_content = (structure_summary + "\n\n" + research_summary)[:6000]
```

**LLM Call Details:**
- Max tokens: 2048
- Sends JSON with leading `"{"` trick (same pattern as analyzer)
- All outputs must be Chinese
- Credibility score is parsed as float

**Output Fields:**
- `pro_arguments` — supporting evidence extracted from research
- `con_arguments` — counter-evidence and limitations
- `neutral_context` — background/contextual information
- `bias_assessment` — identified biases or perspectives in the article
- `credibility_score` — 0.0–1.0, based on:
  - Evidence quality (academic vs. casual sources)
  - Source diversity (supporting vs. opposing count)
  - Logical rigor of arguments
- `overall_verdict` — 2-3 sentence summary
- `further_exploration` — unanswered questions or areas needing deeper study

---

### 5. REPORTER (reporter.py) - Markdown Report Generation

**Purpose:** Render the complete Deep Dive analysis into an Obsidian-compatible Markdown file.

**Output Format:**
- Directory: `{report_dir}/YYYY-MM-DD-HHMM-{slug}.md`
- Slug: lowercase, URL-safe, max 40 chars
- Example: `2026-02-28-1430-ai-agents-reshaping-workplace.md`

**Markdown Structure:**

```markdown
# 深度探索：{article.title}

> 分析时间：YYYY-MM-DD HH:MM ｜ 来源：{url or "手动输入"}

## 核心主张
{main_thesis}

## 论证结构
**论证方式**：{logical_structure}

### 主要论点
1. {sub_claim1}
2. {sub_claim2}
...

### 关键实体
{entity1}, {entity2}, ...

## 互联网多维检索

### 📗 支持性证据
- **{source.title}**：{snippet[:150]} — {url}
...
(or "*未找到相关支持性来源*")

### 📕 反对与质疑
- **{source.title}**：{snippet[:150]} — {url}
...
(or "*未找到相关反对性来源*")

### 🎓 权威观点
- **{source.title}**：{snippet[:150]} — {url}
...
(or "*未找到相关权威来源*")

## 事实核查

| 声明 | 状态 | 依据 | 来源 |
|------|------|------|------|
| {claim} | {emoji} {status} | {evidence[:80]} | {url} |
...
(or "*未执行事实核查*")

## 多角度评价

### 正方：支持论点
- {pro_argument1}
- {pro_argument2}

### 反方：质疑与挑战
- {con_argument1}
- {con_argument2}

### 中性背景
- {context1}
- {context2}

## 综合评估

| 维度 | 评分/结论 |
|------|----------|
| 可信度 | {score:.2f} / 1.0 |
| 偏见识别 | {bias_assessment} |
| 总体结论 | {overall_verdict} |

### 延伸探索建议
- {suggestion1}
- {suggestion2}

---
*Deep Dive Skill · Exa.ai + Claude Sonnet 4.6 · 生成时间：YYYY-MM-DD HH:MM*
```

**Fact-Check Status Emoji Mapping:**
- `"verified"` → ✅
- `"disputed"` → ⚠
- `"false"` → ❌
- `"unverifiable"` → ❓

**Special Handling:**
- Pipe characters in evidence text are replaced with `｜` (full-width) to avoid breaking markdown tables
- Empty sections show fallback messages ("*未找到相关支持性来源*")

---

### 6. ORCHESTRATOR (cli.py) - Pipeline Orchestration

**DeepDiveOrchestrator Class:**

Coordinates the 4-step pipeline:

```python
def run(url, text, file, dry_run):
    # Step 0: Content acquisition
    if url:
        article = fetcher.fetch_url(url)
    elif text:
        article = fetcher.from_text(text)
    elif file:
        article = fetcher.from_text(file.read_text(), title=file.name)
    else:
        raise ValueError(...)

    # Step 1: Structural analysis
    structure = analyzer.run(article)

    # Step 2: Web research
    research = researcher.run(structure.search_queries)

    # Step 3: Multi-perspective evaluation
    evaluation = evaluator.run(structure, research)

    # Create report object
    report = DeepDiveReport(
        article=article,
        structure=structure,
        research=research,
        evaluation=evaluation,
        generated_at=datetime.now(tz=UTC)
    )

    # Step 4: Write report (unless dry-run)
    if not dry_run:
        report_path = reporter.write(report)
        report = report.model_copy(update={"report_path": report_path})

    return report
```

**CLI Arguments:**
```
--url TEXT              # URL to analyze
--text TEXT             # Raw article text (mutually exclusive)
--file PATH             # Path to text file (mutually exclusive)
--dry-run               # Print to stdout without writing to disk
```

**CLI Output:**
- If `--dry-run`: prints markdown to stdout
- Otherwise: prints:
  - `REPORT_PATH:{path}` — parsed by slash command
  - `✅ 报告已保存至 Obsidian：{path}`
  - Credibility score and overall verdict summary

**Component Initialization:**
- Reads `Settings()` from environment (ANTHROPIC_API_KEY, EXA_API_KEY, DEEP_DIVE_MODEL, DEEP_DIVE_REPORT_DIR)
- Creates Anthropic client with proxy support (if ANTHROPIC_BASE_URL set)
- Instantiates all 5 pipeline components (fetcher, analyzer, researcher, evaluator, reporter)
- Default model: `claude-sonnet-4-6` (overridable via DEEP_DIVE_MODEL env var)

---

### 7. FETCHER (fetcher.py) - Content Acquisition

**Purpose:** Fetch article content from URL or wrap raw text.

**Max Content:** 8000 characters

**Dual Fetch Strategy:**

1. **Primary (httpx):**
   - Browser headers (User-Agent, Accept, Accept-Language)
   - 15-second timeout
   - Follows redirects
   - Custom HTML parser extracts visible text (skips script, style, nav, footer, header)
   - Extracts `<title>` tag for headline

2. **Fallback (Exa.ai):**
   - Triggered on 4xx/5xx HTTP errors
   - Uses `exa_client.get_contents([url], text=True)`
   - Handles Cloudflare-protected and paywalled sites

**Methods:**

`fetch_url(url: str) -> ArticleContent`
- Tries httpx.get → _parse_html
- Falls back to Exa.get_contents() on HTTPStatusError
- Raises RuntimeError if both fail

`from_text(text: str, title="手动输入") -> ArticleContent`
- Wraps raw text without network access
- Sets `url=None`
- Useful for clipboard/file input

**HTML Parser (_TextExtractor):**
- Extends `html.parser.HTMLParser`
- Skips tags: script, style, head, noscript, nav, footer, header
- Collects text chunks, joins with spaces
- Extracts title from `<title>` tag if present

---

## Pipeline Flow Summary

```
INPUT (URL/text/file)
    ↓
[Fetcher] → ArticleContent
    ↓
[Analyzer] → StructuralAnalysis (with SearchQueries)
    ↓
[Researcher] → ResearchResults (4 categories, ~20 sources)
    ↓
[Evaluator] → MultiPerspectiveEvaluation (with credibility score)
    ↓
[Reporter] → Markdown file (saved to Obsidian)
    ↓
OUTPUT (DeepDiveReport + filepath)
```

**All outputs are in Chinese (except URL queries in Analyzer's search_queries).**

---

This implementation creates a rigorous, multi-perspective fact-checking and analysis pipeline designed to evaluate articles through:
- Structural argument extraction
- Multi-sourced evidence gathering
- Balanced pro/con/neutral assessment
- Credibility scoring with clear rubric
- Obsidian-formatted final report