# Project export: Fact Check

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: One-click AI fact-checking for tweets, right where misinformation spreads.
- Devpost: https://devpost.com/software/fact-check-b25pcg
- GitHub: https://github.com/taohaoze7-prog/tweet-factchecker
- Video: https://www.youtube.com/embed/_yUd2M3P2Jc?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Claude Opus 4.8 (15 commits), taohaoze7-prog (1 commits)

## Devpost submission (written by the team)

### Overview

About the Project Tweet FactChecker is a one-click fact-checking tool for X/Twitter posts. The idea is simple: when a user sees a suspicious tweet, they can click a “Fact Check” button directly inside the browser. The extension sends the tweet to a backend pipeline, where multiple agents extract claims, evaluate evidence, critique the reasoning, and return a concise verdict in an overlay.

### Inspiration

Social media moves faster than truth. A misleading post can spread widely before most people have time to verify it. We wanted to build something that fits naturally into the place where misinformation appears: the timeline itself. Instead of asking users to copy a tweet, open another tab, search manually, and compare sources, Tweet FactChecker brings the verification workflow directly into the browser. Our goal was to make fact-checking feel lightweight enough that people would actually use it in the moment. What It Does Tweet FactChecker adds a fact-checking layer to X/Twitter: A browser extension detects tweets and injects a fact-check button. When clicked, the extension sends the tweet text to a FastAPI backend. The backend runs a multi-agent pipeline: ClaimAgent extracts checkable claims from the tweet. EvaluatorAgent searches for supporting or contradicting evidence. CriticAgent reviews the reasoning and catches weak conclusions. ClaimAgent extracts checkable claims from the tweet. EvaluatorAgent searches for supporting or contradicting evidence. CriticAgent reviews the reasoning and catches weak conclusions. The system aggregates the results into a final verdict. The extension displays the conclusion in a clean floating overlay. Conceptually, the pipeline turns a tweet into structured reasoning: tweet \rightarrow claims \rightarrow evidence \rightarrow critique \rightarrow verdict How We Built It The project is split into two main parts: extension/: a TypeScript + Vite + Manifest V3 browser extension backend/: a Python 3.12 FastAPI service The extension handles tweet extraction, UI injection, API calls, and result rendering. The backend owns the fact-checking pipeline, shared contracts, mock responses, real-agent wiring, and integration smoke tests. A key design decision was to keep the data contract frozen between frontend and backend. The Pydantic models in backend/contracts/models.py mirror the TypeScript types in extension/src/types.ts, so both sides agree exactly on the shape of requests and responses. We also built the system so it can run in two modes: Mock mode for local development without API keys Real agent mode using USE_REAL_AGENTS=1 for the full multi-agent pipeline This made it easier to develop the UI and backend independently while still preserving an end-to-end path. Challenges We Faced One of the hardest parts was keeping the frontend and backend synchronized. Since the extension depends on exact response shapes, even a small contract mismatch could break the overlay rendering. Freezing and mirroring the contract helped reduce that risk. Another challenge was designing the agent pipeline so it did more than produce a single rushed answer. We wanted the system to separate claim extraction, evidence evaluation, and critique, because fact-checking requires more than just confidence. It requires structured reasoning. We also had to balance real-world usability with development speed. The browser extension needed to feel immediate, while the backend needed enough structure to support mock testing, real agents, and future expansion. What We Learned We learned that fact-checking is not just a search problem. It is a workflow problem. A useful system needs to identify what is actually being claimed, evaluate evidence carefully, and explain the result in a way users can understand quickly. We also learned the value of strict contracts between frontend and backend. By treating the response schema as a shared source of truth, we made the system easier to test, debug, and extend. Most importantly, we learned that AI agents are most useful when they have clear roles. Splitting the pipeline into claim extraction, evaluation, and critique made the final result more reliable than a single all-purpose step. What's Next Next, we want to improve source citation quality, support multi-claim tweets more deeply, add richer verdict explanations, and make the extension more robust across different X/Twitter layouts. We also want to experiment with confidence scoring and user feedback so the system can better communicate uncertainty instead of pretending every answer is absolute.

## README (from the GitHub repository)

# Tweet FactChecker

X/Twitter 推文一键事实核查：浏览器扩展抓推文 → 后端多 agent 管道核查 → 浮层展示结论。

## 架构

```
extension/  (TypeScript + Vite + MV3)        ← 1号 worktree: frontend
  抓推文 → 注入"核查"按钮 → 浮层显示结论
        │  POST /factcheck  （契约见 backend/contracts ↔ extension/src/types.ts）
        ▼
backend/   (FastAPI + Python)
  contracts/   共享数据契约（Pydantic）——三线命脉，已冻结
  agents/      claim / evaluator / critic   ← 2号 worktree: agent-layer
  core.py      单断言管道 + 结果收敛
  orchestrator.py  串 claim→evaluator→critic ← 3号 worktree: engine-wiring
  mocks/       固定数据 mock，全线联调用
  app.py       HTTP 入口（默认挂 mock 编排器）
```

核查管道：`推文 → ClaimAgent 抽断言 → EvaluatorAgent 搜证初判 → CriticAgent 复核 → 聚合`

模型分层：claim=`claude-haiku-4-5`，evaluator/critic=`claude-sonnet-4-6`。

## 并行开发（git worktree）

`main` 已冻结 contracts + engine 骨架 + mocks。三条线在 worktree 上并行：

```bash
git worktree add ../factchecker-frontend     -b frontend       # extension/
git worktree add ../factchecker-agent-layer  -b agent-layer    # backend/agents/
git worktree add ../factchecker-engine-wiring -b engine-wiring  # backend/orchestrator + core
# 第4个终端留在 main，做监督 / 集成
```

各线均依赖 `backend/contracts`（Python）/ `extension/src/types.ts`（TS）的冻结契约。

## 本地运行（全 mock，无需 API key）

> **Python 钉死 3.12**（三线统一，避免 3.9 的 `X | None` / Pydantic 建类炸裂）。

```bash
cd backend
python3.12 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
uvicorn app:app --reload          # http://localhost:8000/factcheck
pytest                            # 契约单测 + mock 链路 HTTP 集成测试
python smoke.py                   # 集成冒烟（双链路）：mock 始终跑，real 需 key
```

### 集成冒烟（双链路）

三线合并回 `main` 后一键验证，过真实 HTTP 表面（ASGI）：

```bash
python smoke.py                                              # 仅 mock（硬门槛）
USE_REAL_AGENTS=1 ANTHROPIC_API_KEY=sk-... python smoke.py  # mock + real

cd ../extension
npm install
npm run build                     # 产物 dist/ → Chrome 加载已解压扩展
```

接入真实 Claude agent：`export USE_REAL_AGENTS=1`（待 engine-wiring 落地组装）。


## Detected evidence (automated analysis)

Indexed codebase: 32 recognized source files, 105 KB.
- Anthropic (technology) — detected in the code
- FastAPI (technology) — detected in the code
- Python (language) — detected in the code
- TypeScript (language) — detected in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers

## Codebase structure (from repository index)

### Files (40 of 40)

```
.gitignore
.python-version
backend/agents/__init__.py
backend/agents/_structured.py
backend/agents/base.py
backend/agents/claim.py
backend/agents/critic.py
backend/agents/evaluator.py
backend/app.py
backend/cache.py
backend/contracts/__init__.py
backend/contracts/models.py
backend/core.py
backend/llm/__init__.py
backend/llm/client.py
backend/mocks/__init__.py
backend/mocks/mock_agents.py
backend/orchestrator.py
backend/pytest.ini
backend/requirements.txt
backend/smoke.py
backend/stream_events.py
backend/tests/__init__.py
backend/tests/test_agents.py
backend/tests/test_contracts.py
backend/wiring.py
CLAUDE.md
docs/INTEGRATION.md
extension/manifest.json
extension/mocks/response.json
extension/package.json
extension/src/api.ts
extension/src/content.ts
extension/src/overlay.ts
extension/src/stream.ts
extension/src/types.ts
extension/src/vite-env.d.ts
extension/tsconfig.json
extension/vite.config.ts
README.md
```

### Dependencies

- backend/requirements.txt: anthropic@>=0.69, fastapi@>=0.115, httpx@>=0.27, pydantic@>=2.9, pytest@>=8.3, pytest-asyncio@>=0.24, uvicorn[standard]@>=0.32
- extension/package.json: @types/chrome@^0.0.270, typescript@^5.6.0, vite@^5.4.0

### Recent commits (newest first)

- feat(evaluator): 证据溯源观测（A）+ 筛选/过滤复盘归档
- feat(core): 聚合结论透明化覆盖率（UNVERIFIABLE 不再被头部一笔带过）
- Merge branch 'agent-layer': agents 单测 + 对齐 messages.parse
- docs: 总验收——补容错闭环/缓存/流式增量 + 一页状态总账
- feat: 流式进度（SSE）——冷启动从干等变渐进渲染
- perf(engine): 结果缓存——重复推文复用结论，不重跑管道
- test(agents): 补 14 个单测 + 对齐 DoD 改用 messages.parse
- docs: INTEGRATION 补扩展双链路验收（VITE_USE_MOCK 构建期开关）
- fix(extension): USE_MOCK 改为 Vite 构建期开关，默认打真后端
- docs: 三线集成记录（合并范围 + 端到端验证证据） (#1)
- feat(engine): orchestrator 并行容错降级（闭环最后一块）
- Merge branch 'frontend'
- Merge branch 'agent-layer'
- test: 双链路集成冒烟脚本 + app create_app 工厂，替掉过时直调测试
- feat(frontend): 推文核查全链路打通（假数据，显示 score=35.2 卡片）
- feat(agents): 实现 claim/evaluator/critic 真实 agent
- docs: frontend 线作业书（启动 + DoD 硬标准 + 铁律）
- docs: agent-layer 线作业书（启动 + DoD 硬标准 + 铁律）
- refactor: 真实接线提成独立 wiring.py（app.py 只调度）
- chore: 钉死 Python 3.12（requirements + README + .python-version）

## Key source files (fetched from GitHub, selected and truncated for size)

### CLAUDE.md

```markdown
# CLAUDE.md — Tweet FactChecker（集成主干 / main）

X/Twitter 推文一键事实核查：浏览器扩展抓推文 → 后端多 agent 管道核查 → 浮层展示结论。

> 三条 worktree（frontend / agent-layer / engine-wiring）已合并回 `main`。
> 各线的作业书保留在各自分支；本文件是集成主干的总览。

## 架构

```
extension/  (TypeScript + Vite + MV3)        ← 1 号线 frontend
  抓推文 → 注入「核查」按钮 → POST /factcheck → 浮层渲染结论
        │
        ▼
backend/   (FastAPI + Python 3.12)
  contracts/   共享数据契约（Pydantic）—— 冻结，与 extension/src/types.ts 镜像
  agents/      claim / evaluator / critic   ← 2 号线 agent-layer（真实 Claude agent）
  core.py      单断言收敛 + 聚合
  orchestrator.py  串 claim→evaluator→critic
  wiring.py    真实 agent 组装（USE_REAL_AGENTS=1）← 3 号线 engine-wiring
  mocks/       固定数据 mock，零 key 联调
  app.py       create_app(orchestrator) 工厂 + HTTP 入口
  smoke.py     双链路集成冒烟（mock 硬门槛 + real 可选）
```

核查管道：`推文 → ClaimAgent 抽断言 → EvaluatorAgent 搜证初判 → CriticAgent 复核 → 聚合`
模型分层：claim=`claude-haiku-4-5`，evaluator/critic=`claude-sonnet-4-6`。

## 🚫 铁律

**契约冻结**：`backend/contracts/models.py` 与 `extension/src/types.ts` 的形状一一对应，
改动必须两侧同步——这是前后端唯一会漂移打架的地方。

## 本地端到端

```bash
# 后端（默认全 mock，无需 key）
cd backend
python3.12 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
pytest                                                # 契约单测 + mock 链路 HTTP 集成
python smoke.py                                       # 集成冒烟（mock）
uvicorn app:app --reload                              # http://localhost:8000/factcheck

# 接真实 Claude 链路
USE_REAL_AGENTS=1 ANTHROPIC_API_KEY=sk-ant-... uvicorn app:app --reload

# 前端
cd ../extension
npm install
npm run build                                         # 产物 dist/ → Chrome 加载已解压扩展
```

扩展默认打 `http://localhost:8000/factcheck`（`extension/src/api.ts`）。
本地离线开发用 `VITE_USE_MOCK=true npm run build`，走 `extension/mocks/response.json` 假数据；
上线构建不带该 env → 自动真接口（Vite 构建期静态注入，杜绝忘改 const 把假数据发上线）。

```

### docs/INTEGRATION.md

```markdown
# 集成记录 — 三线合并回 main

Tweet FactChecker 采用 git worktree 三线并行开发，`main` 冻结契约 + 引擎骨架 + mocks，
三条线在各自 worktree 落地后合并回主干。本文件记录这次集成的范围与验证证据。

## 合并范围

| 线 | 分支 | 战场 | 合并提交 |
|----|------|------|----------|
| 1 号 frontend | `frontend` | `extension/`（MV3 + Vite + TS）抓推文→注入按钮→POST→浮层 | `ec9094e Merge branch 'frontend'` |
| 2 号 agent-layer | `agent-layer` | `backend/agents/` claim / evaluator / critic 真实 Claude agent | `ff36c01 Merge branch 'agent-layer'` |
| 3 号 engine-wiring | `engine-wiring` | `backend/wiring.py` 真实组装 | 已先期落入 main（`7cb6700`） |
| 4 号 监督/集成 | `main` | `create_app` 工厂 + `smoke.py` 双链路冒烟 | `6556dbf` |

冲突仅 `CLAUDE.md`（各线作业书 add/add）：main 改写为集成总览，各线作业书保留在各自分支。

## 契约一致性

`backend/contracts/models.py`（Pydantic）↔ `extension/src/types.ts`（TS 镜像）全程未改动，
前后端零漂移。

## 验证证据

### 后端（Python 3.12）

```
pytest        → 4 passed（契约单测 + mock 链路 HTTP 集成）
smoke.py mock → verdict=mostly_true conf=0.72 claims=2
```

### 浏览器端到端（扩展 → 真实 HTTP → 后端）

扩展 `USE_MOCK=false`，content script 打 `POST http://localhost:8000/factcheck`，
浮层渲染后端 mock orchestrator 结果：**可信分 72.0 / 基本属实 / 2 断言**（证明走真实 HTTP，
非前端静态 `mocks/response.json`）。

### 真实 Claude 链路

```
USE_REAL_AGENTS=1 smoke.py
real → verdict=true conf=0.99 claims=2
       models={claim: claude-haiku-4-5, evaluator: claude-sonnet-4-6, critic: claude-sonnet-4-6}
```

测试推文「地球是圆的。月球绕地球转。」走完
claim(haiku) → evaluator(sonnet + web_search 搜证) → critic(sonnet) 复核 → 聚合，
判定 `true / 0.99`，模型分层与契约约定一致。

### 扩展双链路（构建期 mock 开关）

`extension/src/api.ts` 用 `import.meta.env.VITE_USE_MOCK` 在构建期决定走向，
缺省打真后端，杜绝误把假数据发上线。两条链路均经浏览器点「核查」验收：

| 构建 | 产物 | 链路 | 浮层结果 |
|------|------|------|----------|
| `npm run build`（缺省）| 6.81 kB（mock JSON 被 tree-shake）| `POST localhost:8000/factcheck` 真后端 | 72.0 / 基本属实（mock orchestrator）；真实 agent 时 true / 0.99 |
| `VITE_USE_MOCK=true npm run build` | 8.13 kB（mock JSON 内联）| 离线 `mocks/response.json`，无需后端 | 35.2 / 大体不实 / 2 断言 / 3 证据 |

## 集成加固（合并后增量）

四线合并后，集成位（4 号）在 main 上补齐了闭环与延迟优化。均不改冻结契约。

### 容错降级（闭环最后一块）

engine-wiring 线代码未落地，集成位代补其 DoD：

- `core.safe_check_claim`：单条 claim 超时/异常一律降级为 `UNVERIFIABLE`，**绝不向上抛** →
  `orchestrator` 并行 `gather` 里任何单条失败不再 500 整条推文。
- `core.degraded_result`：降级带原因，可审计。
- `CLAIM_TIMEOUT_S`：初设 120s 太紧，真实两段式联网 evaluator（3 次串行 sonnet）会被误降级；
  实测诊断后调到 300s（超时只兜病态卡死）。
- 验证：新增「evaluator 抛错→整条降级不崩」单测；真链路恢复 `true / 0.99`。

### 延迟优化①：结果缓存

- `cache.CachingChecker` + `ResultCache`（带 TTL 的有界内存 LRU，键=规范化文本+语言），
  app 层包装，mock/real 通用，`CACHE_TTL_S` 可调（默认 1h）。
- 命中回填当前 `tweet_id` 与真实耗时（不可变更新），对判定质量零影响。
- 真链路实测：**冷启动 102.9s → 命中 1.0ms（≈10⁵×），结论一致**。
- 多 worker 部署需换共享缓存（Redis），已留 TODO。

### 延迟优化②：流式进度（SSE）

- `POST /factcheck/stream`：`claims 骨架 → claim×N（完成顺序）→ done 聚合`。
  `orchestrator.check_stream` 用 `as_completed` 边评边推；缓存命中直推单个 `done`。
- 前端 `stream.ts` 用 fetch + ReadableStream 自解析 SSE（EventSource 仅 GET，推文走 POST）；
  `ProgressOverlay` 骨架→逐条填判定→done 换规范最终卡片。
- 总时长不变，降的是**感知延迟**：冷启动不再干等。
- 验证：真 HTTP（Node fetch，与扩展同解析逻辑）跑出 `claims → claim → claim → done`。

---

## 总验收（一页）

> 截至 `745a19c`。Python 3.12 钉死；契约
[truncated — 2098 more characters]
```

### backend/requirements.txt

```
# 需要 Python >= 3.12（三线统一，消灭 3.9 的 `X | None` / Pydantic 建类炸裂问题）
# 每条线第一步：python3.12 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt
anthropic>=0.69
fastapi>=0.115
uvicorn[standard]>=0.32
pydantic>=2.9
pytest>=8.3
pytest-asyncio>=0.24
httpx>=0.27

```

### extension/package.json

```
{
  "name": "factchecker-extension",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite build --watch",
    "build": "tsc --noEmit && vite build",
    "typecheck": "tsc --noEmit"
  },
  "devDependencies": {
    "typescript": "^5.6.0",
    "vite": "^5.4.0",
    "@types/chrome": "^0.0.270"
  }
}

```

### backend/app.py

```python
"""FastAPI 入口：POST /factcheck。

默认挂全 mock 编排器，三个 worktree 都能 `uvicorn app:app --reload` 起服务。
USE_REAL_AGENTS=1 时切真实 Claude 链路（接线见 wiring.py）。

create_app(orchestrator) 工厂：smoke.py / 测试可注入任意编排器（mock 或真实），
不依赖 import 时的环境变量——这是双链路冒烟的接缝。
"""

from __future__ import annotations

import os

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse

from cache import CachingChecker, Checker, ResultCache
from contracts.models import FactCheckRequest, FactCheckResult
from mocks import build_mock_orchestrator
from stream_events import ErrorEvent, to_sse
from wiring import build_real_orchestrator


def create_app(orchestrator: Checker) -> FastAPI:
    """用指定编排器组装 FastAPI app（任意满足 Checker 协议者，含缓存包装）。"""
    app = FastAPI(title="Tweet FactChecker", version="0.1.0")

    # 浏览器扩展 content script 跨域调用 → 放开 CORS（生产应收紧来源）。
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_methods=["POST", "OPTIONS"],
        allow_headers=["*"],
    )

    @app.get("/health")
    async def health() -> dict[str, str]:
        return {"status": "ok"}

    @app.post("/factcheck", response_model=FactCheckResult)
    async def factcheck(request: FactCheckRequest) -> FactCheckResult:
        """核查一条推文，返回完整结论（一次性）。"""
        return await orchestrator.check(request)

    @app.post("/factcheck/stream")
    async def factcheck_stream(request: FactCheckRequest) -> StreamingResponse:
        """流式核查：SSE 推 claims → claim×N → done，前端渐进渲染。"""

        async def gen():
            try:
                async for event in orchestrator.check_stream(request):
                    yield to_sse(event)
            except Exception as exc:  # noqa: BLE001 — 流内兜底，转 error 事件
                yield to_sse(ErrorEvent(message=f"{type(exc).__name__}: {exc}"))

        return StreamingResponse(
            gen(),
            media_type="text/event-stream",
            headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
        )

    return app


def get_orchestrator() -> Checker:
    """编排器工厂（外层套结果缓存）。

    USE_REAL_AGENTS=1 时切换到真实 Claude agent（接线见 wiring.py）。
    默认走全 mock，无需 API key 即可起服务联调。
    结果缓存只省重复推文的重复计算，不影响判定质量；TTL 由 CACHE_TTL_S 调（默认 1h）。
    """
    inner = build_real_orchestrator() if os.getenv("USE_REAL_AGENTS") == "1" else build_mock_orchestrator()
    ttl = float(os.getenv("CACHE_TTL_S", "3600"))
    return CachingChecker(inner, ResultCache(ttl_s=ttl))


# uvicorn app:app 入口（按 USE_REAL_AGENTS 选链路 + 结果缓存）。
app = create_app(get_orchestrator())

```

### extension/vite.config.ts

```typescript
import { defineConfig } from "vite";
import { resolve } from "node:path";

// MV3 content script 必须是单文件 IIFE，关闭代码分割。
export default defineConfig({
  build: {
    outDir: "dist",
    emptyOutDir: true,
    rollupOptions: {
      input: { content: resolve(__dirname, "src/content.ts") },
      output: {
        entryFileNames: "[name].js",
        format: "iife",
        inlineDynamicImports: true,
      },
    },
  },
});

```

### backend/stream_events.py

```python
"""流式进度事件 + SSE 序列化。

新增传输面，**不改冻结契约**（contracts/models.py 原样复用）。
事件序列：claims（断言骨架）→ claim×N（逐条完成，完成顺序）→ done（最终聚合）。
失败时 error。前端按 claim_id 把 claim 事件填进骨架对应行。
"""

from __future__ import annotations

from typing import Literal, Union

from pydantic import BaseModel

from contracts.models import Claim, ClaimResult, FactCheckResult


class ClaimsEvent(BaseModel):
    """断言抽取完成，给前端渲染骨架行（仅可核查的断言，与最终 result.claims 一致）。"""

    type: Literal["claims"] = "claims"
    claims: list[Claim]


class ClaimDoneEvent(BaseModel):
    """单条断言核查完成（evaluator+critic+收敛），前端填进对应行。"""

    type: Literal["claim"] = "claim"
    result: ClaimResult


class DoneEvent(BaseModel):
    """全部完成，携带最终聚合结论（与 /factcheck 返回的 FactCheckResult 同形）。"""

    type: Literal["done"] = "done"
    result: FactCheckResult


class ErrorEvent(BaseModel):
    type: Literal["error"] = "error"
    message: str


StreamEvent = Union[ClaimsEvent, ClaimDoneEvent, DoneEvent, ErrorEvent]


def to_sse(event: BaseModel) -> str:
    """序列化为一条 SSE 记录：event:<type>\\ndata:<json>\\n\\n。"""
    name = getattr(event, "type", "message")
    return f"event: {name}\ndata: {event.model_dump_json()}\n\n"

```

### backend/wiring.py

```python
"""真实编排器组装（engine-wiring worktree 负责）。

把共享的 ClaudeClient 注入三个真实 agent，再装进 Orchestrator——
与 mocks.build_mock_orchestrator 同构，仅替换被注入的 agent 实现。

注意：本模块只负责"接线"。三个 agent 的内部实现（提示词 / structured
outputs / web_search）属于 agent-layer worktree；其方法当前可能仍抛
NotImplementedError，待 agent-layer 落地后整条链路即自动可用。
"""

from __future__ import annotations

from typing import Optional

from agents.claim import ClaudeClaimAgent, MODEL as CLAIM_MODEL
from agents.evaluator import ClaudeEvaluatorAgent, MODEL as EVALUATOR_MODEL
from agents.critic import ClaudeCriticAgent, MODEL as CRITIC_MODEL
from llm.client import ClaudeClient
from orchestrator import Orchestrator


def build_real_orchestrator(client: Optional[ClaudeClient] = None) -> Orchestrator:
    """组装一个调用真实 Claude 的编排器。

    三个 agent 共用同一个 ClaudeClient（连接池 / 凭证复用）。
    model_versions 取各 agent 自带的 MODEL 常量，写入响应供审计。
    """
    client = client or ClaudeClient()
    return Orchestrator(
        claim_agent=ClaudeClaimAgent(client),
        evaluator=ClaudeEvaluatorAgent(client),
        critic=ClaudeCriticAgent(client),
        model_versions={
            "claim": CLAIM_MODEL,
            "evaluator": EVALUATOR_MODEL,
            "critic": CRITIC_MODEL,
        },
    )

```

### backend/smoke.py

```python
"""集成冒烟：mock + real 双链路，过 FastAPI app（真集成，非直调 orchestrator）。

供三线合并回 main 后一键验证。用法：

    python smoke.py                 # 仅 mock 链路（默认，无需 key）
    USE_REAL_AGENTS=1 ANTHROPIC_API_KEY=sk-... python smoke.py   # mock + real

退出码：0 全过；非 0 有失败。
- mock 链路始终跑，是每次合并的硬门槛。
- real 链路仅在 USE_REAL_AGENTS=1 且有 key 时跑；一旦启用，失败即非 0
  （三线全部落地后应通过；agent-layer 未实现时启用会如实报错）。
"""

from __future__ import annotations

import asyncio
import os
import sys

import httpx

from app import create_app
from contracts.models import FactCheckResult
from mocks import build_mock_orchestrator
from wiring import build_real_orchestrator

SAMPLE = {
    "tweet_id": "smoke-1",
    "text": "地球是圆的。月球绕地球转。",
    "author_handle": "@smoke",
}


async def _hit(app) -> FactCheckResult:
    """过 ASGI 打 POST /factcheck，校验响应符合契约。"""
    transport = httpx.ASGITransport(app=app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
        health = await c.get("/health")
        assert health.status_code == 200, f"/health -> {health.status_code}"
        resp = await c.post("/factcheck", json=SAMPLE)
        assert resp.status_code == 200, f"/factcheck -> {resp.status_code}: {resp.text}"
        # 用契约模型反序列化 = 形状校验
        result = FactCheckResult.model_validate(resp.json())
        assert result.tweet_id == SAMPLE["tweet_id"]
        assert 0.0 <= result.overall_confidence <= 1.0
        return result


async def run_mock() -> None:
    app = create_app(build_mock_orchestrator())
    result = await _hit(app)
    print(
        f"  ✓ mock  : verdict={result.overall_verdict.value} "
        f"conf={result.overall_confidence:.2f} claims={len(result.claims)} "
        f"ms={result.processing_ms}"
    )


async def run_real() -> None:
    app = create_app(build_real_orchestrator())
    result = await _hit(app)
    print(
        f"  ✓ real  : verdict={result.overall_verdict.value} "
        f"conf={result.overall_confidence:.2f} claims={len(result.claims)} "
        f"models={result.model_versions}"
    )


async def main() -> int:
    print("集成冒烟（双链路）")
    failures: list[str] = []

    # ── mock 链路：硬门槛 ──
    try:
        await run_mock()
    except Exception as e:  # noqa: BLE001 — 冒烟脚本顶层兜底，原样上报
        failures.append(f"mock: {e!r}")
        print(f"  ✗ mock  : {e!r}")

    # ── real 链路：仅在显式启用时跑 ──
    real_enabled = os.getenv("USE_REAL_AGENTS") == "1" and bool(
        os.getenv("ANTHROPIC_API_KEY") or os.getenv("ANTHROPIC_AUTH_TOKEN")
    )
    if real_enabled:
        try:
            await run_real()
        except Exception as e:  # noqa: BLE001
            failures.append(f"real: {e!r}")
            print(f"  ✗ real  : {e!r}")
    else:
        print("  – real  : SKIP（设 USE_REAL_AGENTS=1 + ANTHROPIC_API_KEY 启用）")

    if failures:
        print(f"\n✗ 冒烟失败 {len(failures)} 项")
        return 1
    print("\n✓ 冒烟全过")
    return 0


if __name__ == "__main__":
    sys.exit(asyncio.run(main()))

```

### backend/orchestrator.py

```python
"""编排器：串起 claim → evaluator → critic，产出 FactCheckResult。

engine-wiring worktree 的主战场。注入三个 agent（真实或 mock），
对外暴露 check()（一次性）与 check_stream()（流式进度）。
"""

from __future__ import annotations

import asyncio
import time
from typing import AsyncIterator, Optional

from agents.base import ClaimAgent, EvaluatorAgent, CriticAgent
from contracts.models import ClaimResult, FactCheckRequest, FactCheckResult
from core import aggregate, safe_check_claim
from stream_events import ClaimDoneEvent, ClaimsEvent, DoneEvent, StreamEvent


class Orchestrator:
    """核查管道编排器。"""

    def __init__(
        self,
        claim_agent: ClaimAgent,
        evaluator: EvaluatorAgent,
        critic: CriticAgent,
        model_versions: Optional[dict[str, str]] = None,
    ) -> None:
        self._claim_agent = claim_agent
        self._evaluator = evaluator
        self._critic = critic
        self._model_versions = model_versions or {}

    async def check(self, request: FactCheckRequest) -> FactCheckResult:
        """一次性主流程：抽取断言 → 并行评估每条 → 聚合。"""
        started = time.monotonic()

        claims = await self._claim_agent.extract(request.text, request.lang)
        checkable = [c for c in claims if c.checkable]

        # 各断言相互独立 → 并行评估，缩短端到端时延。
        # safe_check_claim：单条超时/异常一律降级为 UNVERIFIABLE，绝不拖垮整条推文。
        results = await asyncio.gather(
            *(safe_check_claim(c, self._evaluator, self._critic) for c in checkable)
        )
        return self._build_result(request, list(results), started)

    async def check_stream(
        self, request: FactCheckRequest
    ) -> AsyncIterator[StreamEvent]:
        """流式主流程：先推断言骨架，再按完成顺序逐条推结果，最后推最终聚合。

        总时长与 check() 相同（claims 仍并行），但前端能渐进渲染、消除"干等"。
        """
        started = time.monotonic()

        claims = await self._claim_agent.extract(request.text, request.lang)
        checkable = [c for c in claims if c.checkable]
        yield ClaimsEvent(claims=checkable)

        results: list[ClaimResult] = []
        if checkable:
            tasks = [
                asyncio.create_task(
                    safe_check_claim(c, self._evaluator, self._critic)
                )
                for c in checkable
            ]
            # as_completed：哪条先评完先推哪条，前端按 claim_id 填行。
            for fut in asyncio.as_completed(tasks):
                cr = await fut
                results.append(cr)
                yield ClaimDoneEvent(result=cr)

        yield DoneEvent(result=self._build_result(request, results, started))

    def _build_result(
        self,
        request: FactCheckRequest,
        results: list[ClaimResult],
        started: float,
    ) -> FactCheckResult:
        overall, confidence, summary = aggregate(request.tweet_id, results)
        elapsed_ms = int((time.monotonic() - started) * 1000)
        return FactCheckResult(
            tweet_id=request.tweet_id,
            overall_verdict=overall,
            overall_confidence=confidence,
            summary=summary,
            claims=results,
            processing_ms=elapsed_ms,
            model_versions=self._model_versions,
        )

```

[22 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]