Metadata-Version: 2.4
Name: mingpan
Version: 0.1.1
Summary: 明判（MingPan）Python SDK —— 三原语判断接口的类型安全客户端
License: MIT
Project-URL: Homepage, https://mingpan.oneapis.cn
Project-URL: Documentation, https://mingpan.oneapis.cn/docs
Keywords: mingpan,jev,judge,llm,type-safe,routing
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# 明判 Python SDK

明判（MingPan）三原语判断接口的 Python 客户端。**运行时零依赖**（只用标准库 `urllib`），
`pip install` 后即可调用，不引入任何版本冲突。

```bash
pip install mingpan
```

**零运行时依赖** —— 装完不会引入任何第三方包（只用标准库 `urllib`）。
要求 **Python ≥ 3.9**（PyPI 上的 `Requires-Python`）。

装完自检（两条都应正常输出）：

```bash
python -c "import mingpan; print(mingpan.__version__)"   # 0.1.1
python -c "from mingpan import Client, DEFAULT_BASE_URL; print(DEFAULT_BASE_URL)"
```

需要跟仓库里最新未发版的改动时，从源码目录装：`pip install ./sdk/python`。

---

## 快速开始

```python
from mingpan import Client, choice, score, noul

client = Client(api_key="sk-xxx")          # 控制台「API Key」页创建

result = client.judge(
    state="工单：客户反映发票抬头开错，要求尽快重开，语气着急。",
    questions={
        "intent":  choice({"售后": "退换货、维修", "物流": "发货与配送", "计费": "账单与退款"}),
        "urgency": score(["平静", "略不满", "明显不满", "情绪激烈", "准备投诉"]),
        "is_vip":  noul("该用户是 VIP 客户"),
    },
)

result.choice("intent").choice        # '计费' —— str，不是 dict 里捞出来的
result.score("urgency").score         # 1.5   —— float，0-based 加权
result.noul("is_vip").is_true(0.6)    # False
result.usage.input_tokens             # 391   —— 计费依据
```

> 示例里的返回值（`'计费'` / `1.5` / `False` / `391`）取自一次真实调用，
> 实际数值随材料与模型版本变化 —— 它们演示的是**形状与拆包方式**，不是固定值。

## 三个原语

| 原语 | 构造 | 返回 | 说明 |
|---|---|---|---|
| `choice` | `choice({"标签": "说明"})` | `choice`(str) / `confidence` / `probabilities` | 从候选中选唯一答案。**`criteria` 是对象**，1–255 项 |
| `score` | `score(["低", "中", "高"])` | `score`(float) / `confidence` / `probabilities` / `legend` | 放到有序量级上打分。**`criteria` 是数组**，2–10 级，从低到高 |
| `noul` | `noul("命题正文")` | `noul`(float 0–1) | 判断命题是否成立。**没有 `confidence`** |

几个必须知道的事实：

- **`score` 是 0-based 浮点**。`score=1.5` 表示重心落在第 2 级（序号 1）附近 ——
  不是「1.5 分 / 满分 5 分」。别拿它当整数用。
- **`noul` 没有 `confidence`**。SDK 如实不提供该属性：读它会 `AttributeError`，
  而不是悄悄给个 `0.0` 让你的路由代码走错分支。
- **`criteria` 的说明会进模型**。`{"计费": "账单与退款"}` 的判定质量明显高于
  `["计费"]`。只给标签时模型只能靠字面猜；说明才是准确率的来源。
- **只有 `input_tokens` 计费**。`output_tokens` 会返回（实测 58–67，不是 0），
  但不在计费式里。

## 类型安全

`answers` 与 `questions` 同构，但 SDK 会按原语类型**派发成不同的类**，
取值器对类型不符直接报错：

```python
result.noul("is_vip")      # NoulAnswer
result.choice("is_vip")    # ✗ TypeMismatchError: 实际类型是 noul，不能用 result.choice() 取值
```

这比「字典下标 + `.get()`」安全得多：类型错了在**调用点**就炸，
而不是在几十行之外的 `NoneType has no attribute` 处炸。

结果对象上还有两个便于排查/展示的方法：

```python
result.choice("intent").top(2)     # [('计费', 0.91), ('售后', 0.09)]
                                   # 前二名贴得越近，越值得转人工
result.score("urgency").top(2)     # [('1', 0.26), ('0', 0.2)]
result.score("urgency").label()    # '略不满' —— 仅为可读性，
                                   # 业务分支请直接比 score 本身
```

## 错误处理

每类失败对应一个可分别捕获的类型：

```python
from mingpan import (
    AuthenticationError, InvalidRequestError,
    RateLimitError, ServiceUnavailableError, TransportError,
)

try:
    result = client.judge(state=state, questions=questions)
except AuthenticationError:      # 401/403：Key 无效、过期、无权。重试无意义
    rotate_key()
except InvalidRequestError as e: # 400/422：参数不符。message 通常直接给出正确写法
    print(e.message)             #   → "Choice criteria must contain between 1 and 255 options"
except RateLimitError:           # 429：退避重试，或改用 judge_batch 降低并发
    backoff()
except ServiceUnavailableError:  # 5xx：不计费，可退避重试
    backoff()
except TransportError:           # 网络层：连不上/DNS/超时，检查网络或代理
    pass
```

`InvalidRequestError` 的 message 是最有价值的错误信息 ——
上游的校验错误会直接说出**正确字段名与取值范围**，照着改即可，不必翻文档。

## 并发与批量

```python
import asyncio

# 单次异步（不阻塞事件循环）—— 注意它只能在协程里 await
async def main():
    result = await client.judge_async(state=state, questions=questions)
    return result

result = asyncio.run(main())

# 多个 state 共享同一套 questions，并发发送，结果按下标对应
results = client.judge_batch(
    states=[t1, t2, t3],
    questions=questions,
    max_workers=8,            # 别调太高：先撞上游限流，换来一批 429
    return_exceptions=True,   # 单条失败作为异常对象返回，不中断整批
)
```

## 把 confidence 接进你的代码

三原语的输出可以拼成一条可测试、可审计的路由链路：

```python
r = client.judge(state=ticket, questions={
    "intent":  choice({"退款": "要退款", "咨询": "问问题", "投诉": "表达不满"}),
    "urgency": score(["平静", "略不满", "明显不满", "情绪激烈", "准备投诉"]),
})

# 置信度足够 + 情绪不高 → 自动执行；否则转人工
if r.choice("intent").confidence >= 0.85 and r.score("urgency").score < 2.0:
    auto_execute(r.choice("intent").choice)
elif r.choice("intent").confidence >= 0.5:
    queue_for_human(r)
else:
    escalate(r)     # 低置信：转大模型或人工
```

阈值按**业务误判成本**定，不是按模型表现定 —— 退款错判的代价和分类错标的代价不一样。
路由决策本身是普通代码，所以它能被单测、能被 review、能进审计日志。

## 自建网关

默认走明判的正式入口（`https://mingpan.oneapis.cn/v1/judge`）。
如果判断能力挂在自己的网关上，直接指向那里即可：

```python
client = Client(
    api_key="sk-xxx",
    base_url="https://your-gateway.example.com",
    path="/typesafe/v1/systemone",   # 插件自有路由，路径与 BFF 不同
)
```

⚠️ 不同网关实例的插件 key 与路由路径可能不同（`/typesafe/v1/systemone` vs
`/jev/v1/systemone`），照抄另一台的路径会得到 404 或「SPA 兜底返回 HTML」。
客户端遇到 HTML 响应会明确提示「请检查地址是否正确」，而不是报一句
含糊的解析失败。

## 代理说明

SDK 默认沿用环境变量里的 `HTTP_PROXY` / `HTTPS_PROXY`（企业网络下通常必须）。
想绕过本机代理：

```python
client = Client(api_key="sk-xxx", use_env_proxy=False)
```

代理配错时的典型症状是「同一个 Key 在 `curl` 里能通、在脚本里连不上」——
遇到这种情况先怀疑代理，再怀疑 Key。

## 开发

```bash
cd sdk/python
python tests/test_sdk.py                              # 离线：59 项，不联网
MINGPAN_TEST_KEY=sk-xxx python tests/test_sdk.py --live   # 真实调用（会产生费用）
```
