Metadata-Version: 2.4
Name: maze-moderation-sdk
Version: 0.1.2
Summary: Maze 内容审核 SDK：文本审核先行，后续扩展图片与视频审核。
Project-URL: Homepage, https://github.com/MazeAI-pro/content_security
Project-URL: Repository, https://github.com/MazeAI-pro/content_security
Project-URL: Documentation, https://github.com/MazeAI-pro/content_security/tree/main/sdks/moderation#readme
Project-URL: Changelog, https://github.com/MazeAI-pro/content_security/blob/main/sdks/moderation/CHANGELOG.md
Project-URL: Issues, https://github.com/MazeAI-pro/content_security/issues
Author: Tada
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: opencc>=1.1
Requires-Dist: pyahocorasick>=2.0
Requires-Dist: pypinyin>=0.51
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# maze-moderation-sdk

Maze 内容审核 SDK。当前首发文本审核能力，后续在同一个 SDK 下扩展图片、视频审核。

文本审核模块是无状态文本审核核——不碰会话状态、不碰 Redis、不碰配置中心、不碰任何 channel。所有配置由调用方构造 `ReviewConfig` 注入，密钥由调用方构造 `CloudProvider` 时注入，永不进包逻辑。

完整设计见 `../../../to-tada/security/design-sdk.md`，开发计划见 `../../../dev-plan/text-censor-sdk-dev-plan.md`。

## 作为 SDK 引入

阶段 A（本地联调）推荐使用 sibling editable path：

```toml
[project]
dependencies = ["maze-moderation-sdk"]

[tool.uv.sources]
maze-moderation-sdk = { path = "../content_security/sdks/moderation", editable = true }
```

团队内 CI 或跨仓验证可锁定 Git tag：

```toml
[project]
dependencies = ["maze-moderation-sdk"]

[tool.uv.sources]
maze-moderation-sdk = { git = "ssh://git@github.com/MazeAI-pro/content_security.git", tag = "v0.1.2", subdirectory = "sdks/moderation" }
```

公开发布到 PyPI 后：

```toml
dependencies = ["maze-moderation-sdk>=0.1,<0.2"]
```

不要把本地 path 依赖写进生产环境配置。PyPI 发布说明见 [`docs/publish.md`](docs/publish.md)。

## 公共 API

文本审核能力位于 `maze_moderation.text`：

| 函数 | 层 | 说明 |
| --- | --- | --- |
| `normalize(text)` | 纯函数 | 变体归一化（零宽/全半角/繁简/大小写/重复压缩/拼音） |
| `scan_local(text, *, lexicon)` | 只本地 | 已归一化文本上的 AC 自动机扫描 |
| `review_cloud(text, *, provider, chat_id, done)` | 只云 | 只调云 API，不跑本地 DFA |
| `collect_signals(text, *, config)` | mechanism | 跑 DFA + 云，返回原始 `Signals`，不做判定 |
| `review_text(text, *, config)` | mechanism+policy | 收集信号后交由 `config.risk_strategy` 判定，出 `Verdict` |

外加 `RiskStrategy` / `CloudProvider` 两个 Protocol 和一组 frozen dataclass（`Hit`/`CloudVerdict`/`Signals`/`Verdict`）。

## 三种用法

| 消费方 | 用法 |
| --- | --- |
| tada | `review_text` + `DefaultRiskStrategy`，阈值走 config 微调；流式切片在包外多次调 |
| 第二个项目 | `review_text` + 默认策略，非流式，一段调一次 |
| 任意未来项目 | `collect_signals` 拿原始信号自己合并，或传自定义 `RiskStrategy` |

## 接入示例

```python
from maze_moderation.text import ReviewConfig, review_text, DefaultRiskStrategy
from maze_moderation.text.cloud.aliyun import AliyunProvider

# 有状态编排（如 tada）：编排层构造 config，会话状态/流式切片留在包外
cfg = ReviewConfig(
    cloud_provider=AliyunProvider(
        access_key="...",
        access_secret="...",
        service="llm_response_moderation",  # 场景由调用方选：评论/昵称/LLM 等
        # endpoint 默认 green-cip.cn-shenzhen.aliyuncs.com
        # 内网: endpoint="green-cip-vpc.cn-shenzhen.aliyuncs.com"
    ),
    risk_strategy=DefaultRiskStrategy(l2_confidence_threshold=0.75),
)
verdict = await review_text(user_text, config=cfg)
if verdict.action == "refuse":
    ...  # 调用方自己处置

# 无状态、非流式（第二个项目）：同样的 review_text 直调
cfg = ReviewConfig(cloud_provider=AliyunProvider(...))
verdict = await review_text(output_text, config=cfg)

# 多场景共享凭证与连接池：每个 service 仍配独立 ReviewConfig / risk_strategy
import httpx
shared_client = httpx.AsyncClient()
base = AliyunProvider(
    access_key="...",
    access_secret="...",
    service="llm_query_moderation",
    http_client=shared_client,
)
cfg_input = ReviewConfig(cloud_provider=base)
cfg_reply = ReviewConfig(
    cloud_provider=base.with_service("llm_response_moderation"),
    risk_strategy=DefaultRiskStrategy(block_confidence_threshold=0.8),
)
```

多场景时凭证与 `http_client` 共享；每个 `ReviewConfig` 仍可独立配置 `risk_strategy`、`cloud_timeout_ms` 等。

## 依赖边界

- **依赖**：`pyahocorasick`、`opencc`、`pypinyin`、`httpx`。
- **不依赖**：`redis`、`fastapi`、`agentscope`、Config Center、任何 tada 内部模块。

## 版本策略

公共面（五函数签名 + frozen dataclass + 两个 Protocol）= SemVer 契约，改字段即 major bump。首发 `0.1.0` 已发布到 PyPI；后续 breaking change 走 major bump。

## 本地审核（M2）

> **local 层定位**：不替云端识别敏感词，泛化召回（语义/上下文/对抗变体）交给云端。local 层只做云端难以低成本覆盖的四件事——**确定性拦截**（高置信明确违禁词，零延迟）、**断网兜底**（云端降级时本地 hard 命中仍出 L3；无 hard 命中时兜底到 L2 同步送审，不静默放行）、**隐私前置**（PII 正则本地命中即拦，不外发）、**业务热修**（竞品/品牌/黑话黑名单，加词分钟级生效）。故词库**控规模、提精度**，不追求泛化覆盖。

```python
from maze_moderation.text import load_lexicon, scan_local

# 仅包内默认词库（konsheng 快照 v1，3267 词，全 hard，按 4 类分文件）
lexicon = load_lexicon()
hits = scan_local("待审文本", lexicon=lexicon)
```

词库只随包发布，不支持外部路径或运行期注入词条；后续扩词走云配置或发新版包。
包内词库目录结构：

```
lexicon/
  manifest.json    # version + files 元数据（category/severity）
  v1/              # 与 manifest.version 同名的子目录
    涉政.txt        # 文件名即 category；本地层全 hard，故只分类不分级
    暴恐.txt        # 每行一个词；跨分类冲突按红线优先级归一文件
    色情.txt
    灰区.txt
```

词库维护：当前快照基于 konsheng 词库经人工加工（合并去重、剔除误伤词、分类调整），
维护与重建说明见 `docs/lexicon-maintenance.md`。

## 开发

```bash
pip install -e ".[dev]"
pytest                # 覆盖率门槛 80%（见 pyproject.toml）
```
