Upload folder using huggingface_hub
Browse files- .gitignore +35 -0
- README.md +1 -1
- configs/case_builder_config.json +11 -0
- configs/case_builder_config_README.md +151 -0
- data_storage/users/demo_anomaly.jsonl +0 -0
- data_storage/users/demo_pattern.jsonl +0 -0
- data_storage/users/sample_user.jsonl +0 -0
- demo_patchad_cases/demo_anomaly_official.json +41 -6
- demo_patchad_cases/demo_anomaly_platform.json +36 -1
- demo_patchad_cases/demo_pattern_official.json +41 -6
- demo_patchad_cases/demo_pattern_platform.json +36 -1
- demo_patchad_cases/demo_pattern_test_official.json +127 -0
- demo_patchad_cases/manifest.json +9 -19
- gradio_app.py +0 -0
- requirements.txt +1 -0
- simulate_patchad_case_pipeline.py +18 -13
- utils/case_builder.py +137 -5
- 流程说明_修正版.md +102 -0
.gitignore
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
|
| 8 |
+
# Environment
|
| 9 |
+
.env
|
| 10 |
+
.venv
|
| 11 |
+
env/
|
| 12 |
+
venv/
|
| 13 |
+
|
| 14 |
+
# IDE
|
| 15 |
+
.vscode/
|
| 16 |
+
.idea/
|
| 17 |
+
*.swp
|
| 18 |
+
*.swo
|
| 19 |
+
|
| 20 |
+
# OS
|
| 21 |
+
.DS_Store
|
| 22 |
+
Thumbs.db
|
| 23 |
+
|
| 24 |
+
# Gradio
|
| 25 |
+
flagged/
|
| 26 |
+
gradio_cached_examples/
|
| 27 |
+
|
| 28 |
+
# Large files (if using Git LFS)
|
| 29 |
+
*.pt
|
| 30 |
+
*.pth
|
| 31 |
+
*.ckpt
|
| 32 |
+
|
| 33 |
+
# Logs
|
| 34 |
+
*.log
|
| 35 |
+
|
README.md
CHANGED
|
@@ -4,7 +4,7 @@ emoji: 📟
|
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: "4.
|
| 8 |
app_file: gradio_app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
|
|
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: "4.44.0"
|
| 8 |
app_file: gradio_app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
configs/case_builder_config.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"main_model_confirmation": {
|
| 3 |
+
"enabled": true,
|
| 4 |
+
"mode": "auto",
|
| 5 |
+
"model_dir": "checkpoints/phase2/exp_factor_balanced",
|
| 6 |
+
"device": "cpu",
|
| 7 |
+
"threshold": null,
|
| 8 |
+
"min_duration_days": 3
|
| 9 |
+
}
|
| 10 |
+
}
|
| 11 |
+
|
configs/case_builder_config_README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CaseBuilder 配置说明
|
| 2 |
+
|
| 3 |
+
## 配置文件位置
|
| 4 |
+
|
| 5 |
+
`configs/case_builder_config.json`
|
| 6 |
+
|
| 7 |
+
## 配置项说明
|
| 8 |
+
|
| 9 |
+
```json
|
| 10 |
+
{
|
| 11 |
+
"main_model_confirmation": {
|
| 12 |
+
"enabled": false, // 是否启用主时序模型确认
|
| 13 |
+
"mode": "auto", // 模式:auto(自动)或 manual(手工)
|
| 14 |
+
"model_dir": "checkpoints/phase2/exp_factor_balanced", // 主时序模型路径
|
| 15 |
+
"device": "cpu", // 设备:cpu 或 cuda
|
| 16 |
+
"threshold": null, // 异常阈值(null 表示使用模型默认值)
|
| 17 |
+
"min_duration_days": 3 // 最小持续天数(用于 detect_pattern)
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
## 使用场景
|
| 23 |
+
|
| 24 |
+
### 场景1:自动模式(推荐)
|
| 25 |
+
|
| 26 |
+
**配置:**
|
| 27 |
+
```json
|
| 28 |
+
{
|
| 29 |
+
"main_model_confirmation": {
|
| 30 |
+
"enabled": true,
|
| 31 |
+
"mode": "auto",
|
| 32 |
+
"model_dir": "checkpoints/phase2/exp_factor_balanced"
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
**代码:**
|
| 38 |
+
```python
|
| 39 |
+
from utils.case_builder import CaseBuilder
|
| 40 |
+
|
| 41 |
+
builder = CaseBuilder() # 自动从配置文件加载
|
| 42 |
+
result = builder.build_case(payload)
|
| 43 |
+
|
| 44 |
+
# 检查主时序模型确认结果
|
| 45 |
+
if result.get("risk_confirmed") is False:
|
| 46 |
+
print("主时序模型确认无风险,终止流程")
|
| 47 |
+
return
|
| 48 |
+
|
| 49 |
+
# 继续生成 LLM 输入
|
| 50 |
+
llm_input = result["llm_input"]
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
**行为:**
|
| 54 |
+
- CaseBuilder 内部自动调用主时序模型确认风险
|
| 55 |
+
- 返回 `risk_confirmed: true/false`
|
| 56 |
+
- 外部可以根据结果决定是否继续
|
| 57 |
+
|
| 58 |
+
### 场景2:手工模式
|
| 59 |
+
|
| 60 |
+
**配置:**
|
| 61 |
+
```json
|
| 62 |
+
{
|
| 63 |
+
"main_model_confirmation": {
|
| 64 |
+
"enabled": false
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
**代码:**
|
| 70 |
+
```python
|
| 71 |
+
from utils.case_builder import CaseBuilder
|
| 72 |
+
from wearable_anomaly_detector import WearableAnomalyDetector
|
| 73 |
+
|
| 74 |
+
builder = CaseBuilder() # 只用 PatchAD,不调用主时序模型
|
| 75 |
+
result = builder.build_case(payload)
|
| 76 |
+
|
| 77 |
+
# 外部自己调用主时序模型确认
|
| 78 |
+
detector = WearableAnomalyDetector(model_dir="...")
|
| 79 |
+
all_data_points = []
|
| 80 |
+
for item in payload["history_windows"]:
|
| 81 |
+
all_data_points.extend(item["window"])
|
| 82 |
+
|
| 83 |
+
pattern_result = detector.detect_pattern(all_data_points, days=len(payload["history_windows"]))
|
| 84 |
+
risk_confirmed = pattern_result.get('anomaly_pattern', {}).get('has_pattern', False)
|
| 85 |
+
|
| 86 |
+
if not risk_confirmed:
|
| 87 |
+
print("主时序模型确认无风险,终止流程")
|
| 88 |
+
return
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
**行为:**
|
| 92 |
+
- CaseBuilder 只用 PatchAD 分数,不调用主时序模型
|
| 93 |
+
- 外部可以自己调用 `WearableAnomalyDetector.detect_pattern()` 来确认风险
|
| 94 |
+
|
| 95 |
+
### 场景3:外部传入检测器(复用已加载的模型)
|
| 96 |
+
|
| 97 |
+
**代码:**
|
| 98 |
+
```python
|
| 99 |
+
from utils.case_builder import CaseBuilder
|
| 100 |
+
from wearable_anomaly_detector import WearableAnomalyDetector
|
| 101 |
+
|
| 102 |
+
# 外部已加载的检测器
|
| 103 |
+
detector = WearableAnomalyDetector(model_dir="...")
|
| 104 |
+
|
| 105 |
+
# 传入检测器实例,避免重复加载
|
| 106 |
+
builder = CaseBuilder(detector=detector)
|
| 107 |
+
result = builder.build_case(payload)
|
| 108 |
+
|
| 109 |
+
# 检查确认结果
|
| 110 |
+
if result.get("risk_confirmed") is False:
|
| 111 |
+
print("主时序模型确认无风险,终止流程")
|
| 112 |
+
return
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
**行为:**
|
| 116 |
+
- CaseBuilder 使用外部传入的检测器
|
| 117 |
+
- 如果配置 `enabled=true`,会自动调用确认
|
| 118 |
+
- 避免重复加载模型,提高性能
|
| 119 |
+
|
| 120 |
+
## 返回值说明
|
| 121 |
+
|
| 122 |
+
`build_case()` 返回的字典包含以下字段:
|
| 123 |
+
|
| 124 |
+
```python
|
| 125 |
+
{
|
| 126 |
+
"case": {...}, # 标准 case JSON
|
| 127 |
+
"llm_input": "...", # LLM 输入 Markdown
|
| 128 |
+
"validation": {...}, # 验证结果
|
| 129 |
+
"risk_confirmed": True/False/None, # 主时序模型确认结果(如果启用)
|
| 130 |
+
"main_model_result": {...} # 主时序模型完整结果(如果启用)
|
| 131 |
+
}
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
**`risk_confirmed` 说明:**
|
| 135 |
+
- `True`: 主时序模型确认有风险
|
| 136 |
+
- `False`: 主时序模型确认无风险
|
| 137 |
+
- `None`: 未启用主时序模型确认,或确认失败
|
| 138 |
+
|
| 139 |
+
## 向后兼容
|
| 140 |
+
|
| 141 |
+
- 如果配置文件不存在,默认 `enabled=false`(只用 PatchAD)
|
| 142 |
+
- 如果 `enabled=false`,行为与现有代码完全一致
|
| 143 |
+
- 现有代码无需修改即可正常工作
|
| 144 |
+
|
| 145 |
+
## 注意事项
|
| 146 |
+
|
| 147 |
+
1. **性能**:自动模式需要加载模型,可能增加初始化时间(约 1-2 秒)
|
| 148 |
+
2. **内存**:自动模式会占用更多内存(模型加载)
|
| 149 |
+
3. **错误处理**:如果模型加载失败,会自动降级为只用 PatchAD,不会抛出异常
|
| 150 |
+
4. **路径**:`model_dir` 可以是相对路径(相对于 `hf_release` 目录)或绝对路径
|
| 151 |
+
|
data_storage/users/demo_anomaly.jsonl
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data_storage/users/demo_pattern.jsonl
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data_storage/users/sample_user.jsonl
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
demo_patchad_cases/demo_anomaly_official.json
CHANGED
|
@@ -2,18 +2,18 @@
|
|
| 2 |
"mode": "official",
|
| 3 |
"sample": "demo_anomaly",
|
| 4 |
"precheck": {
|
| 5 |
-
"event_id": "
|
| 6 |
-
"user_id": "
|
| 7 |
"status": "abnormal",
|
| 8 |
"score": 0.3484,
|
| 9 |
"threshold": 0.18,
|
| 10 |
-
"created_at": "2025-11-
|
| 11 |
},
|
| 12 |
"case_bundle": {
|
| 13 |
"case": {
|
| 14 |
-
"case_id": "
|
| 15 |
-
"user_id": "
|
| 16 |
-
"generated_at": "2025-11-
|
| 17 |
"anomaly_pattern": {
|
| 18 |
"type": "continuous_anomaly",
|
| 19 |
"duration_days": 3,
|
|
@@ -87,6 +87,41 @@
|
|
| 87 |
"passed": true,
|
| 88 |
"errors": [],
|
| 89 |
"warnings": []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
}
|
| 91 |
}
|
| 92 |
}
|
|
|
|
| 2 |
"mode": "official",
|
| 3 |
"sample": "demo_anomaly",
|
| 4 |
"precheck": {
|
| 5 |
+
"event_id": "demo_anomaly-4ba26c5e",
|
| 6 |
+
"user_id": "demo_anomaly",
|
| 7 |
"status": "abnormal",
|
| 8 |
"score": 0.3484,
|
| 9 |
"threshold": 0.18,
|
| 10 |
+
"created_at": "2025-11-28T02:50:34.582609+00:00"
|
| 11 |
},
|
| 12 |
"case_bundle": {
|
| 13 |
"case": {
|
| 14 |
+
"case_id": "demo_anomaly-4ba26c5e",
|
| 15 |
+
"user_id": "demo_anomaly",
|
| 16 |
+
"generated_at": "2025-11-28T02:50:34.603862+00:00",
|
| 17 |
"anomaly_pattern": {
|
| 18 |
"type": "continuous_anomaly",
|
| 19 |
"duration_days": 3,
|
|
|
|
| 87 |
"passed": true,
|
| 88 |
"errors": [],
|
| 89 |
"warnings": []
|
| 90 |
+
},
|
| 91 |
+
"risk_confirmed": false,
|
| 92 |
+
"main_model_result": {
|
| 93 |
+
"anomaly_pattern": {
|
| 94 |
+
"has_pattern": false,
|
| 95 |
+
"duration_days": 0,
|
| 96 |
+
"anomaly_dates": [],
|
| 97 |
+
"anomaly_scores": [],
|
| 98 |
+
"pattern_description": "异常仅持续0天,未达到最小持续天数3天"
|
| 99 |
+
},
|
| 100 |
+
"baseline_info": null,
|
| 101 |
+
"related_indicators": null,
|
| 102 |
+
"daily_results": [
|
| 103 |
+
{
|
| 104 |
+
"date": "2025-11-27T13:47:23.434200",
|
| 105 |
+
"anomaly_score": 0.5196528434753418,
|
| 106 |
+
"is_anomaly": false,
|
| 107 |
+
"hrv_rmssd": 68.38103114993835,
|
| 108 |
+
"hr": 73.06798211937824
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"date": "2025-11-27T13:47:23.434200",
|
| 112 |
+
"anomaly_score": 0.5196528434753418,
|
| 113 |
+
"is_anomaly": false,
|
| 114 |
+
"hrv_rmssd": 68.38103114993835,
|
| 115 |
+
"hr": 73.06798211937824
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
"date": "2025-11-27T13:47:23.434200",
|
| 119 |
+
"anomaly_score": 0.5196528434753418,
|
| 120 |
+
"is_anomaly": false,
|
| 121 |
+
"hrv_rmssd": 68.38103114993835,
|
| 122 |
+
"hr": 73.06798211937824
|
| 123 |
+
}
|
| 124 |
+
]
|
| 125 |
}
|
| 126 |
}
|
| 127 |
}
|
demo_patchad_cases/demo_anomaly_platform.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
| 10 |
"case": {
|
| 11 |
"case_id": "platform-demo-evt",
|
| 12 |
"user_id": "demo_anomaly",
|
| 13 |
-
"generated_at": "2025-11-
|
| 14 |
"anomaly_pattern": {
|
| 15 |
"type": "continuous_anomaly",
|
| 16 |
"duration_days": 3,
|
|
@@ -83,6 +83,41 @@
|
|
| 83 |
"passed": true,
|
| 84 |
"errors": [],
|
| 85 |
"warnings": []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
}
|
| 87 |
}
|
| 88 |
}
|
|
|
|
| 10 |
"case": {
|
| 11 |
"case_id": "platform-demo-evt",
|
| 12 |
"user_id": "demo_anomaly",
|
| 13 |
+
"generated_at": "2025-11-28T02:50:34.467699+00:00",
|
| 14 |
"anomaly_pattern": {
|
| 15 |
"type": "continuous_anomaly",
|
| 16 |
"duration_days": 3,
|
|
|
|
| 83 |
"passed": true,
|
| 84 |
"errors": [],
|
| 85 |
"warnings": []
|
| 86 |
+
},
|
| 87 |
+
"risk_confirmed": false,
|
| 88 |
+
"main_model_result": {
|
| 89 |
+
"anomaly_pattern": {
|
| 90 |
+
"has_pattern": false,
|
| 91 |
+
"duration_days": 0,
|
| 92 |
+
"anomaly_dates": [],
|
| 93 |
+
"anomaly_scores": [],
|
| 94 |
+
"pattern_description": "异常仅持续0天,未达到最小持续天数3天"
|
| 95 |
+
},
|
| 96 |
+
"baseline_info": null,
|
| 97 |
+
"related_indicators": null,
|
| 98 |
+
"daily_results": [
|
| 99 |
+
{
|
| 100 |
+
"date": "2025-11-27T13:47:23.434200",
|
| 101 |
+
"anomaly_score": 0.5196528434753418,
|
| 102 |
+
"is_anomaly": false,
|
| 103 |
+
"hrv_rmssd": 68.38103114993835,
|
| 104 |
+
"hr": 73.06798211937824
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"date": "2025-11-27T13:47:23.434200",
|
| 108 |
+
"anomaly_score": 0.5196528434753418,
|
| 109 |
+
"is_anomaly": false,
|
| 110 |
+
"hrv_rmssd": 68.38103114993835,
|
| 111 |
+
"hr": 73.06798211937824
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
"date": "2025-11-27T13:47:23.434200",
|
| 115 |
+
"anomaly_score": 0.5196528434753418,
|
| 116 |
+
"is_anomaly": false,
|
| 117 |
+
"hrv_rmssd": 68.38103114993835,
|
| 118 |
+
"hr": 73.06798211937824
|
| 119 |
+
}
|
| 120 |
+
]
|
| 121 |
}
|
| 122 |
}
|
| 123 |
}
|
demo_patchad_cases/demo_pattern_official.json
CHANGED
|
@@ -2,18 +2,18 @@
|
|
| 2 |
"mode": "official",
|
| 3 |
"sample": "demo_pattern",
|
| 4 |
"precheck": {
|
| 5 |
-
"event_id": "
|
| 6 |
-
"user_id": "
|
| 7 |
"status": "abnormal",
|
| 8 |
"score": 0.1969,
|
| 9 |
"threshold": 0.18,
|
| 10 |
-
"created_at": "2025-11-
|
| 11 |
},
|
| 12 |
"case_bundle": {
|
| 13 |
"case": {
|
| 14 |
-
"case_id": "
|
| 15 |
-
"user_id": "
|
| 16 |
-
"generated_at": "2025-11-
|
| 17 |
"anomaly_pattern": {
|
| 18 |
"type": "continuous_anomaly",
|
| 19 |
"duration_days": 3,
|
|
@@ -87,6 +87,41 @@
|
|
| 87 |
"passed": true,
|
| 88 |
"errors": [],
|
| 89 |
"warnings": []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
}
|
| 91 |
}
|
| 92 |
}
|
|
|
|
| 2 |
"mode": "official",
|
| 3 |
"sample": "demo_pattern",
|
| 4 |
"precheck": {
|
| 5 |
+
"event_id": "demo_pattern-e89db848",
|
| 6 |
+
"user_id": "demo_pattern",
|
| 7 |
"status": "abnormal",
|
| 8 |
"score": 0.1969,
|
| 9 |
"threshold": 0.18,
|
| 10 |
+
"created_at": "2025-11-28T02:50:31.275982+00:00"
|
| 11 |
},
|
| 12 |
"case_bundle": {
|
| 13 |
"case": {
|
| 14 |
+
"case_id": "demo_pattern-e89db848",
|
| 15 |
+
"user_id": "demo_pattern",
|
| 16 |
+
"generated_at": "2025-11-28T02:50:31.298789+00:00",
|
| 17 |
"anomaly_pattern": {
|
| 18 |
"type": "continuous_anomaly",
|
| 19 |
"duration_days": 3,
|
|
|
|
| 87 |
"passed": true,
|
| 88 |
"errors": [],
|
| 89 |
"warnings": []
|
| 90 |
+
},
|
| 91 |
+
"risk_confirmed": false,
|
| 92 |
+
"main_model_result": {
|
| 93 |
+
"anomaly_pattern": {
|
| 94 |
+
"has_pattern": false,
|
| 95 |
+
"duration_days": 0,
|
| 96 |
+
"anomaly_dates": [],
|
| 97 |
+
"anomaly_scores": [],
|
| 98 |
+
"pattern_description": "异常仅持续0天,未达到最小持续天数3天"
|
| 99 |
+
},
|
| 100 |
+
"baseline_info": null,
|
| 101 |
+
"related_indicators": null,
|
| 102 |
+
"daily_results": [
|
| 103 |
+
{
|
| 104 |
+
"date": "2025-11-20T08:00:00",
|
| 105 |
+
"anomaly_score": 0.5221930146217346,
|
| 106 |
+
"is_anomaly": false,
|
| 107 |
+
"hrv_rmssd": 76.75195878346763,
|
| 108 |
+
"hr": 69.55658937643992
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"date": "2025-11-21T08:00:00",
|
| 112 |
+
"anomaly_score": 0.522602379322052,
|
| 113 |
+
"is_anomaly": false,
|
| 114 |
+
"hrv_rmssd": 74.44976175238928,
|
| 115 |
+
"hr": 69.4180874776572
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
"date": "2025-11-22T08:00:00",
|
| 119 |
+
"anomaly_score": 0.5226739645004272,
|
| 120 |
+
"is_anomaly": false,
|
| 121 |
+
"hrv_rmssd": 75.61461435432079,
|
| 122 |
+
"hr": 69.15957575082764
|
| 123 |
+
}
|
| 124 |
+
]
|
| 125 |
}
|
| 126 |
}
|
| 127 |
}
|
demo_patchad_cases/demo_pattern_platform.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
| 10 |
"case": {
|
| 11 |
"case_id": "platform-demo-evt",
|
| 12 |
"user_id": "demo_pattern",
|
| 13 |
-
"generated_at": "2025-11-
|
| 14 |
"anomaly_pattern": {
|
| 15 |
"type": "continuous_anomaly",
|
| 16 |
"duration_days": 3,
|
|
@@ -83,6 +83,41 @@
|
|
| 83 |
"passed": true,
|
| 84 |
"errors": [],
|
| 85 |
"warnings": []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
}
|
| 87 |
}
|
| 88 |
}
|
|
|
|
| 10 |
"case": {
|
| 11 |
"case_id": "platform-demo-evt",
|
| 12 |
"user_id": "demo_pattern",
|
| 13 |
+
"generated_at": "2025-11-28T02:50:31.157741+00:00",
|
| 14 |
"anomaly_pattern": {
|
| 15 |
"type": "continuous_anomaly",
|
| 16 |
"duration_days": 3,
|
|
|
|
| 83 |
"passed": true,
|
| 84 |
"errors": [],
|
| 85 |
"warnings": []
|
| 86 |
+
},
|
| 87 |
+
"risk_confirmed": false,
|
| 88 |
+
"main_model_result": {
|
| 89 |
+
"anomaly_pattern": {
|
| 90 |
+
"has_pattern": false,
|
| 91 |
+
"duration_days": 0,
|
| 92 |
+
"anomaly_dates": [],
|
| 93 |
+
"anomaly_scores": [],
|
| 94 |
+
"pattern_description": "异常仅持续0天,未达到最小持续天数3天"
|
| 95 |
+
},
|
| 96 |
+
"baseline_info": null,
|
| 97 |
+
"related_indicators": null,
|
| 98 |
+
"daily_results": [
|
| 99 |
+
{
|
| 100 |
+
"date": "2025-11-20T08:00:00",
|
| 101 |
+
"anomaly_score": 0.5221930146217346,
|
| 102 |
+
"is_anomaly": false,
|
| 103 |
+
"hrv_rmssd": 76.75195878346763,
|
| 104 |
+
"hr": 69.55658937643992
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"date": "2025-11-21T08:00:00",
|
| 108 |
+
"anomaly_score": 0.522602379322052,
|
| 109 |
+
"is_anomaly": false,
|
| 110 |
+
"hrv_rmssd": 74.44976175238928,
|
| 111 |
+
"hr": 69.4180874776572
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
"date": "2025-11-22T08:00:00",
|
| 115 |
+
"anomaly_score": 0.5226739645004272,
|
| 116 |
+
"is_anomaly": false,
|
| 117 |
+
"hrv_rmssd": 75.61461435432079,
|
| 118 |
+
"hr": 69.15957575082764
|
| 119 |
+
}
|
| 120 |
+
]
|
| 121 |
}
|
| 122 |
}
|
| 123 |
}
|
demo_patchad_cases/demo_pattern_test_official.json
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"mode": "official",
|
| 3 |
+
"sample": "demo_pattern_test",
|
| 4 |
+
"precheck": {
|
| 5 |
+
"event_id": "demo_pattern-f92fc28c",
|
| 6 |
+
"user_id": "demo_pattern",
|
| 7 |
+
"status": "abnormal",
|
| 8 |
+
"score": 0.1969,
|
| 9 |
+
"threshold": 0.18,
|
| 10 |
+
"created_at": "2025-11-28T02:45:33.134958+00:00"
|
| 11 |
+
},
|
| 12 |
+
"case_bundle": {
|
| 13 |
+
"case": {
|
| 14 |
+
"case_id": "demo_pattern-f92fc28c",
|
| 15 |
+
"user_id": "demo_pattern",
|
| 16 |
+
"generated_at": "2025-11-28T02:45:33.158854+00:00",
|
| 17 |
+
"anomaly_pattern": {
|
| 18 |
+
"type": "continuous_anomaly",
|
| 19 |
+
"duration_days": 3,
|
| 20 |
+
"trend": "improving",
|
| 21 |
+
"min_score": 0.1969,
|
| 22 |
+
"max_score": 0.2275,
|
| 23 |
+
"avg_score": 0.2153,
|
| 24 |
+
"threshold": 0.18
|
| 25 |
+
},
|
| 26 |
+
"baseline_info": {
|
| 27 |
+
"personal_mean": 75.0,
|
| 28 |
+
"personal_std": 0.0,
|
| 29 |
+
"group_mean": 67.5,
|
| 30 |
+
"record_count": 36,
|
| 31 |
+
"baseline_type": "personal",
|
| 32 |
+
"baseline_reliability": "high"
|
| 33 |
+
},
|
| 34 |
+
"related_indicators": {
|
| 35 |
+
"activity_level": {
|
| 36 |
+
"level": "低",
|
| 37 |
+
"avg_steps": 0.0,
|
| 38 |
+
"trend": "stable"
|
| 39 |
+
},
|
| 40 |
+
"sleep_quality": {
|
| 41 |
+
"available": false,
|
| 42 |
+
"quality": "数据不可用"
|
| 43 |
+
},
|
| 44 |
+
"stress_indicators": {
|
| 45 |
+
"level": "unknown"
|
| 46 |
+
}
|
| 47 |
+
},
|
| 48 |
+
"daily_results": [
|
| 49 |
+
{
|
| 50 |
+
"date": "2025-11-20",
|
| 51 |
+
"hrv_rmssd": 76.75,
|
| 52 |
+
"hr": 69.56,
|
| 53 |
+
"anomaly_score": 0.2275
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"date": "2025-11-21",
|
| 57 |
+
"hrv_rmssd": 74.45,
|
| 58 |
+
"hr": 69.42,
|
| 59 |
+
"anomaly_score": 0.2215
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"date": "2025-11-22",
|
| 63 |
+
"hrv_rmssd": 75.61,
|
| 64 |
+
"hr": 69.16,
|
| 65 |
+
"anomaly_score": 0.1969
|
| 66 |
+
}
|
| 67 |
+
],
|
| 68 |
+
"user_profile": {
|
| 69 |
+
"age_group": "30-35岁",
|
| 70 |
+
"estimated_age": 33,
|
| 71 |
+
"sex": "男性",
|
| 72 |
+
"exercise": "每周5次以上",
|
| 73 |
+
"coffee": "偶尔",
|
| 74 |
+
"smoking": "不吸烟",
|
| 75 |
+
"drinking": "经常饮酒",
|
| 76 |
+
"MEQ": 62.0,
|
| 77 |
+
"MEQ_type": "晨型"
|
| 78 |
+
},
|
| 79 |
+
"metadata": {
|
| 80 |
+
"detector": "official_patchad",
|
| 81 |
+
"patchad_score": 0.1969,
|
| 82 |
+
"threshold": 0.18
|
| 83 |
+
}
|
| 84 |
+
},
|
| 85 |
+
"llm_input": "# 健康异常检测分析报告\n\n## 异常概览\n**异常类型**:continuous_anomaly \n**持续时长**:3天 \n**异常趋势**:improving \n\n## 异常评分分析\n- **异常分数范围**:0.1969 - 0.2275\n- **平均异常分数**:0.2153\n- **检测阈值**:0.1800\n\n## 当前生理状态评估\n| 指标 | 当前值 | 基线值 | 偏离基线 |\n|------|--------|--------|----------|\n| HRV RMSSD | 75.61 ms | 75.00 ms | +0.8% |\n\n## 历史趋势分析\n| 日期 | HRV (ms) | 心率 (bpm) | 异常分数 |\n|------|----------|------------|----------|\n| 2025-11-20 | 76.75 | 69.56 | 0.2275 |\n| 2025-11-21 | 74.45 | 69.42 | 0.2215 |\n| 2025-11-22 | 75.61 | 69.16 | 0.1969 |\n\n## 相关健康指标分析\n- 活动水平:低(平均步数=0.0,趋势=stable)\n- 睡眠质量:数据不可用\n- 压力指标:暂无显著异常\n\n## 用户背景信息\n- 年龄:33岁(30-35岁)\n- 性别:男性\n- 运动习惯:每周5次以上\n- 咖啡:偶尔 / 饮酒:经常饮酒\n- 生物节律:MEQ=62.0 (晨型)",
|
| 86 |
+
"validation": {
|
| 87 |
+
"passed": true,
|
| 88 |
+
"errors": [],
|
| 89 |
+
"warnings": []
|
| 90 |
+
},
|
| 91 |
+
"risk_confirmed": false,
|
| 92 |
+
"main_model_result": {
|
| 93 |
+
"anomaly_pattern": {
|
| 94 |
+
"has_pattern": false,
|
| 95 |
+
"duration_days": 0,
|
| 96 |
+
"anomaly_dates": [],
|
| 97 |
+
"anomaly_scores": [],
|
| 98 |
+
"pattern_description": "异常仅持续0天,未达到最小持续天数3天"
|
| 99 |
+
},
|
| 100 |
+
"baseline_info": null,
|
| 101 |
+
"related_indicators": null,
|
| 102 |
+
"daily_results": [
|
| 103 |
+
{
|
| 104 |
+
"date": "2025-11-20T08:00:00",
|
| 105 |
+
"anomaly_score": 0.5221930146217346,
|
| 106 |
+
"is_anomaly": false,
|
| 107 |
+
"hrv_rmssd": 76.75195878346763,
|
| 108 |
+
"hr": 69.55658937643992
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"date": "2025-11-21T08:00:00",
|
| 112 |
+
"anomaly_score": 0.522602379322052,
|
| 113 |
+
"is_anomaly": false,
|
| 114 |
+
"hrv_rmssd": 74.44976175238928,
|
| 115 |
+
"hr": 69.4180874776572
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
"date": "2025-11-22T08:00:00",
|
| 119 |
+
"anomaly_score": 0.5226739645004272,
|
| 120 |
+
"is_anomaly": false,
|
| 121 |
+
"hrv_rmssd": 75.61461435432079,
|
| 122 |
+
"hr": 69.15957575082764
|
| 123 |
+
}
|
| 124 |
+
]
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
}
|
demo_patchad_cases/manifest.json
CHANGED
|
@@ -1,26 +1,16 @@
|
|
| 1 |
[
|
| 2 |
{
|
| 3 |
"sample": "demo_pattern",
|
| 4 |
-
"
|
| 5 |
-
"
|
| 6 |
-
"
|
| 7 |
-
|
| 8 |
-
{
|
| 9 |
-
"sample": "demo_pattern",
|
| 10 |
-
"mode": "official",
|
| 11 |
-
"title": "demo_pattern · 模式B:官方预筛",
|
| 12 |
-
"file": "demo_pattern_official.json"
|
| 13 |
-
},
|
| 14 |
-
{
|
| 15 |
-
"sample": "demo_anomaly",
|
| 16 |
-
"mode": "platform",
|
| 17 |
-
"title": "demo_anomaly · 模式A:平台自带",
|
| 18 |
-
"file": "demo_anomaly_platform.json"
|
| 19 |
},
|
| 20 |
{
|
| 21 |
"sample": "demo_anomaly",
|
| 22 |
-
"
|
| 23 |
-
"
|
| 24 |
-
"
|
|
|
|
| 25 |
}
|
| 26 |
-
]
|
|
|
|
| 1 |
[
|
| 2 |
{
|
| 3 |
"sample": "demo_pattern",
|
| 4 |
+
"title": "正常模式案例",
|
| 5 |
+
"file_platform": "demo_pattern_platform.json",
|
| 6 |
+
"file_official": "demo_pattern_official.json",
|
| 7 |
+
"description": "正常心率变异性模式,展示正常情况下的处理流程"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
},
|
| 9 |
{
|
| 10 |
"sample": "demo_anomaly",
|
| 11 |
+
"title": "异常模式案例",
|
| 12 |
+
"file_platform": "demo_anomaly_platform.json",
|
| 13 |
+
"file_official": "demo_anomaly_official.json",
|
| 14 |
+
"description": "异常心率变异性模式,展示异常情况下的处理流程"
|
| 15 |
}
|
| 16 |
+
]
|
gradio_app.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
requirements.txt
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
torch>=2.1.0
|
| 2 |
numpy>=1.24
|
| 3 |
pandas>=2.0
|
|
|
|
| 1 |
+
gradio>=4.44.0
|
| 2 |
torch>=2.1.0
|
| 3 |
numpy>=1.24
|
| 4 |
pandas>=2.0
|
simulate_patchad_case_pipeline.py
CHANGED
|
@@ -139,33 +139,38 @@ def run_mode_platform(records: List[Dict[str, Any]], sample_name: str, save_dir:
|
|
| 139 |
|
| 140 |
def run_mode_official(records: List[Dict[str, Any]], sample_name: str, save_dir: Path | None) -> Dict[str, Any] | None:
|
| 141 |
print("\n=== 模式B:官方 precheck + build_case ===")
|
| 142 |
-
|
| 143 |
-
if len(
|
| 144 |
-
|
| 145 |
-
current_window = windows[-1]["window"]
|
| 146 |
|
| 147 |
server = PrecheckServer(score_threshold=0.18)
|
| 148 |
-
pre_result = server.precheck(
|
| 149 |
print(f"预筛接口返回: {pre_result}")
|
| 150 |
if pre_result["status"] != "abnormal":
|
| 151 |
print("预筛未触发异常,示例终止。")
|
| 152 |
return None
|
| 153 |
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
payload = {
|
| 156 |
-
"event_id": pre_result["event_id"],
|
| 157 |
-
"user_id":
|
| 158 |
-
"window_data": pending["window_data"],
|
| 159 |
"user_profile": sample_profile(),
|
| 160 |
-
"history_windows": windows,
|
| 161 |
"metadata": {
|
| 162 |
"detector": "official_patchad",
|
| 163 |
-
"patchad_score":
|
| 164 |
-
"threshold":
|
| 165 |
},
|
| 166 |
}
|
| 167 |
|
| 168 |
-
builder = CaseBuilder()
|
| 169 |
result = builder.build_case(payload)
|
| 170 |
print(f"验证结果: {result['validation']}")
|
| 171 |
print(f"Case ID: {result['case']['case_id']}")
|
|
|
|
| 139 |
|
| 140 |
def run_mode_official(records: List[Dict[str, Any]], sample_name: str, save_dir: Path | None) -> Dict[str, Any] | None:
|
| 141 |
print("\n=== 模式B:官方 precheck + build_case ===")
|
| 142 |
+
print("步骤1:平台传入当前窗口到 precheck 接口")
|
| 143 |
+
current_window = records[-12:] if len(records) >= 12 else records
|
| 144 |
+
user_id = current_window[0].get("deviceId", "demo_user") if current_window else "demo_user"
|
|
|
|
| 145 |
|
| 146 |
server = PrecheckServer(score_threshold=0.18)
|
| 147 |
+
pre_result = server.precheck(user_id, current_window)
|
| 148 |
print(f"预筛接口返回: {pre_result}")
|
| 149 |
if pre_result["status"] != "abnormal":
|
| 150 |
print("预筛未触发异常,示例终止。")
|
| 151 |
return None
|
| 152 |
|
| 153 |
+
print(f"\n步骤2:平台收到 event_id={pre_result['event_id']},平台内部合成历史窗口数据")
|
| 154 |
+
# 模拟平台自己合成历史窗口(从历史数据平台获取)
|
| 155 |
+
windows = split_windows(records, days=3)
|
| 156 |
+
if len(windows) < 2:
|
| 157 |
+
raise RuntimeError("样例数据不足以生成历史窗口")
|
| 158 |
+
print(f"平台合成历史窗口: {len(windows)} 天")
|
| 159 |
+
|
| 160 |
+
print(f"\n步骤3:平台调用 build_case,传入 event_id 和合成的历史窗口(不传 window_data,系统从缓存获取)")
|
| 161 |
payload = {
|
| 162 |
+
"event_id": pre_result["event_id"], # 关键:只传 event_id,不传 window_data
|
| 163 |
+
"user_id": user_id,
|
|
|
|
| 164 |
"user_profile": sample_profile(),
|
| 165 |
+
"history_windows": windows, # 平台自己合成的历史窗口
|
| 166 |
"metadata": {
|
| 167 |
"detector": "official_patchad",
|
| 168 |
+
"patchad_score": pre_result["score"],
|
| 169 |
+
"threshold": pre_result["threshold"],
|
| 170 |
},
|
| 171 |
}
|
| 172 |
|
| 173 |
+
builder = CaseBuilder(precheck_server=server) # 传入 PrecheckServer 实例
|
| 174 |
result = builder.build_case(payload)
|
| 175 |
print(f"验证结果: {result['validation']}")
|
| 176 |
print(f"Case ID: {result['case']['case_id']}")
|
utils/case_builder.py
CHANGED
|
@@ -5,11 +5,21 @@ import statistics
|
|
| 5 |
from dataclasses import dataclass
|
| 6 |
from datetime import datetime, timezone
|
| 7 |
from pathlib import Path
|
| 8 |
-
from typing import Any, Dict, List, Tuple
|
| 9 |
from uuid import uuid4
|
| 10 |
|
| 11 |
from utils.patchad_filter import PatchADFilter
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
@dataclass
|
| 15 |
class ValidationResult:
|
|
@@ -28,10 +38,80 @@ class ValidationResult:
|
|
| 28 |
class CaseBuilder:
|
| 29 |
"""
|
| 30 |
根据规范化 payload 生成 case JSON + Markdown 输入描述。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
"""
|
| 32 |
|
| 33 |
-
def __init__(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
self.patchad = PatchADFilter()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
def validate_payload(self, payload: Dict[str, Any]) -> ValidationResult:
|
| 37 |
errors: List[str] = []
|
|
@@ -40,9 +120,31 @@ class CaseBuilder:
|
|
| 40 |
if not payload.get("user_id"):
|
| 41 |
errors.append("缺少 user_id")
|
| 42 |
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
if not window or len(window) < 12:
|
| 45 |
-
errors.append("window_data 不足 12
|
| 46 |
|
| 47 |
profile = payload.get("user_profile")
|
| 48 |
if not profile:
|
|
@@ -50,7 +152,7 @@ class CaseBuilder:
|
|
| 50 |
|
| 51 |
history = payload.get("history_windows", [])
|
| 52 |
if not history:
|
| 53 |
-
errors.append("缺少 history_windows")
|
| 54 |
|
| 55 |
metadata = payload.get("metadata", {})
|
| 56 |
if not metadata.get("detector"):
|
|
@@ -242,10 +344,40 @@ class CaseBuilder:
|
|
| 242 |
|
| 243 |
llm_input = self._format_llm_input(case)
|
| 244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
return {
|
| 246 |
"case": case,
|
| 247 |
"llm_input": llm_input,
|
| 248 |
"validation": validation.to_dict(),
|
|
|
|
|
|
|
| 249 |
}
|
| 250 |
|
| 251 |
|
|
|
|
| 5 |
from dataclasses import dataclass
|
| 6 |
from datetime import datetime, timezone
|
| 7 |
from pathlib import Path
|
| 8 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 9 |
from uuid import uuid4
|
| 10 |
|
| 11 |
from utils.patchad_filter import PatchADFilter
|
| 12 |
|
| 13 |
+
# 可选导入主时序模型(如果不存在则使用 None)
|
| 14 |
+
try:
|
| 15 |
+
import sys
|
| 16 |
+
base_dir = Path(__file__).parent.parent
|
| 17 |
+
if str(base_dir) not in sys.path:
|
| 18 |
+
sys.path.insert(0, str(base_dir))
|
| 19 |
+
from wearable_anomaly_detector import WearableAnomalyDetector
|
| 20 |
+
except ImportError:
|
| 21 |
+
WearableAnomalyDetector = None
|
| 22 |
+
|
| 23 |
|
| 24 |
@dataclass
|
| 25 |
class ValidationResult:
|
|
|
|
| 38 |
class CaseBuilder:
|
| 39 |
"""
|
| 40 |
根据规范化 payload 生成 case JSON + Markdown 输入描述。
|
| 41 |
+
|
| 42 |
+
支持配置主时序模型确认(自动/手工模式):
|
| 43 |
+
- enabled=true, mode=auto: 自动调用主时序模型确认风险
|
| 44 |
+
- enabled=false: 只用 PatchAD 分数,不调用主时序模型
|
| 45 |
+
|
| 46 |
+
支持两种模式:
|
| 47 |
+
- 模式A:平台有PatchAD,直接传入所有数据(window_data + history_windows)
|
| 48 |
+
- 模式B:平台没有PatchAD,通过 event_id 从 PrecheckServer 缓存获取 window_data
|
| 49 |
"""
|
| 50 |
|
| 51 |
+
def __init__(
|
| 52 |
+
self,
|
| 53 |
+
config_path: Optional[Path] = None,
|
| 54 |
+
detector: Optional[Any] = None,
|
| 55 |
+
precheck_server: Optional[Any] = None
|
| 56 |
+
) -> None:
|
| 57 |
+
"""
|
| 58 |
+
初始化 CaseBuilder
|
| 59 |
+
|
| 60 |
+
参数:
|
| 61 |
+
config_path: 配置文件路径,如果为None则使用默认配置
|
| 62 |
+
detector: 可选的主时序模型检测器实例(如果外部已加载)
|
| 63 |
+
precheck_server: 可选的 PrecheckServer 实例,用于模式B从缓存获取 window_data
|
| 64 |
+
"""
|
| 65 |
self.patchad = PatchADFilter()
|
| 66 |
+
self.detector = detector # 外部传入的检测器(可选)
|
| 67 |
+
self.precheck_server = precheck_server # PrecheckServer 实例(用于模式B)
|
| 68 |
+
self.config = self._load_config(config_path)
|
| 69 |
+
|
| 70 |
+
# 如果配置为自动且未传入 detector,则自动加载
|
| 71 |
+
if self.config.get("main_model_confirmation", {}).get("enabled", False):
|
| 72 |
+
if self.detector is None and WearableAnomalyDetector is not None:
|
| 73 |
+
model_dir = self.config["main_model_confirmation"].get("model_dir")
|
| 74 |
+
if model_dir:
|
| 75 |
+
# 转换为绝对路径(相对于 hf_release 目录)
|
| 76 |
+
base_dir = Path(__file__).parent.parent
|
| 77 |
+
model_path = base_dir / model_dir if not Path(model_dir).is_absolute() else Path(model_dir)
|
| 78 |
+
device = self.config["main_model_confirmation"].get("device", "cpu")
|
| 79 |
+
threshold = self.config["main_model_confirmation"].get("threshold")
|
| 80 |
+
|
| 81 |
+
try:
|
| 82 |
+
self.detector = WearableAnomalyDetector(
|
| 83 |
+
model_dir=model_path,
|
| 84 |
+
device=device,
|
| 85 |
+
threshold=threshold
|
| 86 |
+
)
|
| 87 |
+
print(f"✅ 已自动加载主时序模型: {model_path}")
|
| 88 |
+
except Exception as e:
|
| 89 |
+
print(f"⚠️ 自动加载主时序模型失败: {e},将只使用 PatchAD 分数")
|
| 90 |
+
self.detector = None
|
| 91 |
+
|
| 92 |
+
def _load_config(self, config_path: Optional[Path]) -> Dict[str, Any]:
|
| 93 |
+
"""加载配置文件"""
|
| 94 |
+
if config_path is None:
|
| 95 |
+
config_path = Path(__file__).parent.parent / "configs" / "case_builder_config.json"
|
| 96 |
+
|
| 97 |
+
if config_path.exists():
|
| 98 |
+
try:
|
| 99 |
+
with open(config_path, 'r', encoding='utf-8') as f:
|
| 100 |
+
return json.load(f)
|
| 101 |
+
except Exception as e:
|
| 102 |
+
print(f"⚠️ 加载 CaseBuilder 配置失败: {e},使用默认配置")
|
| 103 |
+
|
| 104 |
+
# 返回默认配置(向后兼容)
|
| 105 |
+
return {
|
| 106 |
+
"main_model_confirmation": {
|
| 107 |
+
"enabled": False,
|
| 108 |
+
"mode": "auto",
|
| 109 |
+
"model_dir": "checkpoints/phase2/exp_factor_balanced",
|
| 110 |
+
"device": "cpu",
|
| 111 |
+
"threshold": None,
|
| 112 |
+
"min_duration_days": 3
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
|
| 116 |
def validate_payload(self, payload: Dict[str, Any]) -> ValidationResult:
|
| 117 |
errors: List[str] = []
|
|
|
|
| 120 |
if not payload.get("user_id"):
|
| 121 |
errors.append("缺少 user_id")
|
| 122 |
|
| 123 |
+
# 模式B:通过 event_id 从缓存获取 window_data
|
| 124 |
+
event_id = payload.get("event_id")
|
| 125 |
+
window = payload.get("window_data")
|
| 126 |
+
|
| 127 |
+
if event_id and self.precheck_server and not window:
|
| 128 |
+
# 模式B:从 PrecheckServer 缓存获取 window_data(不删除,因为可能还需要)
|
| 129 |
+
if self.precheck_server.has_pending(event_id):
|
| 130 |
+
# 使用 pop_event 获取并删除(因为已经使用过了)
|
| 131 |
+
pending = self.precheck_server.pop_event(event_id)
|
| 132 |
+
window = pending.get("window_data")
|
| 133 |
+
payload["window_data"] = window
|
| 134 |
+
if pending.get("user_id") and not payload.get("user_id"):
|
| 135 |
+
payload["user_id"] = pending.get("user_id")
|
| 136 |
+
# 补充 metadata 中的 patchad 信息
|
| 137 |
+
if not payload.get("metadata", {}).get("patchad_score"):
|
| 138 |
+
metadata = payload.get("metadata", {})
|
| 139 |
+
if "patchad_score" not in metadata:
|
| 140 |
+
metadata["patchad_score"] = pending.get("metadata", {}).get("patchad_score", 0.0)
|
| 141 |
+
if "threshold" not in metadata:
|
| 142 |
+
metadata["threshold"] = pending.get("metadata", {}).get("threshold", 0.0)
|
| 143 |
+
payload["metadata"] = metadata
|
| 144 |
+
|
| 145 |
+
# 模式A:直接传入 window_data
|
| 146 |
if not window or len(window) < 12:
|
| 147 |
+
errors.append("window_data 不足 12 条(模式A需直接传入,模式B需通过 event_id 从缓存获取)")
|
| 148 |
|
| 149 |
profile = payload.get("user_profile")
|
| 150 |
if not profile:
|
|
|
|
| 152 |
|
| 153 |
history = payload.get("history_windows", [])
|
| 154 |
if not history:
|
| 155 |
+
errors.append("缺少 history_windows(平台需自己合成历史窗口数据)")
|
| 156 |
|
| 157 |
metadata = payload.get("metadata", {})
|
| 158 |
if not metadata.get("detector"):
|
|
|
|
| 344 |
|
| 345 |
llm_input = self._format_llm_input(case)
|
| 346 |
|
| 347 |
+
# 【新增】主时序模型确认(如果配置启用)
|
| 348 |
+
risk_confirmed = None
|
| 349 |
+
main_model_result = None
|
| 350 |
+
|
| 351 |
+
if self.config.get("main_model_confirmation", {}).get("enabled", False):
|
| 352 |
+
if self.detector:
|
| 353 |
+
try:
|
| 354 |
+
# 使用主时序模型对历史窗口进行确认
|
| 355 |
+
# 将数据组织成按天的格式:[[day1_data], [day2_data], ...]
|
| 356 |
+
daily_data_points = []
|
| 357 |
+
for item in normalized_history:
|
| 358 |
+
daily_data_points.append(item["window"])
|
| 359 |
+
|
| 360 |
+
if daily_data_points:
|
| 361 |
+
pattern_result = self.detector.detect_pattern(
|
| 362 |
+
daily_data_points,
|
| 363 |
+
days=len(normalized_history),
|
| 364 |
+
min_duration_days=self.config["main_model_confirmation"].get("min_duration_days", 3)
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
# 判断是否确认有风险
|
| 368 |
+
risk_confirmed = pattern_result.get('anomaly_pattern', {}).get('has_pattern', False)
|
| 369 |
+
main_model_result = pattern_result
|
| 370 |
+
except Exception as e:
|
| 371 |
+
print(f"⚠️ 主时序模型确认失败: {e},将只使用 PatchAD 分数")
|
| 372 |
+
risk_confirmed = None
|
| 373 |
+
main_model_result = None
|
| 374 |
+
|
| 375 |
return {
|
| 376 |
"case": case,
|
| 377 |
"llm_input": llm_input,
|
| 378 |
"validation": validation.to_dict(),
|
| 379 |
+
"risk_confirmed": risk_confirmed, # 【新增】主时序模型确认结果
|
| 380 |
+
"main_model_result": main_model_result, # 【新增】主时序模型完整结果
|
| 381 |
}
|
| 382 |
|
| 383 |
|
流程说明_修正版.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CaseBuilder 流程说明(修正版)
|
| 2 |
+
|
| 3 |
+
## 两种流程模式
|
| 4 |
+
|
| 5 |
+
### 模式A:平台有 PatchAD
|
| 6 |
+
|
| 7 |
+
**流程**:
|
| 8 |
+
1. 平台自己有 PatchAD,自己检测到异常
|
| 9 |
+
2. 平台自己合成数据:
|
| 10 |
+
- `window_data`:当前窗口(12条)
|
| 11 |
+
- `history_windows`:历史窗口(3-7天,每天12条)
|
| 12 |
+
- `user_profile`:用户档案
|
| 13 |
+
3. 平台直接调用 `build_case`,传入所有数据
|
| 14 |
+
|
| 15 |
+
**代码示例**:
|
| 16 |
+
```python
|
| 17 |
+
payload = {
|
| 18 |
+
"event_id": "platform-evt-123",
|
| 19 |
+
"user_id": "user_001",
|
| 20 |
+
"window_data": current_window, # 平台自己传入
|
| 21 |
+
"user_profile": user_profile, # 平台自己传入
|
| 22 |
+
"history_windows": history_windows, # 平台自己合成的
|
| 23 |
+
"metadata": {"detector": "platform_patchad", "score": 0.82},
|
| 24 |
+
}
|
| 25 |
+
builder = CaseBuilder()
|
| 26 |
+
result = builder.build_case(payload)
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
---
|
| 30 |
+
|
| 31 |
+
### 模式B:平台没有 PatchAD
|
| 32 |
+
|
| 33 |
+
**流程**:
|
| 34 |
+
1. **步骤1**:平台传入当前窗口(12条)到系统的 `/precheck` 接口
|
| 35 |
+
- 系统返回 `event_id` 和 `status`
|
| 36 |
+
- 系统内部缓存 `window_data`
|
| 37 |
+
|
| 38 |
+
2. **步骤2**:平台收到 `event_id` 后,**在平台内部合成历史窗口数据**
|
| 39 |
+
- 平台从历史数据平台获取最近几天的数据
|
| 40 |
+
- 平台自己按日期分割,生成 `history_windows`
|
| 41 |
+
- 平台自己获取 `user_profile`
|
| 42 |
+
|
| 43 |
+
3. **步骤3**:平台调用 `build_case`,传入:
|
| 44 |
+
- `event_id`:系统根据此从缓存获取 `window_data`
|
| 45 |
+
- `history_windows`:平台自己合成的
|
| 46 |
+
- `user_profile`:平台自己获取的
|
| 47 |
+
- **不传 `window_data`**(系统从缓存获取)
|
| 48 |
+
|
| 49 |
+
**代码示例**:
|
| 50 |
+
```python
|
| 51 |
+
# 步骤1:precheck
|
| 52 |
+
from utils.patchad_filter import PrecheckServer
|
| 53 |
+
server = PrecheckServer()
|
| 54 |
+
pre_result = server.precheck(user_id, current_window)
|
| 55 |
+
# 返回: {"event_id": "evt-123", "status": "abnormal", ...}
|
| 56 |
+
|
| 57 |
+
# 步骤2:平台自己合成历史窗口(平台内部操作)
|
| 58 |
+
history_windows = platform_synthesize_history_windows(user_id, days=3)
|
| 59 |
+
user_profile = platform_get_user_profile(user_id)
|
| 60 |
+
|
| 61 |
+
# 步骤3:build_case(只传 event_id,不传 window_data)
|
| 62 |
+
from utils.case_builder import CaseBuilder
|
| 63 |
+
builder = CaseBuilder(precheck_server=server) # 传入 PrecheckServer 实例
|
| 64 |
+
payload = {
|
| 65 |
+
"event_id": pre_result["event_id"], # 关键:只传 event_id
|
| 66 |
+
"user_id": user_id,
|
| 67 |
+
"user_profile": user_profile, # 平台自己获取
|
| 68 |
+
"history_windows": history_windows, # 平台自己合成的
|
| 69 |
+
"metadata": {"detector": "official_patchad"},
|
| 70 |
+
}
|
| 71 |
+
result = builder.build_case(payload) # 系统从缓存获取 window_data
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
---
|
| 75 |
+
|
| 76 |
+
## 关键区别
|
| 77 |
+
|
| 78 |
+
| 项目 | 模式A | 模式B |
|
| 79 |
+
|------|-------|-------|
|
| 80 |
+
| **预筛位置** | 平台内部 | 系统 `/precheck` 接口 |
|
| 81 |
+
| **window_data 来源** | 平台直接传入 | 系统从缓存获取(通过 event_id) |
|
| 82 |
+
| **history_windows 来源** | 平台自己合成 | 平台自己合成 |
|
| 83 |
+
| **user_profile 来源** | 平台自己获取 | 平台自己获取 |
|
| 84 |
+
| **交互次数** | 1次(build_case) | 2次(precheck + build_case) |
|
| 85 |
+
|
| 86 |
+
---
|
| 87 |
+
|
| 88 |
+
## 重要说明
|
| 89 |
+
|
| 90 |
+
1. **模式B中,平台需要自己合成 `history_windows`**
|
| 91 |
+
- 这不是系统的工作
|
| 92 |
+
- 平台从历史数据平台获取数据,按日期分割
|
| 93 |
+
- 系统只负责根据 `event_id` 从缓存获取 `window_data`
|
| 94 |
+
|
| 95 |
+
2. **模式B中,`build_case` 不需要传入 `window_data`**
|
| 96 |
+
- 系统会根据 `event_id` 从 `PrecheckServer` 的缓存中获取
|
| 97 |
+
- 如果传入了 `window_data`,则优先使用传入的(兼容模式A)
|
| 98 |
+
|
| 99 |
+
3. **CaseBuilder 需要传入 `precheck_server` 参数(模式B)**
|
| 100 |
+
- 这样系统才能从缓存中获取 `window_data`
|
| 101 |
+
- 模式A不需要传入
|
| 102 |
+
|