prompt-gray-release 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. prompt_gray_release-0.2.0/LICENSE +21 -0
  2. prompt_gray_release-0.2.0/PKG-INFO +275 -0
  3. prompt_gray_release-0.2.0/README.md +240 -0
  4. prompt_gray_release-0.2.0/pyproject.toml +70 -0
  5. prompt_gray_release-0.2.0/setup.cfg +4 -0
  6. prompt_gray_release-0.2.0/src/prompt_gray_release/__init__.py +67 -0
  7. prompt_gray_release-0.2.0/src/prompt_gray_release/cli.py +186 -0
  8. prompt_gray_release-0.2.0/src/prompt_gray_release/envs.py +29 -0
  9. prompt_gray_release-0.2.0/src/prompt_gray_release/evaluators.py +86 -0
  10. prompt_gray_release-0.2.0/src/prompt_gray_release/exceptions.py +44 -0
  11. prompt_gray_release-0.2.0/src/prompt_gray_release/gate.py +134 -0
  12. prompt_gray_release-0.2.0/src/prompt_gray_release/py.typed +0 -0
  13. prompt_gray_release-0.2.0/src/prompt_gray_release/recorder.py +200 -0
  14. prompt_gray_release-0.2.0/src/prompt_gray_release/registry.py +87 -0
  15. prompt_gray_release-0.2.0/src/prompt_gray_release/render.py +25 -0
  16. prompt_gray_release-0.2.0/src/prompt_gray_release/rollout.py +174 -0
  17. prompt_gray_release-0.2.0/src/prompt_gray_release/router.py +71 -0
  18. prompt_gray_release-0.2.0/src/prompt_gray_release/tracing.py +78 -0
  19. prompt_gray_release-0.2.0/src/prompt_gray_release.egg-info/PKG-INFO +275 -0
  20. prompt_gray_release-0.2.0/src/prompt_gray_release.egg-info/SOURCES.txt +30 -0
  21. prompt_gray_release-0.2.0/src/prompt_gray_release.egg-info/dependency_links.txt +1 -0
  22. prompt_gray_release-0.2.0/src/prompt_gray_release.egg-info/entry_points.txt +2 -0
  23. prompt_gray_release-0.2.0/src/prompt_gray_release.egg-info/requires.txt +16 -0
  24. prompt_gray_release-0.2.0/src/prompt_gray_release.egg-info/top_level.txt +1 -0
  25. prompt_gray_release-0.2.0/tests/test_cli.py +206 -0
  26. prompt_gray_release-0.2.0/tests/test_evaluators.py +110 -0
  27. prompt_gray_release-0.2.0/tests/test_gate.py +110 -0
  28. prompt_gray_release-0.2.0/tests/test_recorder.py +134 -0
  29. prompt_gray_release-0.2.0/tests/test_registry.py +93 -0
  30. prompt_gray_release-0.2.0/tests/test_render.py +42 -0
  31. prompt_gray_release-0.2.0/tests/test_rollout.py +146 -0
  32. prompt_gray_release-0.2.0/tests/test_router.py +89 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 helloworldtang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,275 @@
1
+ Metadata-Version: 2.4
2
+ Name: prompt-gray-release
3
+ Version: 0.2.0
4
+ Summary: 把 Prompt 当生产代码发布:版本化、灰度放量、评测门禁、一键回滚、事故重演。Prompt release engineering: versioning, canary rollout, eval gates, rollback and incident replay.
5
+ Author: helloworldtang
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/helloworldtang/prompt-gray-release
8
+ Project-URL: Issues, https://github.com/helloworldtang/prompt-gray-release/issues
9
+ Keywords: prompt,llm,gray-release,canary,prompt-engineering,llmops,evaluation
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Classifier: Topic :: Text Processing
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: PyYAML>=6.0
22
+ Provides-Extra: schema
23
+ Requires-Dist: jsonschema>=4.0; extra == "schema"
24
+ Provides-Extra: otel
25
+ Requires-Dist: opentelemetry-api>=1.20; extra == "otel"
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7.0; extra == "dev"
28
+ Requires-Dist: ruff>=0.6; extra == "dev"
29
+ Requires-Dist: mypy>=1.10; extra == "dev"
30
+ Requires-Dist: types-PyYAML; extra == "dev"
31
+ Requires-Dist: jsonschema>=4.0; extra == "dev"
32
+ Requires-Dist: build; extra == "dev"
33
+ Requires-Dist: twine; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # prompt-gray-release
37
+
38
+ **把 Prompt 当生产代码发布。** 轻量、可嵌入、零平台依赖的 Prompt 发布工程库:
39
+ 版本化 → 评测门禁 → 灰度放量 → 一键回滚 → 事故确定性重演。
40
+
41
+ > Prompt release engineering: versioning, canary rollout, eval gates,
42
+ > one-click rollback and deterministic incident replay. Lightweight, embeddable, no platform required.
43
+ >
44
+ > [English README](README.en.md)
45
+
46
+ ## 为什么是它:与平台方案的根本差异
47
+
48
+ LangSmith / Langfuse 是**评测与观测平台**——它们管"效果好不好",管不住**上线那一刻**。
49
+ prompt-gray-release 补的就是上线那一刻,并且不要求你搭建或迁移到任何平台:
50
+
51
+ | | LangSmith / Langfuse 等平台 | prompt-gray-release |
52
+ |---|---|---|
53
+ | 形态 | SaaS / 自托管服务,数据要出域或进它们的存储 | **纯库**,`pip install` 即用,数据不出你的进程 |
54
+ | 私有化与审计 | 自托管版要搭整套服务 | **JSONL 审计事件内建**,append-only,合规环境直接对接 |
55
+ | CI 门禁 | 依赖平台 CI 集成 | `prompt-gray check` 本地/CI 原生跑,**模板漂移直接挡合并** |
56
+ | 事故重演 | 数据集快照(要再调模型) | **确定性重演**:录下当时的响应,重放零调用、零成本、结果恒定 |
57
+ | 依赖 | 平台 SDK + 账号 + 网络 | **唯一依赖 PyYAML** |
58
+
59
+ 一句话:**用平台看效果,用 prompt-gray-release 管上线。** 没有平台的小团队,它单独工作。
60
+
61
+ ## 项目目标
62
+
63
+ **让任何团队——无需搭建平台——把 Prompt 变更纳入与代码同级的发布纪律。**
64
+
65
+ 拆成三个可验收的子目标:
66
+
67
+ 1. **安全上线**:Prompt 变更先过评测门禁,再按流量灰度,异常秒级回滚(对应 Registry / EvalGate / RolloutController);
68
+ 2. **行为可验证**:Prompt 的每次行为变化都能被确定性重演并自动检出(对应 Recorder / Replayer / `prompt-gray check`);
69
+ 3. **过程可审计**:谁、何时、把哪个版本、放了多少流量,全程 JSONL 可追溯。
70
+
71
+ **目标用户**:没有 MLOps 平台、也不打算搭平台的 LLM 应用团队;需要私有化与审计的强合规企业;个人开发者。
72
+ **非目标**:不做模型路由、不做评测平台、不做 Web 控制台——只做「发布链路」这一层。
73
+
74
+ ## 为什么需要它
75
+
76
+ LLM 应用里,**Prompt 就是生产代码**——但它的变更方式却像在改配置:
77
+
78
+ - 改一句措辞就可能让格式崩坏、答非所问、成本飙升,而且是**全量瞬间生效**;
79
+ - 没有 diff 审查,没有回归测试,没有发布批次,出事了只能全网搜"刚才谁改的 Prompt";
80
+ - 观测平台能告诉你效果好不好,但**管不住上线那一刻**。
81
+
82
+ prompt-gray-release 补的就是"上线那一刻":**新 Prompt 先过评测门禁,再按 1% → 10% → 50% → 100% 灰度放量,出问题秒级回滚,历史事故随时确定性重演。**
83
+
84
+ ## 安装
85
+
86
+ ```bash
87
+ pip install prompt-gray-release
88
+ ```
89
+
90
+ Python >= 3.10。唯一运行时依赖 PyYAML。
91
+
92
+ ## 快速开始
93
+
94
+ 完整可运行示例见 [examples/quickstart.py](examples/quickstart.py)(零 API key,
95
+ 模拟模型 5 分钟跑通)与 [examples/openai_demo.py](examples/openai_demo.py)(真实模型全流程,
96
+ 任意 OpenAI 兼容端点)。
97
+
98
+ ### 1. 定义 Prompt 版本(prompts.yaml)
99
+
100
+ ```yaml
101
+ prompts:
102
+ support_reply:
103
+ v1:
104
+ template: |
105
+ 你好 $name,这是 v1 的回复模板。关于 $topic 的问题我们会尽快处理。
106
+ metadata:
107
+ model: gpt-4o-mini
108
+ temperature: 0.2
109
+ v2:
110
+ template: |
111
+ 你好 $name!这是 v2 的全新回复模板,专治 $topic 相关问题。
112
+ ```
113
+
114
+ ### 2. 灰度路由:同一用户永远同一版本
115
+
116
+ ```python
117
+ from prompt_gray_release import GrayRouter, PromptRegistry, RolloutController
118
+
119
+ registry = PromptRegistry.from_yaml("prompts.yaml")
120
+ rollout = RolloutController()
121
+ rollout.set_stable("support_reply", "v1")
122
+ router = GrayRouter(registry, rollout)
123
+
124
+ rollout.start_rollout("support_reply", "v2", percent=10) # 先放 10% 流量
125
+ r = router.render("support_reply", {"name": "张三", "topic": "发票"}, user_key="user-42")
126
+ print(r.version, "->", r.text) # user-42 落在哪个版本,以后就一直在哪个版本
127
+ ```
128
+
129
+ 规则:`percent >= 100` 全量走 candidate;`0 < percent < 100` 按稳定 hash 分桶;
130
+ **没有 user_key 时不猜**——非全量一律走 stable(宁可不灰度,不可不确定)。
131
+
132
+ ### 3. 评测门禁:不达标就拦截
133
+
134
+ ```python
135
+ from prompt_gray_release import EvalCase, EvalGate
136
+
137
+ gate = EvalGate(registry)
138
+ cases = [EvalCase({"name": "张三", "topic": "发票"}, "期望的标准答案……")]
139
+ report = gate.evaluate("support_reply", candidate="v2", baseline="v1", cases=cases)
140
+ print(report.summary()) # [support_reply] v2=0.000 vs 基线 v1=1.000(要求 >= +0.000)→ 拦截
141
+
142
+ if report.passed:
143
+ rollout.set_percent("support_reply", 50) # 过门禁才放量
144
+ else:
145
+ rollout.rollback("support_reply") # 秒级回滚
146
+ ```
147
+
148
+ 评测函数可插拔:内置 `exact_match` / `contains`,也可以传自定义 `evaluator(output, expected) -> float`
149
+ (包括把真实模型当裁判——见 [examples/openai_demo.py](examples/openai_demo.py))。
150
+ `gate.enforce(...)` 在不达标时直接抛 `EvalGateBlocked`(fail-closed),适合放进 CI。
151
+
152
+ ### 4. 放量、回滚与审计
153
+
154
+ ```python
155
+ rollout.set_percent("support_reply", 10) # 1% -> 10%
156
+ rollout.set_percent("support_reply", 50) # 观察指标没问题,继续放大
157
+ rollout.promote_full("support_reply") # 全量,candidate 升级为 stable
158
+ # 或发现异常:
159
+ rollout.rollback("support_reply") # 秒级回到 stable
160
+ ```
161
+
162
+ 传入 `state_path` 时,每次变更都会**原子落盘**并追加 **JSONL 审计事件**
163
+ (谁在什么时候把哪个版本放到了多少流量),满足企业合规的可追溯要求。
164
+
165
+ ### 5. 录制与确定性重演
166
+
167
+ ```python
168
+ from prompt_gray_release import Cassette, Recorder, Replayer
169
+
170
+ recorder = Recorder("traffic.jsonl")
171
+ model = recorder.wrap(real_model) # 调用即录制(输入 + 模型响应)
172
+
173
+ # 事后:确定性重演
174
+ cassette = Cassette.load("traffic.jsonl")
175
+ for item in Replayer(cassette).replay_registry(registry):
176
+ print(item.matches, item.rendered_now) # False = 当前模板与事故时行为已漂移
177
+
178
+ fake_model = Replayer(cassette).deterministic_model("support_reply", "v2")
179
+ fake_model("任意文本", {"name": "张三", "topic": "发票"}) # 永远返回当时录制的响应
180
+ ```
181
+
182
+ 确定性重演的意义:LLM 输出不可复现,但把"输入 + 当时的响应"录下来之后,
183
+ **同一批输入永远得到同一批输出**——下游解析逻辑、路由逻辑、降级逻辑的回归测试从此有了稳定靶子;
184
+ `matches=False` 则直接告诉你"事故之后有人改过模板"。
185
+
186
+ ## CLI:不写 Python 也能用
187
+
188
+ 安装后自带 `prompt-gray` 命令。运维子命令全部走唯一写入口,动作自动审计:
189
+
190
+ ```bash
191
+ prompt-gray set-stable support_reply v1 --state rollout.json
192
+ prompt-gray start-rollout support_reply v2 --percent 10 --state rollout.json
193
+ prompt-gray status support_reply --state rollout.json
194
+ prompt-gray set-percent support_reply 50 --state rollout.json
195
+ prompt-gray rollback support_reply --state rollout.json # 秒级回滚
196
+ prompt-gray promote-full support_reply --state rollout.json
197
+ ```
198
+
199
+ ## CI 重演门禁:改一行模板,CI 变红
200
+
201
+ `prompt-gray check` 把"确定性重演"变成 CI 门禁——拿录制的真实流量对当前注册表逐条重演,
202
+ 模板漂移 / 版本缺失 / 渲染失败任一出现即退出码非 0:
203
+
204
+ ```bash
205
+ prompt-gray check --cassette traffic.jsonl --registry prompts.yaml --strict
206
+ ```
207
+
208
+ `--strict` 下空 cassette 也算失败(防止"没录制所以全过"的假绿)。GitHub Actions 用法:
209
+
210
+ ```yaml
211
+ # .github/workflows/prompt-drift.yml
212
+ on: [pull_request]
213
+ jobs:
214
+ prompt-drift:
215
+ runs-on: ubuntu-latest
216
+ steps:
217
+ - uses: actions/checkout@v4
218
+ - uses: actions/setup-python@v5
219
+ with: { python-version: "3.12" }
220
+ - run: pip install prompt-gray-release
221
+ - run: prompt-gray check --cassette traffic.jsonl --registry prompts.yaml --strict
222
+ ```
223
+
224
+ 效果:**PR 里改一行模板 → CI 拦截合并;还原 → CI 恢复绿色。**
225
+ 录制文件(traffic.jsonl)跟着仓库走,门禁在本地与 CI 行为完全一致,不依赖任何外部服务。
226
+
227
+ ## 用真实模型 5 分钟跑通
228
+
229
+ ```bash
230
+ pip install openai prompt-gray-release
231
+ export OPENAI_API_KEY=sk-...
232
+ export OPENAI_BASE_URL=https://... # 可选:任意 OpenAI 兼容端点
233
+ python examples/openai_demo.py
234
+ ```
235
+
236
+ 示例覆盖:灰度路由选中候选 → 真实模型调用并录制 → 真实模型当裁判的评测门禁
237
+ (不达标自动回滚)→ 零成本确定性重演。库本身不引入 openai 依赖。
238
+
239
+ ## 集成
240
+
241
+ - FastAPI:[examples/fastapi_app.py](examples/fastapi_app.py)——`router.render` 进请求处理,
242
+ 附带回滚端点;
243
+ - pytest:[examples/test_with_cassette.py](examples/test_with_cassette.py)——用 cassette
244
+ 的确定性假模型做下游逻辑回归;
245
+ - OpenTelemetry(可选 `[otel]` extra):`traced(router)` / `traced_model(model)` 导出 span;
246
+ - 多环境:`RolloutController.for_env("rollout", "prod")` → `rollout.prod.json`,环境间状态天然隔离。
247
+
248
+ ## 设计原则
249
+
250
+ 1. **库,不是平台**:一个 `pip install` 就能嵌入现有业务,不要求你迁移到任何系统;
251
+ 2. **fail-fast / fail-closed**:缺变量即报错、评测不达标即拦截、重演找不到即抛错——发布链路上不玩默认静默;
252
+ 3. **确定性优先**:没有 user_key 不做随机灰度;重演靠录制响应而不是"再问一次模型";
253
+ 4. **审计内建**:状态每次变更原子落盘 + JSONL 事件流,合规环境可直接对接审计。
254
+
255
+ ## 路线图
256
+
257
+ 详细计划与进度见 [TODO.md](TODO.md)。
258
+
259
+ - [x] CLI:`prompt-gray status / set-stable / start-rollout / set-percent / promote-full / rollback / check`
260
+ - [x] 评测函数注册表(正则 / 关键词集合 / JSON 合法性 / JSON Schema)
261
+ - [x] 与 OpenTelemetry 对接的调用追踪导出(`traced` / `traced_model`,可选 extra)
262
+ - [x] 多环境(dev / staging / prod)状态隔离(`for_env`)
263
+
264
+ ## 开发
265
+
266
+ ```bash
267
+ pip install -e ".[dev]"
268
+ pytest # 109 个测试
269
+ ruff check src tests examples && ruff format --check src tests examples
270
+ mypy src # strict
271
+ ```
272
+
273
+ ## License
274
+
275
+ [MIT](LICENSE) © helloworldtang
@@ -0,0 +1,240 @@
1
+ # prompt-gray-release
2
+
3
+ **把 Prompt 当生产代码发布。** 轻量、可嵌入、零平台依赖的 Prompt 发布工程库:
4
+ 版本化 → 评测门禁 → 灰度放量 → 一键回滚 → 事故确定性重演。
5
+
6
+ > Prompt release engineering: versioning, canary rollout, eval gates,
7
+ > one-click rollback and deterministic incident replay. Lightweight, embeddable, no platform required.
8
+ >
9
+ > [English README](README.en.md)
10
+
11
+ ## 为什么是它:与平台方案的根本差异
12
+
13
+ LangSmith / Langfuse 是**评测与观测平台**——它们管"效果好不好",管不住**上线那一刻**。
14
+ prompt-gray-release 补的就是上线那一刻,并且不要求你搭建或迁移到任何平台:
15
+
16
+ | | LangSmith / Langfuse 等平台 | prompt-gray-release |
17
+ |---|---|---|
18
+ | 形态 | SaaS / 自托管服务,数据要出域或进它们的存储 | **纯库**,`pip install` 即用,数据不出你的进程 |
19
+ | 私有化与审计 | 自托管版要搭整套服务 | **JSONL 审计事件内建**,append-only,合规环境直接对接 |
20
+ | CI 门禁 | 依赖平台 CI 集成 | `prompt-gray check` 本地/CI 原生跑,**模板漂移直接挡合并** |
21
+ | 事故重演 | 数据集快照(要再调模型) | **确定性重演**:录下当时的响应,重放零调用、零成本、结果恒定 |
22
+ | 依赖 | 平台 SDK + 账号 + 网络 | **唯一依赖 PyYAML** |
23
+
24
+ 一句话:**用平台看效果,用 prompt-gray-release 管上线。** 没有平台的小团队,它单独工作。
25
+
26
+ ## 项目目标
27
+
28
+ **让任何团队——无需搭建平台——把 Prompt 变更纳入与代码同级的发布纪律。**
29
+
30
+ 拆成三个可验收的子目标:
31
+
32
+ 1. **安全上线**:Prompt 变更先过评测门禁,再按流量灰度,异常秒级回滚(对应 Registry / EvalGate / RolloutController);
33
+ 2. **行为可验证**:Prompt 的每次行为变化都能被确定性重演并自动检出(对应 Recorder / Replayer / `prompt-gray check`);
34
+ 3. **过程可审计**:谁、何时、把哪个版本、放了多少流量,全程 JSONL 可追溯。
35
+
36
+ **目标用户**:没有 MLOps 平台、也不打算搭平台的 LLM 应用团队;需要私有化与审计的强合规企业;个人开发者。
37
+ **非目标**:不做模型路由、不做评测平台、不做 Web 控制台——只做「发布链路」这一层。
38
+
39
+ ## 为什么需要它
40
+
41
+ LLM 应用里,**Prompt 就是生产代码**——但它的变更方式却像在改配置:
42
+
43
+ - 改一句措辞就可能让格式崩坏、答非所问、成本飙升,而且是**全量瞬间生效**;
44
+ - 没有 diff 审查,没有回归测试,没有发布批次,出事了只能全网搜"刚才谁改的 Prompt";
45
+ - 观测平台能告诉你效果好不好,但**管不住上线那一刻**。
46
+
47
+ prompt-gray-release 补的就是"上线那一刻":**新 Prompt 先过评测门禁,再按 1% → 10% → 50% → 100% 灰度放量,出问题秒级回滚,历史事故随时确定性重演。**
48
+
49
+ ## 安装
50
+
51
+ ```bash
52
+ pip install prompt-gray-release
53
+ ```
54
+
55
+ Python >= 3.10。唯一运行时依赖 PyYAML。
56
+
57
+ ## 快速开始
58
+
59
+ 完整可运行示例见 [examples/quickstart.py](examples/quickstart.py)(零 API key,
60
+ 模拟模型 5 分钟跑通)与 [examples/openai_demo.py](examples/openai_demo.py)(真实模型全流程,
61
+ 任意 OpenAI 兼容端点)。
62
+
63
+ ### 1. 定义 Prompt 版本(prompts.yaml)
64
+
65
+ ```yaml
66
+ prompts:
67
+ support_reply:
68
+ v1:
69
+ template: |
70
+ 你好 $name,这是 v1 的回复模板。关于 $topic 的问题我们会尽快处理。
71
+ metadata:
72
+ model: gpt-4o-mini
73
+ temperature: 0.2
74
+ v2:
75
+ template: |
76
+ 你好 $name!这是 v2 的全新回复模板,专治 $topic 相关问题。
77
+ ```
78
+
79
+ ### 2. 灰度路由:同一用户永远同一版本
80
+
81
+ ```python
82
+ from prompt_gray_release import GrayRouter, PromptRegistry, RolloutController
83
+
84
+ registry = PromptRegistry.from_yaml("prompts.yaml")
85
+ rollout = RolloutController()
86
+ rollout.set_stable("support_reply", "v1")
87
+ router = GrayRouter(registry, rollout)
88
+
89
+ rollout.start_rollout("support_reply", "v2", percent=10) # 先放 10% 流量
90
+ r = router.render("support_reply", {"name": "张三", "topic": "发票"}, user_key="user-42")
91
+ print(r.version, "->", r.text) # user-42 落在哪个版本,以后就一直在哪个版本
92
+ ```
93
+
94
+ 规则:`percent >= 100` 全量走 candidate;`0 < percent < 100` 按稳定 hash 分桶;
95
+ **没有 user_key 时不猜**——非全量一律走 stable(宁可不灰度,不可不确定)。
96
+
97
+ ### 3. 评测门禁:不达标就拦截
98
+
99
+ ```python
100
+ from prompt_gray_release import EvalCase, EvalGate
101
+
102
+ gate = EvalGate(registry)
103
+ cases = [EvalCase({"name": "张三", "topic": "发票"}, "期望的标准答案……")]
104
+ report = gate.evaluate("support_reply", candidate="v2", baseline="v1", cases=cases)
105
+ print(report.summary()) # [support_reply] v2=0.000 vs 基线 v1=1.000(要求 >= +0.000)→ 拦截
106
+
107
+ if report.passed:
108
+ rollout.set_percent("support_reply", 50) # 过门禁才放量
109
+ else:
110
+ rollout.rollback("support_reply") # 秒级回滚
111
+ ```
112
+
113
+ 评测函数可插拔:内置 `exact_match` / `contains`,也可以传自定义 `evaluator(output, expected) -> float`
114
+ (包括把真实模型当裁判——见 [examples/openai_demo.py](examples/openai_demo.py))。
115
+ `gate.enforce(...)` 在不达标时直接抛 `EvalGateBlocked`(fail-closed),适合放进 CI。
116
+
117
+ ### 4. 放量、回滚与审计
118
+
119
+ ```python
120
+ rollout.set_percent("support_reply", 10) # 1% -> 10%
121
+ rollout.set_percent("support_reply", 50) # 观察指标没问题,继续放大
122
+ rollout.promote_full("support_reply") # 全量,candidate 升级为 stable
123
+ # 或发现异常:
124
+ rollout.rollback("support_reply") # 秒级回到 stable
125
+ ```
126
+
127
+ 传入 `state_path` 时,每次变更都会**原子落盘**并追加 **JSONL 审计事件**
128
+ (谁在什么时候把哪个版本放到了多少流量),满足企业合规的可追溯要求。
129
+
130
+ ### 5. 录制与确定性重演
131
+
132
+ ```python
133
+ from prompt_gray_release import Cassette, Recorder, Replayer
134
+
135
+ recorder = Recorder("traffic.jsonl")
136
+ model = recorder.wrap(real_model) # 调用即录制(输入 + 模型响应)
137
+
138
+ # 事后:确定性重演
139
+ cassette = Cassette.load("traffic.jsonl")
140
+ for item in Replayer(cassette).replay_registry(registry):
141
+ print(item.matches, item.rendered_now) # False = 当前模板与事故时行为已漂移
142
+
143
+ fake_model = Replayer(cassette).deterministic_model("support_reply", "v2")
144
+ fake_model("任意文本", {"name": "张三", "topic": "发票"}) # 永远返回当时录制的响应
145
+ ```
146
+
147
+ 确定性重演的意义:LLM 输出不可复现,但把"输入 + 当时的响应"录下来之后,
148
+ **同一批输入永远得到同一批输出**——下游解析逻辑、路由逻辑、降级逻辑的回归测试从此有了稳定靶子;
149
+ `matches=False` 则直接告诉你"事故之后有人改过模板"。
150
+
151
+ ## CLI:不写 Python 也能用
152
+
153
+ 安装后自带 `prompt-gray` 命令。运维子命令全部走唯一写入口,动作自动审计:
154
+
155
+ ```bash
156
+ prompt-gray set-stable support_reply v1 --state rollout.json
157
+ prompt-gray start-rollout support_reply v2 --percent 10 --state rollout.json
158
+ prompt-gray status support_reply --state rollout.json
159
+ prompt-gray set-percent support_reply 50 --state rollout.json
160
+ prompt-gray rollback support_reply --state rollout.json # 秒级回滚
161
+ prompt-gray promote-full support_reply --state rollout.json
162
+ ```
163
+
164
+ ## CI 重演门禁:改一行模板,CI 变红
165
+
166
+ `prompt-gray check` 把"确定性重演"变成 CI 门禁——拿录制的真实流量对当前注册表逐条重演,
167
+ 模板漂移 / 版本缺失 / 渲染失败任一出现即退出码非 0:
168
+
169
+ ```bash
170
+ prompt-gray check --cassette traffic.jsonl --registry prompts.yaml --strict
171
+ ```
172
+
173
+ `--strict` 下空 cassette 也算失败(防止"没录制所以全过"的假绿)。GitHub Actions 用法:
174
+
175
+ ```yaml
176
+ # .github/workflows/prompt-drift.yml
177
+ on: [pull_request]
178
+ jobs:
179
+ prompt-drift:
180
+ runs-on: ubuntu-latest
181
+ steps:
182
+ - uses: actions/checkout@v4
183
+ - uses: actions/setup-python@v5
184
+ with: { python-version: "3.12" }
185
+ - run: pip install prompt-gray-release
186
+ - run: prompt-gray check --cassette traffic.jsonl --registry prompts.yaml --strict
187
+ ```
188
+
189
+ 效果:**PR 里改一行模板 → CI 拦截合并;还原 → CI 恢复绿色。**
190
+ 录制文件(traffic.jsonl)跟着仓库走,门禁在本地与 CI 行为完全一致,不依赖任何外部服务。
191
+
192
+ ## 用真实模型 5 分钟跑通
193
+
194
+ ```bash
195
+ pip install openai prompt-gray-release
196
+ export OPENAI_API_KEY=sk-...
197
+ export OPENAI_BASE_URL=https://... # 可选:任意 OpenAI 兼容端点
198
+ python examples/openai_demo.py
199
+ ```
200
+
201
+ 示例覆盖:灰度路由选中候选 → 真实模型调用并录制 → 真实模型当裁判的评测门禁
202
+ (不达标自动回滚)→ 零成本确定性重演。库本身不引入 openai 依赖。
203
+
204
+ ## 集成
205
+
206
+ - FastAPI:[examples/fastapi_app.py](examples/fastapi_app.py)——`router.render` 进请求处理,
207
+ 附带回滚端点;
208
+ - pytest:[examples/test_with_cassette.py](examples/test_with_cassette.py)——用 cassette
209
+ 的确定性假模型做下游逻辑回归;
210
+ - OpenTelemetry(可选 `[otel]` extra):`traced(router)` / `traced_model(model)` 导出 span;
211
+ - 多环境:`RolloutController.for_env("rollout", "prod")` → `rollout.prod.json`,环境间状态天然隔离。
212
+
213
+ ## 设计原则
214
+
215
+ 1. **库,不是平台**:一个 `pip install` 就能嵌入现有业务,不要求你迁移到任何系统;
216
+ 2. **fail-fast / fail-closed**:缺变量即报错、评测不达标即拦截、重演找不到即抛错——发布链路上不玩默认静默;
217
+ 3. **确定性优先**:没有 user_key 不做随机灰度;重演靠录制响应而不是"再问一次模型";
218
+ 4. **审计内建**:状态每次变更原子落盘 + JSONL 事件流,合规环境可直接对接审计。
219
+
220
+ ## 路线图
221
+
222
+ 详细计划与进度见 [TODO.md](TODO.md)。
223
+
224
+ - [x] CLI:`prompt-gray status / set-stable / start-rollout / set-percent / promote-full / rollback / check`
225
+ - [x] 评测函数注册表(正则 / 关键词集合 / JSON 合法性 / JSON Schema)
226
+ - [x] 与 OpenTelemetry 对接的调用追踪导出(`traced` / `traced_model`,可选 extra)
227
+ - [x] 多环境(dev / staging / prod)状态隔离(`for_env`)
228
+
229
+ ## 开发
230
+
231
+ ```bash
232
+ pip install -e ".[dev]"
233
+ pytest # 109 个测试
234
+ ruff check src tests examples && ruff format --check src tests examples
235
+ mypy src # strict
236
+ ```
237
+
238
+ ## License
239
+
240
+ [MIT](LICENSE) © helloworldtang
@@ -0,0 +1,70 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "prompt-gray-release"
7
+ version = "0.2.0"
8
+ description = "把 Prompt 当生产代码发布:版本化、灰度放量、评测门禁、一键回滚、事故重演。Prompt release engineering: versioning, canary rollout, eval gates, rollback and incident replay."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "helloworldtang" }]
13
+ keywords = ["prompt", "llm", "gray-release", "canary", "prompt-engineering", "llmops", "evaluation"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Software Development :: Libraries",
22
+ "Topic :: Text Processing",
23
+ ]
24
+ dependencies = ["PyYAML>=6.0"]
25
+
26
+ [project.optional-dependencies]
27
+ schema = ["jsonschema>=4.0"]
28
+ otel = ["opentelemetry-api>=1.20"]
29
+ dev = [
30
+ "pytest>=7.0",
31
+ "ruff>=0.6",
32
+ "mypy>=1.10",
33
+ "types-PyYAML",
34
+ "jsonschema>=4.0",
35
+ "build",
36
+ "twine",
37
+ ]
38
+
39
+ [tool.ruff]
40
+ target-version = "py310"
41
+ line-length = 100
42
+ src = ["src", "tests"]
43
+
44
+ [tool.ruff.lint]
45
+ select = ["E", "W", "F", "I", "B", "C4", "UP", "SIM"]
46
+
47
+ [tool.mypy]
48
+ strict = true
49
+ python_version = "3.10"
50
+
51
+ [[tool.mypy.overrides]]
52
+ module = ["jsonschema", "jsonschema.exceptions", "opentelemetry", "opentelemetry.*"]
53
+ ignore_missing_imports = true
54
+
55
+ [project.scripts]
56
+ prompt-gray = "prompt_gray_release.cli:main"
57
+
58
+ [project.urls]
59
+ Homepage = "https://github.com/helloworldtang/prompt-gray-release"
60
+ Issues = "https://github.com/helloworldtang/prompt-gray-release/issues"
61
+
62
+ [tool.setuptools.packages.find]
63
+ where = ["src"]
64
+
65
+ [tool.setuptools.package-data]
66
+ prompt_gray_release = ["py.typed"]
67
+
68
+ [tool.pytest.ini_options]
69
+ testpaths = ["tests"]
70
+ addopts = "-q"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,67 @@
1
+ """prompt-gray-release: 把 Prompt 当生产代码发布。
2
+
3
+ 版本化、灰度放量、评测门禁、一键回滚、事故确定性重演。
4
+ """
5
+
6
+ from .envs import state_path_for_env
7
+ from .evaluators import json_schema, json_valid, keywords, regex
8
+ from .exceptions import (
9
+ EvalGateBlocked,
10
+ PromptGrayReleaseError,
11
+ PromptNotFoundError,
12
+ ReplayMissError,
13
+ RolloutStateError,
14
+ TemplateRenderError,
15
+ VersionNotFoundError,
16
+ )
17
+ from .gate import EvalCase, EvalGate, EvalReport, contains, exact_match
18
+ from .recorder import CallRecord, Cassette, Recorder, Replayer
19
+ from .registry import PromptRegistry, PromptVersion
20
+ from .render import render_template
21
+ from .rollout import RolloutController, RolloutState
22
+ from .router import GrayRouter, RenderedPrompt
23
+ from .tracing import traced, traced_model
24
+
25
+ __version__ = "0.2.0"
26
+
27
+ __all__ = [
28
+ "__version__",
29
+ # registry
30
+ "PromptRegistry",
31
+ "PromptVersion",
32
+ # render
33
+ "render_template",
34
+ # rollout / router
35
+ "RolloutController",
36
+ "RolloutState",
37
+ "GrayRouter",
38
+ "RenderedPrompt",
39
+ # gate
40
+ "EvalGate",
41
+ "EvalCase",
42
+ "EvalReport",
43
+ "exact_match",
44
+ "contains",
45
+ # evaluators
46
+ "regex",
47
+ "keywords",
48
+ "json_valid",
49
+ "json_schema",
50
+ # envs / tracing
51
+ "state_path_for_env",
52
+ "traced",
53
+ "traced_model",
54
+ # recorder / replay
55
+ "Recorder",
56
+ "Cassette",
57
+ "CallRecord",
58
+ "Replayer",
59
+ # exceptions
60
+ "PromptGrayReleaseError",
61
+ "PromptNotFoundError",
62
+ "VersionNotFoundError",
63
+ "TemplateRenderError",
64
+ "RolloutStateError",
65
+ "EvalGateBlocked",
66
+ "ReplayMissError",
67
+ ]