Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/scenarioforge/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
HY3_BASE_URL=https://tokenhub-intl.tencentcloudmaas.com/v1
HY3_API_KEY=replace-me
HY3_MODEL=hy3
HY3_TIMEOUT_SECONDS=120
# Use only for the two bundled, visibly labelled offline walkthroughs.
# SCENARIOFORGE_DEMO_MODE=1
11 changes: 11 additions & 0 deletions apps/scenarioforge/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.env
.venv/
.pytest_cache/
.ruff_cache/
.playwright-cli/
output/
build/
dist/
*.egg-info/
__pycache__/
*.py[cod]
143 changes: 143 additions & 0 deletions apps/scenarioforge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# ScenarioForge:Hy3 决策演练台

> 在现实发生前,先让计划经历一次可控的坏结局。

ScenarioForge 是一个由 **Tencent Hy3** 驱动的本地 Web 应用。用户输入待执行方案、成功目标与不可违反的约束;Hy3 先从多个利益相关者视角寻找计划薄弱点,再构造反事实情景,最后把风险收敛为可核验的执行门禁、未来 48 小时动作和止损条件。

![ScenarioForge 首页](docs/demo/scenarioforge-home.png)

## 54 秒交互演示

下方 GIF 连续展示两条端到端界面流程:校园公益夜市与企业账单引擎发布。录制时使用的是仓库内置离线夹具,页面右上角和结果元数据均明确显示“非 Hy3 输出”;它用于验证交互与展示链路,不能冒充真实模型证据。

![ScenarioForge 两条端到端演示](docs/demo/scenarioforge-demo.gif)

## Hy3 在系统中的角色

应用的语义判断全部通过 Hy3 的 OpenAI-compatible Chat Completions API 完成,不训练、不微调、不在本地部署模型:

1. **约束建模**:从自然语言计划中提取目标、不可妥协项和待验证假设。
2. **多视角质询**:以安全、运营、客户等相关角色检查同一计划,并引用计划内证据。
3. **反事实生成**:构造触发条件、早期信号、影响和响应均完整的失败情景。
4. **决策收敛**:给出 `GO / CONDITIONAL_GO / NO_GO`,并生成有条件、负责人、期限和退路的执行门禁。

程序本身只负责输入边界、提示词隔离、JSON 契约校验、重试、错误脱敏、稳定摘要和页面展示。它不会用本地规则替代 Hy3 做语义决策。

```mermaid
flowchart LR
A["计划 + 目标 + 约束"] --> B["Hy3 调用 1:分析"]
B --> C["本地严格契约校验"]
C --> D["Hy3 调用 2:决策"]
D --> E["本地严格契约校验"]
E --> F["可审计演练报告"]
```

## 两条内置流程

| 流程 | 计划 | 演练重点 | 离线夹具结论 |
|---|---|---|---|
| 01 | 雨季校园公益夜市 | 降雨、电气、食品安全、消防通道、志愿者排班 | 有条件执行 |
| 02 | 企业账单引擎周五发布 | 不可逆迁移、重复账单、观察窗口、回填与对账 | 暂停执行 |

真实模式允许直接编辑任意计划。离线模式只接受两条原样内置流程;一旦编辑,服务端会拒绝请求并提示切换真实 Hy3,避免把夹具包装成模型结果。

## 快速启动

要求 Python 3.10+,运行时无第三方依赖。

### 真实 Hy3 模式

先在 TokenHub 创建 API Key,然后:

```bash
cd apps/scenarioforge
export HY3_BASE_URL=https://tokenhub-intl.tencentcloudmaas.com/v1
export HY3_API_KEY='your-tokenhub-key'
export HY3_MODEL=hy3
python3 -m scenarioforge.server
```

打开 <http://127.0.0.1:8787>。密钥只保留在服务端环境变量中,不会发送给浏览器或写入日志。

也支持任意部署了 Hy3 的 OpenAI-compatible 地址:

```bash
export HY3_BASE_URL=http://127.0.0.1:8000/v1
export HY3_API_KEY=EMPTY
export HY3_MODEL=hy3
python3 -m scenarioforge.server
```

### 离线界面演示

```bash
cd apps/scenarioforge
SCENARIOFORGE_DEMO_MODE=1 python3 -m scenarioforge.server
```

离线模式会在页面上持续显示橙色标识,结果元数据固定为 `api calls: 0`。

## 配置

| 变量 | 默认值 | 说明 |
|---|---|---|
| `HY3_BASE_URL` | `https://tokenhub-intl.tencentcloudmaas.com/v1` | OpenAI-compatible API 根地址 |
| `HY3_API_KEY` | 空 | 真实模式必填;本地无鉴权服务可用 `EMPTY` |
| `HY3_MODEL` | `hy3` | API 模型名 |
| `HY3_TIMEOUT_SECONDS` | `120` | 单次 API 超时 |
| `SCENARIOFORGE_DEMO_MODE` | 空 | 设为 `1/true/yes` 才启用离线夹具 |

可复制 [`.env.example`](.env.example) 查看配置,但标准库服务器不会自动读取 `.env`,避免配置来源不透明。

## 验证

```bash
cd apps/scenarioforge
python3 -m unittest discover -s tests -v
python3 -m compileall -q scenarioforge tests
node --check scenarioforge/static/app.js
ruff check .
```

当前提交的本机结果:

- Python 单元/集成测试:**20/20 passed**。
- 两条浏览器流程:`POST /api/rehearse` 均为 **HTTP 200**。
- 浏览器控制台:**0 error / 0 warning**。
- 演示 GIF:**54.67 秒**,低于 2 分钟要求。
- 当前机器未提供 `HY3_API_KEY` 或可达的自托管 Hy3,因此没有把离线输出标成在线推理证据。详见 [验证台账](docs/VALIDATION.md)。

## 安全与可靠性边界

- API Key 不进入前端、导出文件或错误详情。
- 用户计划用 XML 风格边界包裹,并在系统提示中明确作为不可信数据处理。
- 请求体限制为 32 KiB,字段和列表都有长度上限。
- Hy3 两阶段输出分别执行严格字段、数量和枚举校验;坏响应不会直接渲染。
- 只对超时、限流和服务端错误重试一次;鉴权错误不重试。
- 前端通过 `textContent` / DOM 节点渲染模型文本,不把输出作为 HTML 注入。
- 静态响应包含 CSP 与 `X-Content-Type-Options: nosniff`。

## 项目结构

```text
apps/scenarioforge/
├── scenarioforge/
│ ├── config.py # 环境配置与校验
│ ├── hy3.py # Hy3 OpenAI-compatible 客户端
│ ├── prompts.py # 两阶段、可版本化提示词
│ ├── models.py # 输入与模型输出契约
│ ├── service.py # 两次 Hy3 调用编排
│ ├── examples.py # 两条明确标注的离线夹具
│ ├── server.py # 同源 HTTP API 与静态服务器
│ └── static/ # 原生 HTML/CSS/JS 交互前端
├── tests/ # 单元、协议、HTTP 端到端测试
└── docs/demo/ # 54 秒 GIF 与浏览器截图
```

## AI 协作说明

本应用的产品构思、后端、前端、测试、文档和浏览器验证由 **Codex** 协助完成。没有使用 CodeBuddy 生成代码,因此没有虚构 CodeBuddy 贡献记录。Hy3 是应用运行时的唯一语义模型。

## License

Apache License 2.0,与 Hy3 仓库一致。
60 changes: 60 additions & 0 deletions apps/scenarioforge/README_EN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# ScenarioForge: a Hy3-powered decision rehearsal workspace

ScenarioForge stress-tests an execution plan before reality does. It sends the plan, objective,
and non-negotiable constraints to Tencent Hy3 in two structured API calls: the first extracts
assumptions, stakeholder concerns, and counterfactual scenarios; the second converts those risks
into a verdict, owned release gates, immediate actions, and measurable stop conditions.

The application never trains, fine-tunes, or locally runs a model. All semantic decisions use the
OpenAI-compatible Hy3 API. Deterministic code handles input limits, prompt isolation, contract
validation, retries, redacted failures, and rendering.

![ScenarioForge home](docs/demo/scenarioforge-home.png)

## Run with Hy3

Python 3.10+ is required; runtime code has no third-party dependencies.

```bash
cd apps/scenarioforge
export HY3_BASE_URL=https://tokenhub-intl.tencentcloudmaas.com/v1
export HY3_API_KEY='your-tokenhub-key'
export HY3_MODEL=hy3
python3 -m scenarioforge.server
```

Open <http://127.0.0.1:8787>. The API key remains server-side.

To inspect the UI without credentials, run the explicitly labelled fixture mode:

```bash
SCENARIOFORGE_DEMO_MODE=1 python3 -m scenarioforge.server
```

Fixture mode accepts only the two unchanged bundled examples. Edited input is rejected until live
Hy3 mode is enabled, and every fixture report says `api calls: 0`.

## Demo and verification

The [54-second GIF](docs/demo/scenarioforge-demo.gif) covers both bundled browser flows. It is an
offline UI walkthrough, not live-model evidence. The local verification run passed 20/20 Python
tests, returned HTTP 200 for both browser flows, and produced no browser console errors or warnings.
The machine used for this contribution did not have a Hy3 API credential, so no live inference is
claimed. See [the verification ledger](docs/VALIDATION.md).

## Hy3 responsibilities

- extract objectives, hard constraints, and unsupported assumptions;
- challenge the plan from three to five stakeholder perspectives with plan-grounded evidence;
- generate concrete counterfactual scenarios with triggers, early signals, impact, and response;
- produce `GO`, `CONDITIONAL_GO`, or `NO_GO` plus owned gates and stop conditions.

## AI collaboration

Codex assisted with the product design, implementation, tests, documentation, and browser
verification. CodeBuddy was not used, so no CodeBuddy-authored blocks are claimed. Hy3 is the only
runtime semantic model.

## License

Apache License 2.0.
45 changes: 45 additions & 0 deletions apps/scenarioforge/docs/VALIDATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# ScenarioForge verification ledger

Date: 2026-07-23 (Asia/Shanghai)

## Automated checks

| Check | Result |
|---|---|
| `python3 -m unittest discover -s tests -v` | 20/20 passed |
| `python3 -m compileall -q scenarioforge tests` | passed |
| `node --check scenarioforge/static/app.js` | passed |
| `ruff check .` | passed |
| `python3 -m build` | wheel and sdist built successfully |

The test suite covers environment parsing, secret-safe provider failures, OpenAI-compatible request
shape, fenced JSON parsing, input limits, output schemas, two-stage orchestration, CSP headers,
fixture integrity, edited-fixture rejection, and both complete HTTP flows.

## Browser run

Browser: Chromium controlled through Playwright CLI, viewport 1440 × 1000.

| Flow | API result | UI result |
|---|---|---|
| Rainy campus charity night market | `POST /api/rehearse` → 200 | conditional-go report rendered |
| Enterprise billing engine release | `POST /api/rehearse` → 200 | no-go report rendered |

Browser console after both runs: **0 errors, 0 warnings**.

Artifacts:

- `docs/demo/scenarioforge-demo.gif` — 54.67 seconds, 960 × 720, both flows.
- `docs/demo/scenarioforge-home.png` — input workspace.
- `docs/demo/scenarioforge-report.png` — rendered billing report.

## Live Hy3 status

The machine did not expose `HY3_API_KEY` and did not have a reachable self-hosted Hy3 endpoint.
Therefore, the two recorded browser flows used the explicitly labelled offline fixtures. The live
request path is covered by a protocol-level fake transport, but that is not equivalent to a real
Hy3 inference. No README text, UI metadata, or PR claim should describe the fixture output as live.

To produce live evidence, configure credentials and rerun both examples without
`SCENARIOFORGE_DEMO_MODE`; a valid result reports `mode: live`, `provider.name: Hy3`, `calls: 2`,
real model metadata, request IDs, and token usage when provided by the endpoint.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions apps/scenarioforge/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[build-system]
requires = ["setuptools>=69"]
build-backend = "setuptools.build_meta"

[project]
name = "hy3-scenarioforge"
version = "0.1.0"
description = "A Hy3-powered plan stress-testing and decision rehearsal workspace"
readme = "README.md"
requires-python = ">=3.10"
license = "Apache-2.0"
authors = [{ name = "dujujin" }]
dependencies = []

[project.scripts]
scenarioforge = "scenarioforge.server:main"

[tool.setuptools]
packages = ["scenarioforge", "scenarioforge.static"]

[tool.setuptools.package-data]
scenarioforge = ["static/*"]

[tool.ruff]
line-length = 100
target-version = "py310"

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM"]

[tool.pytest.ini_options]
testpaths = ["tests"]
3 changes: 3 additions & 0 deletions apps/scenarioforge/scenarioforge/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""ScenarioForge: rehearse decisions with Hy3 before reality does."""

__version__ = "0.1.0"
52 changes: 52 additions & 0 deletions apps/scenarioforge/scenarioforge/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Runtime configuration with safe defaults for local Hy3 deployments."""

from __future__ import annotations

import os
from dataclasses import dataclass
from urllib.parse import urlsplit


@dataclass(frozen=True)
class Settings:
base_url: str
api_key: str
model: str
timeout_seconds: float
demo_mode: bool

@classmethod
def from_env(cls) -> Settings:
raw_timeout = os.getenv("HY3_TIMEOUT_SECONDS", "120")
try:
timeout = float(raw_timeout)
except ValueError as error:
raise ValueError("HY3_TIMEOUT_SECONDS must be a number") from error

settings = cls(
base_url=os.getenv(
"HY3_BASE_URL", "https://tokenhub-intl.tencentcloudmaas.com/v1"
).rstrip("/"),
api_key=os.getenv("HY3_API_KEY", ""),
model=os.getenv("HY3_MODEL", "hy3"),
timeout_seconds=timeout,
demo_mode=os.getenv("SCENARIOFORGE_DEMO_MODE", "").lower()
in {"1", "true", "yes"},
)
settings.validate()
return settings

@property
def live_ready(self) -> bool:
return bool(self.api_key)

def validate(self) -> None:
parsed = urlsplit(self.base_url)
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise ValueError("HY3_BASE_URL must be an absolute HTTP(S) URL")
if parsed.username or parsed.password:
raise ValueError("HY3_BASE_URL must not contain credentials")
if not self.model.strip():
raise ValueError("HY3_MODEL must not be empty")
if self.timeout_seconds <= 0:
raise ValueError("HY3_TIMEOUT_SECONDS must be greater than zero")
Loading