Metadata-Version: 2.4
Name: maze-moderation-sdk
Version: 0.1.6
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 | 收集可选本地信号 + 云信号，返回原始 `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)

# 如需本地 hard 词库兜底，显式开启；默认关闭
cfg = ReviewConfig(
    cloud_provider=AliyunProvider(...),
    local_lexicon_enabled=True,
)
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。

## 本地审核（可选）

> **local 层定位**：本地词库默认关闭，不替云端识别敏感词。泛化召回（语义/上下文/对抗变体）交给云端。需要本地 hard 兜底时，由调用方显式设置 `ReviewConfig(local_lexicon_enabled=True)`。

只使用 `ReviewConfig()` 且不传 `cloud_provider` 时，不会触发云审或本地词库，默认策略会得到 `L0/pass`。

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

# 直接使用本地扫描 API
lexicon = load_lexicon()
hits = scan_local("待审文本", lexicon=lexicon)

# 或在 review_text 中显式启用本地词库
cfg = ReviewConfig(
    cloud_provider=AliyunProvider(...),
    local_lexicon_enabled=True,
)
verdict = await review_text("待审文本", config=cfg)
```

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

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

## 开发

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