@delta-infra/cli 1.0.79 → 1.0.80
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.
- package/README.md +17 -27
- package/checksums.txt +5 -5
- package/package.json +2 -13
- package/scripts/install-wizard.js +14 -5
- package/skills/delta-science/SKILL.md +128 -149
- package/skills/delta-science/agents/openai.yaml +3 -3
- package/skills/{antbo-service/references/operations.md → delta-science/references/antbo.md} +59 -4
- package/skills/delta-science/references/cli-contract.md +18 -19
- package/skills/delta-science/references/delta-bo.md +90 -0
- package/skills/{gsasii/references/endpoints.md → delta-science/references/gsasii.md} +13 -27
- package/skills/{lammps/references/endpoints.md → delta-science/references/lammps.md} +13 -8
- package/skills/{ldm-bo/references/endpoints.md → delta-science/references/ldm-bo.md} +12 -9
- package/skills/delta-science/references/ldm.md +72 -0
- package/skills/{pubchem/references/endpoints.md → delta-science/references/pubchem.md} +15 -10
- package/skills/{pymatgen/references/endpoints.md → delta-science/references/pymatgen.md} +9 -9
- package/skills/{rdkit/references/endpoints.md → delta-science/references/rdkit.md} +20 -14
- package/skills/delta-science/references/routing-index.md +36 -0
- package/skills/delta-science/references/strbo.md +87 -0
- package/skills/{synbo-service/references/endpoints.md → delta-science/references/synbo.md} +57 -17
- package/skills/delta-science/references/workflows.md +24 -7
- package/skills/delta-science/scripts/catalog.py +92 -0
- package/skills/delta-science/scripts/invoke.py +162 -79
- package/skills/manifest.json +3 -1
- package/skills/antbo-ldm-guard/SKILL.md +0 -58
- package/skills/antbo-ldm-guard/agents/openai.yaml +0 -4
- package/skills/antbo-service/SKILL.md +0 -79
- package/skills/antbo-service/agents/openai.yaml +0 -4
- package/skills/delta-bo/SKILL.md +0 -36
- package/skills/delta-bo/agents/openai.yaml +0 -4
- package/skills/delta-bo/references/endpoints.md +0 -263
- package/skills/delta-science/references/generated-catalog.md +0 -48
- package/skills/delta-science/references/operation-contracts.md +0 -137
- package/skills/delta-science/references/services.md +0 -112
- package/skills/delta-science/scripts/generated_catalog.py +0 -69
- package/skills/gsasii/SKILL.md +0 -42
- package/skills/gsasii/agents/openai.yaml +0 -4
- package/skills/lammps/SKILL.md +0 -39
- package/skills/lammps/agents/openai.yaml +0 -4
- package/skills/ldm-bo/SKILL.md +0 -35
- package/skills/ldm-bo/agents/openai.yaml +0 -4
- package/skills/pubchem/SKILL.md +0 -93
- package/skills/pubchem/agents/openai.yaml +0 -4
- package/skills/pubchem/references/operations.md +0 -41
- package/skills/pymatgen/SKILL.md +0 -54
- package/skills/pymatgen/agents/openai.yaml +0 -4
- package/skills/rdkit/SKILL.md +0 -112
- package/skills/rdkit/agents/openai.yaml +0 -4
- package/skills/rdkit/references/operations.md +0 -64
- package/skills/science-tools.json +0 -127
- package/skills/synbo-service/SKILL.md +0 -36
- package/skills/synbo-service/agents/openai.yaml +0 -4
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# STRBO 详细规则
|
|
2
|
+
|
|
3
|
+
STRBO 用于服务端固定 **KRAS G12D** 靶点的小分子生成式多目标优化。目标顺序固定为
|
|
4
|
+
`[vina, activity]`:vina 越低越好,activity 越高越好。`task` 只引导候选生成,
|
|
5
|
+
不会更换靶点;不得声称支持用户自定义受体。
|
|
6
|
+
|
|
7
|
+
所有调用均使用 `tool="strbo"`。生成、建议、评估和提交 job 都会消耗远端资源,只在
|
|
8
|
+
用户确实要求执行相应科研任务时调用;请求结果未知或超时时不得重试。
|
|
9
|
+
|
|
10
|
+
## 初始化
|
|
11
|
+
|
|
12
|
+
- 同步 operation:`molecules-init`
|
|
13
|
+
- 异步 operation:`molecules-init-job`
|
|
14
|
+
- 仅校验 operation:`molecules-init-validate`
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"task": "为 KRAS G12D 生成兼顾结合与活性的候选小分子",
|
|
19
|
+
"num_initial": 3,
|
|
20
|
+
"seed": 42
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
只需检查参数时使用 validate;正式执行不要先 validate 再提交,避免无必要调用。
|
|
25
|
+
成功结果应包含与 `num_initial` 数量一致的候选 SMILES。
|
|
26
|
+
|
|
27
|
+
## 建议下一批候选
|
|
28
|
+
|
|
29
|
+
- 同步 operation:`molecules-suggest`
|
|
30
|
+
- 异步 operation:`molecules-suggest-job`(重型任务优先)
|
|
31
|
+
- 仅校验 operation:`molecules-suggest-validate`
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"task": "继续改善 KRAS G12D 的结合与活性",
|
|
36
|
+
"history_xs": ["CCO", "CCN", "CCC"],
|
|
37
|
+
"history_ys": [
|
|
38
|
+
[-6.8, 0.31],
|
|
39
|
+
[-7.1, 0.42],
|
|
40
|
+
[-6.5, 0.28]
|
|
41
|
+
],
|
|
42
|
+
"num_suggestions": 2,
|
|
43
|
+
"seed": 42
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`history_xs` 与 `history_ys` 必须等长;每个 `history_ys` 元素必须是两个有限数值,
|
|
48
|
+
顺序严格为 `[vina, activity]`。不得把预测值伪装成湿实验结果。
|
|
49
|
+
|
|
50
|
+
## 评估候选
|
|
51
|
+
|
|
52
|
+
- 同步 operation:`molecules-evaluate`
|
|
53
|
+
- 异步 operation:`molecules-evaluate-job`
|
|
54
|
+
- 仅校验 operation:`molecules-evaluate-validate`
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"designs": ["CCO", "CCN"]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
成功结果中的 score 数量必须与 designs 一致,每行必须恰好有两个有限数值。任一 item
|
|
63
|
+
明确失败时,本轮整体按失败处理,不得只报告成功子集。
|
|
64
|
+
|
|
65
|
+
## 异步作业
|
|
66
|
+
|
|
67
|
+
提交异步 operation 后,从当前 native 读取非空 `job_id`,再轮询:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
invoke("strbo", "job-status", params={"job_id": job_id})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
只有状态变为 `succeeded` 后才读取:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
invoke("strbo", "job-result", params={"job_id": job_id})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`queued` 或 `running` 不是完成;`failed` 立即结束。状态查询可按 5 秒左右间隔重复,
|
|
80
|
+
但提交 operation 绝不重复。单次用户任务的总等待上限为 15 分钟;超限后报告作业仍
|
|
81
|
+
未完成并保留 job_id,不得声称远端已取消。
|
|
82
|
+
|
|
83
|
+
## 完整科研流程
|
|
84
|
+
|
|
85
|
+
用户明确要求完整迭代时按 `初始化 → 评估 → 建议 → 评估` 串行执行。每一步只使用本次
|
|
86
|
+
调用已验证的 native 作为下一步输入。用户只要求生成初始候选或评估给定 SMILES 时,
|
|
87
|
+
不得擅自扩展为完整循环。
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
# SynBO
|
|
1
|
+
# SynBO operation 参考
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
所有在线调用都通过 `delta-science/scripts/invoke.py` 和 Delta CLI,服务地址来自当前
|
|
4
|
+
CLI 的 `science_base_url` 配置。这里的路径只说明业务 operation,不得据此直连 HTTP。
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
用户说“优化反应条件”“推荐下一批实验”“偶联反应怎么继续做”,或给出溶剂、温度、
|
|
7
|
+
催化剂、碱、配体和历史得率时,即使没有提到 SynBO、BO、CLI 或字段名也使用本节。
|
|
8
|
+
每个用户任务最多一次 SynBO 业务调用;成功后立即结束,validation、502、504、
|
|
9
|
+
timeout、断连或空 recommendations 均不得换参数、换模型或重试。
|
|
7
10
|
|
|
8
11
|
## Health
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
operation:`health`,无 body。
|
|
11
14
|
|
|
12
15
|
Reports SynBO import status, Torch/CUDA runtime availability, service limits,
|
|
13
16
|
default device, and visible CUDA devices.
|
|
@@ -43,7 +46,7 @@ Important fields inside gateway `data`:
|
|
|
43
46
|
|
|
44
47
|
## Initialize
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
operation:`initialize`。
|
|
47
50
|
|
|
48
51
|
Use for initial condition sampling when no measured previous results exist.
|
|
49
52
|
|
|
@@ -71,7 +74,7 @@ Supported `sampling_method` values: `sobol`, `random`, `lhs`, `kmeans`.
|
|
|
71
74
|
|
|
72
75
|
## Optimize
|
|
73
76
|
|
|
74
|
-
|
|
77
|
+
operation:`optimize`。
|
|
75
78
|
|
|
76
79
|
Use after measured experiment rows are available. Each previous row must contain
|
|
77
80
|
all condition columns and all objective metric columns.
|
|
@@ -93,11 +96,53 @@ Request body:
|
|
|
93
96
|
],
|
|
94
97
|
"batch_size": 2,
|
|
95
98
|
"accuracy": "tiny",
|
|
99
|
+
"surrogate_model": "RF",
|
|
100
|
+
"acq_func": "UCB",
|
|
96
101
|
"device": "cpu",
|
|
97
102
|
"random_seed": 42
|
|
98
103
|
}
|
|
99
104
|
```
|
|
100
105
|
|
|
106
|
+
### 数值条件必须先离散化
|
|
107
|
+
|
|
108
|
+
SynBO 的 `condition_dict` 只接受显式列表,不接受连续范围对象。对于“温度
|
|
109
|
+
40–100°C、催化剂用量 0.5–2.0%”并已有 60/70/80/90°C 与
|
|
110
|
+
0.5/1.0/1.5/2.0% 历史值的任务,使用:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"condition_dict": {
|
|
115
|
+
"solvent": ["ethanol", "acetonitrile", "DMSO"],
|
|
116
|
+
"temperature_C": ["40", "60", "70", "80", "90", "100"],
|
|
117
|
+
"catalyst_loading_pct": ["0.5", "1.0", "1.5", "2.0"]
|
|
118
|
+
},
|
|
119
|
+
"opt_metrics": ["yield"],
|
|
120
|
+
"previous_results": [
|
|
121
|
+
{
|
|
122
|
+
"batch": 0,
|
|
123
|
+
"solvent": "ethanol",
|
|
124
|
+
"temperature_C": "60",
|
|
125
|
+
"catalyst_loading_pct": "1.0",
|
|
126
|
+
"yield": 55.0
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
"batch_size": 3,
|
|
130
|
+
"accuracy": "tiny",
|
|
131
|
+
"surrogate_model": "RF",
|
|
132
|
+
"acq_func": "UCB",
|
|
133
|
+
"device": "cpu",
|
|
134
|
+
"random_seed": 42
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
约束:
|
|
139
|
+
|
|
140
|
+
- 每个条件值统一为字符串,objective metric 保持数值。
|
|
141
|
+
- 每条 `previous_results` 中的条件值必须出现在对应 `condition_dict` 列表中。
|
|
142
|
+
- 连续范围的离散网格必须包含用户上下界和全部历史观测值;最终答案必须说明实际网格。
|
|
143
|
+
- wrapper 会在发送前完成字符串归一化、成员校验,并为未显式指定的小数据 CPU
|
|
144
|
+
`optimize` 补充 `tiny + RF + UCB + cpu` 默认值。
|
|
145
|
+
|
|
101
146
|
Useful optional fields:
|
|
102
147
|
|
|
103
148
|
- `accuracy`: `tiny`, `low`, `medium`, `high`, or `ultra`.
|
|
@@ -137,7 +182,6 @@ Gateway errors have the standard envelope:
|
|
|
137
182
|
"message": "SynBO service returned HTTP 400",
|
|
138
183
|
"data": {
|
|
139
184
|
"endpoint": "/synbo/optimize",
|
|
140
|
-
"service_url": "http://synbo-service:8897",
|
|
141
185
|
"synbo_response": {
|
|
142
186
|
"detail": "previous_results is missing metric columns: ['yield']"
|
|
143
187
|
}
|
|
@@ -145,13 +189,9 @@ Gateway errors have the standard envelope:
|
|
|
145
189
|
}
|
|
146
190
|
```
|
|
147
191
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
values, and requesting CUDA when no GPU is available.
|
|
192
|
+
常见原因包括条件空间过大、条件值重复、历史条件值不在 condition_dict 中、
|
|
193
|
+
descriptor 非数值、历史行缺列、metric 非数值,以及无 GPU 时请求 CUDA。
|
|
151
194
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
(GP + EHVI, higher accuracy) or when a concurrent request is occupying the
|
|
156
|
-
shared GPU. Retry once the service is idle, or lower `accuracy`, narrow the
|
|
157
|
-
reaction space, or use a GPU device.
|
|
195
|
+
服务端有独立于客户端的单次请求时限。GP、EHVI、高 accuracy 或共享资源繁忙可能导致
|
|
196
|
+
timeout。普通用户任务中不得自动换参数重试;应报告“未产生推荐”,保留本次错误,
|
|
197
|
+
由用户决定是否另起一次明确的回归测试。
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# 跨服务工作流
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
`
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
整个流程由单一 `delta-science` Skill 编排。每一步都通过宿主可用的 shell/Python 调用
|
|
4
|
+
当前 Skill 目录下的 `scripts/invoke.py`,禁止再派发到其他 Science Skill、通用 subagent
|
|
5
|
+
或自由文本执行器;禁止直接 HTTP。wrapper 成功后每一步只消费 `result["native"]` 顶层。
|
|
6
|
+
已知 reference 中没有目标 operation 时,不猜测相近
|
|
7
|
+
endpoint;只有用户明确点名新工具时,才按 `SKILL.md` 的只读实时 catalog 流程确认。
|
|
8
8
|
|
|
9
9
|
## 单个已命名分子
|
|
10
10
|
|
|
@@ -37,8 +37,25 @@ subagent 或自由文本执行器;禁止 Bash、目录探测和直接 HTTP。w
|
|
|
37
37
|
## 优化工作流
|
|
38
38
|
|
|
39
39
|
- 通用数值或分类实验变量:Delta-BO
|
|
40
|
-
-
|
|
41
|
-
-
|
|
40
|
+
- 固定 SMILES 候选池或 PDF2Dock/Vina+NN 轨迹:LDM-BO
|
|
41
|
+
- 固定 KRAS G12D 的生成式小分子闭环:STRBO
|
|
42
|
+
- 指定抗原的 11-aa CDRH3 闭环:AntBO
|
|
43
|
+
- 偶联反应、反应条件和带真实历史得率的下一批实验推荐:SynBO
|
|
44
|
+
|
|
45
|
+
用户以 “LDM/大发现模型” 描述任务时,必须先按 `SKILL.md` 读取 `ldm.md`,再读取唯一
|
|
46
|
+
选定后端的 reference。LDM 是工作流意图,不是 tool;禁止旧
|
|
47
|
+
`large-discovery-model`、`run_ldm_loop.py`、gateway backend 和直接 HTTP。
|
|
42
48
|
|
|
43
49
|
禁止为了让优化器运行而编造观测数据。用户没有提供测量结果时,只能使用初始化或随机建议模式,并明确说明它是待实验验证的建议,不是已经验证的最优条件。
|
|
44
50
|
SynBO/Delta-BO 没有返回 recommendations 时,不得用本地回归、GP、UCB 或人工排序替代。
|
|
51
|
+
|
|
52
|
+
SynBO 只接受离散条件列表。用户给连续范围时,在一次调用前完成以下处理:
|
|
53
|
+
|
|
54
|
+
1. 条件网格包含用户上下界、全部历史观测值和少量合理中间点。
|
|
55
|
+
2. 同一条件列在 `condition_dict` 与 `previous_results` 中统一为字符串。
|
|
56
|
+
3. 得率等 objective metric 保持数值。
|
|
57
|
+
4. 小数据 CPU 默认 `tiny + RF + UCB`,并在最终答案中报告实际离散网格。
|
|
58
|
+
5. wrapper 返回失败或空 recommendations 时结束;不得连续尝试多种 payload 或模型。
|
|
59
|
+
|
|
60
|
+
用户未要求文件时只返回 recommendations 文本,不创建输入 CSV。必须交接时,仅把本次
|
|
61
|
+
成功 native 写入当前 `WORKSPACE_ROOT/synbo_result.json`。
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read the authoritative Delta Science catalog through delta-cli."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
from invoke import _parse_cli_error, _resolve_cli, _runtime_config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _parser() -> argparse.ArgumentParser:
|
|
17
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
18
|
+
action = parser.add_mutually_exclusive_group(required=True)
|
|
19
|
+
action.add_argument("--tools", action="store_true")
|
|
20
|
+
action.add_argument("--endpoints", metavar="TOOL")
|
|
21
|
+
parser.add_argument("--cli")
|
|
22
|
+
parser.add_argument("--science-base-url")
|
|
23
|
+
parser.add_argument("--timeout", type=float, default=30.0)
|
|
24
|
+
return parser
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> int:
|
|
28
|
+
args = _parser().parse_args()
|
|
29
|
+
started = time.perf_counter()
|
|
30
|
+
kind = "tools" if args.tools else "endpoints"
|
|
31
|
+
try:
|
|
32
|
+
runtime = _runtime_config()
|
|
33
|
+
cli = _resolve_cli(args.cli, runtime)
|
|
34
|
+
env = os.environ.copy()
|
|
35
|
+
base_url = args.science_base_url or runtime.get("science_base_url")
|
|
36
|
+
if base_url:
|
|
37
|
+
env["DELTA_INFRA_SCIENCE_BASE_URL"] = str(base_url)
|
|
38
|
+
|
|
39
|
+
command = [cli, "science", "list"]
|
|
40
|
+
if args.endpoints:
|
|
41
|
+
command = [cli, "science", "endpoints", "list", args.endpoints]
|
|
42
|
+
completed = subprocess.run(
|
|
43
|
+
command,
|
|
44
|
+
capture_output=True,
|
|
45
|
+
text=True,
|
|
46
|
+
encoding="utf-8",
|
|
47
|
+
errors="replace",
|
|
48
|
+
env=env,
|
|
49
|
+
timeout=args.timeout,
|
|
50
|
+
check=False,
|
|
51
|
+
)
|
|
52
|
+
if completed.returncode != 0:
|
|
53
|
+
error_type, detail = _parse_cli_error(completed)
|
|
54
|
+
raise RuntimeError(f"{error_type}: {detail}")
|
|
55
|
+
root = json.loads(completed.stdout)
|
|
56
|
+
if not isinstance(root, dict) or root.get("ok") is not True:
|
|
57
|
+
raise RuntimeError(json.dumps(root, ensure_ascii=False))
|
|
58
|
+
if "data" not in root:
|
|
59
|
+
raise RuntimeError("CLI success envelope is missing data")
|
|
60
|
+
print(json.dumps({
|
|
61
|
+
"ok": True,
|
|
62
|
+
"transport": "delta-cli",
|
|
63
|
+
"catalog": kind,
|
|
64
|
+
"tool": args.endpoints,
|
|
65
|
+
"elapsed_seconds": round(time.perf_counter() - started, 3),
|
|
66
|
+
"native": root["data"],
|
|
67
|
+
}, ensure_ascii=False))
|
|
68
|
+
return 0
|
|
69
|
+
except subprocess.TimeoutExpired:
|
|
70
|
+
error_type = "timeout"
|
|
71
|
+
error = f"timeout after {args.timeout}s"
|
|
72
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
73
|
+
error_type = "catalog"
|
|
74
|
+
error = str(exc)
|
|
75
|
+
except RuntimeError as exc:
|
|
76
|
+
error_type = "cli"
|
|
77
|
+
error = str(exc)
|
|
78
|
+
|
|
79
|
+
print(json.dumps({
|
|
80
|
+
"ok": False,
|
|
81
|
+
"transport": "delta-cli",
|
|
82
|
+
"catalog": kind,
|
|
83
|
+
"tool": args.endpoints,
|
|
84
|
+
"error_type": error_type,
|
|
85
|
+
"elapsed_seconds": round(time.perf_counter() - started, 3),
|
|
86
|
+
"error": error[:2000],
|
|
87
|
+
}, ensure_ascii=False))
|
|
88
|
+
return 1
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
sys.exit(main())
|
|
@@ -12,12 +12,17 @@ import subprocess
|
|
|
12
12
|
import sys
|
|
13
13
|
import time
|
|
14
14
|
from typing import Any
|
|
15
|
-
from urllib.parse import urlsplit
|
|
16
|
-
|
|
17
|
-
from generated_catalog import LEGACY_ENDPOINTS, LEGACY_TOOL_ALIASES
|
|
18
|
-
|
|
19
15
|
|
|
20
16
|
SKILL_DIR = Path(__file__).resolve().parents[1]
|
|
17
|
+
_CLI_ERROR_TYPES = {
|
|
18
|
+
2: "validation",
|
|
19
|
+
3: "auth",
|
|
20
|
+
4: "permission",
|
|
21
|
+
5: "not_found",
|
|
22
|
+
6: "network",
|
|
23
|
+
7: "api",
|
|
24
|
+
10: "internal",
|
|
25
|
+
}
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
def _json_object(raw: str | None, flag: str) -> dict[str, Any] | None:
|
|
@@ -32,6 +37,108 @@ def _json_object(raw: str | None, flag: str) -> dict[str, Any] | None:
|
|
|
32
37
|
return value
|
|
33
38
|
|
|
34
39
|
|
|
40
|
+
def _normalize_synbo_payload(
|
|
41
|
+
tool: str,
|
|
42
|
+
endpoint: str,
|
|
43
|
+
data: dict[str, Any] | None,
|
|
44
|
+
) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
|
|
45
|
+
"""Normalize SynBO's discrete condition contract before any remote request."""
|
|
46
|
+
canonical_tool = tool.removesuffix("-service")
|
|
47
|
+
if canonical_tool != "synbo" or endpoint not in {"initialize", "optimize"}:
|
|
48
|
+
return data, None
|
|
49
|
+
if data is None:
|
|
50
|
+
raise ValueError(f"synbo/{endpoint} requires --data-json")
|
|
51
|
+
|
|
52
|
+
condition_dict = data.get("condition_dict")
|
|
53
|
+
if not isinstance(condition_dict, dict) or not condition_dict:
|
|
54
|
+
raise ValueError("SynBO condition_dict must be a non-empty object")
|
|
55
|
+
|
|
56
|
+
normalized_conditions: dict[str, list[str]] = {}
|
|
57
|
+
for name, raw_values in condition_dict.items():
|
|
58
|
+
if not isinstance(raw_values, list) or not raw_values:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"SynBO condition_dict.{name} must be a non-empty explicit list; "
|
|
61
|
+
"range objects are not supported"
|
|
62
|
+
)
|
|
63
|
+
values = [str(value) for value in raw_values]
|
|
64
|
+
if any(value == "" for value in values):
|
|
65
|
+
raise ValueError(f"SynBO condition_dict.{name} contains an empty value")
|
|
66
|
+
if len(set(values)) != len(values):
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f"SynBO condition_dict.{name} contains duplicate values after "
|
|
69
|
+
"string normalization"
|
|
70
|
+
)
|
|
71
|
+
normalized_conditions[str(name)] = values
|
|
72
|
+
|
|
73
|
+
normalized = dict(data)
|
|
74
|
+
normalized["condition_dict"] = normalized_conditions
|
|
75
|
+
normalization = {
|
|
76
|
+
"condition_values": "strings",
|
|
77
|
+
"condition_space_size": _condition_space_size(normalized_conditions),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if endpoint == "initialize":
|
|
81
|
+
return normalized, normalization
|
|
82
|
+
|
|
83
|
+
metrics = normalized.get("opt_metrics")
|
|
84
|
+
if not isinstance(metrics, list) or not metrics:
|
|
85
|
+
raise ValueError("SynBO optimize requires a non-empty opt_metrics list")
|
|
86
|
+
metric_names = [str(metric) for metric in metrics]
|
|
87
|
+
normalized["opt_metrics"] = metric_names
|
|
88
|
+
|
|
89
|
+
previous_results = normalized.get("previous_results")
|
|
90
|
+
if not isinstance(previous_results, list) or not previous_results:
|
|
91
|
+
raise ValueError("SynBO optimize requires non-empty previous_results")
|
|
92
|
+
|
|
93
|
+
normalized_rows: list[dict[str, Any]] = []
|
|
94
|
+
for index, raw_row in enumerate(previous_results):
|
|
95
|
+
if not isinstance(raw_row, dict):
|
|
96
|
+
raise ValueError(f"SynBO previous_results[{index}] must be an object")
|
|
97
|
+
row = dict(raw_row)
|
|
98
|
+
for condition_name, allowed_values in normalized_conditions.items():
|
|
99
|
+
if condition_name not in row:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"SynBO previous_results[{index}] is missing condition "
|
|
102
|
+
f"{condition_name!r}"
|
|
103
|
+
)
|
|
104
|
+
value = str(row[condition_name])
|
|
105
|
+
if value not in allowed_values:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"SynBO previous_results[{index}].{condition_name}={value!r} "
|
|
108
|
+
"is not present in condition_dict"
|
|
109
|
+
)
|
|
110
|
+
row[condition_name] = value
|
|
111
|
+
for metric_name in metric_names:
|
|
112
|
+
metric_value = row.get(metric_name)
|
|
113
|
+
if (
|
|
114
|
+
metric_name not in row
|
|
115
|
+
or isinstance(metric_value, bool)
|
|
116
|
+
or not isinstance(metric_value, (int, float))
|
|
117
|
+
):
|
|
118
|
+
raise ValueError(
|
|
119
|
+
f"SynBO previous_results[{index}].{metric_name} must be numeric"
|
|
120
|
+
)
|
|
121
|
+
normalized_rows.append(row)
|
|
122
|
+
|
|
123
|
+
normalized["previous_results"] = normalized_rows
|
|
124
|
+
normalized.setdefault("accuracy", "tiny")
|
|
125
|
+
normalized.setdefault("device", "cpu")
|
|
126
|
+
normalized.setdefault("surrogate_model", "RF")
|
|
127
|
+
normalized.setdefault("acq_func", "UCB")
|
|
128
|
+
normalization["defaults"] = {
|
|
129
|
+
key: normalized[key]
|
|
130
|
+
for key in ("accuracy", "device", "surrogate_model", "acq_func")
|
|
131
|
+
}
|
|
132
|
+
return normalized, normalization
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _condition_space_size(condition_dict: dict[str, list[str]]) -> int:
|
|
136
|
+
size = 1
|
|
137
|
+
for values in condition_dict.values():
|
|
138
|
+
size *= len(values)
|
|
139
|
+
return size
|
|
140
|
+
|
|
141
|
+
|
|
35
142
|
def _runtime_config() -> dict[str, Any]:
|
|
36
143
|
path = SKILL_DIR / "runtime.local.json"
|
|
37
144
|
if not path.is_file():
|
|
@@ -63,55 +170,6 @@ def _resolve_cli(explicit: str | None, runtime: dict[str, Any]) -> str:
|
|
|
63
170
|
)
|
|
64
171
|
|
|
65
172
|
|
|
66
|
-
def _cli_science_base_url(cli: str, env: dict[str, str], timeout: float) -> str:
|
|
67
|
-
explicit = env.get("DELTA_INFRA_SCIENCE_BASE_URL", "").strip()
|
|
68
|
-
if explicit:
|
|
69
|
-
return explicit
|
|
70
|
-
completed = subprocess.run(
|
|
71
|
-
[cli, "config", "show"],
|
|
72
|
-
capture_output=True,
|
|
73
|
-
text=True,
|
|
74
|
-
encoding="utf-8",
|
|
75
|
-
errors="replace",
|
|
76
|
-
env=env,
|
|
77
|
-
timeout=min(timeout, 15.0),
|
|
78
|
-
check=False,
|
|
79
|
-
)
|
|
80
|
-
if completed.returncode != 0:
|
|
81
|
-
return ""
|
|
82
|
-
try:
|
|
83
|
-
root = json.loads(completed.stdout)
|
|
84
|
-
value = root.get("data", {}).get("science_base_url", "")
|
|
85
|
-
except (AttributeError, json.JSONDecodeError):
|
|
86
|
-
return ""
|
|
87
|
-
return str(value).strip()
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def _catalog_profile(base_url: str, requested: str) -> str:
|
|
91
|
-
if requested != "auto":
|
|
92
|
-
return requested
|
|
93
|
-
try:
|
|
94
|
-
path = urlsplit(base_url).path.rstrip("/").lower()
|
|
95
|
-
except ValueError:
|
|
96
|
-
path = ""
|
|
97
|
-
return "legacy" if path.endswith("/science_tool") else "canonical"
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _resolve_catalog_names(tool: str, endpoint: str, profile: str) -> tuple[str, str]:
|
|
101
|
-
if profile == "canonical":
|
|
102
|
-
return tool, endpoint
|
|
103
|
-
canonical_tool = tool.removesuffix("-service") if tool in {"antbo-service", "synbo-service"} else tool
|
|
104
|
-
resolved_tool = LEGACY_TOOL_ALIASES.get(canonical_tool, canonical_tool)
|
|
105
|
-
if endpoint.startswith(("chem_", "biology_")):
|
|
106
|
-
return resolved_tool, endpoint
|
|
107
|
-
resolved_endpoint = LEGACY_ENDPOINTS.get(canonical_tool, {}).get(endpoint)
|
|
108
|
-
if not resolved_endpoint:
|
|
109
|
-
raise ValueError(
|
|
110
|
-
f"no legacy science_tool mapping for {canonical_tool}/{endpoint}"
|
|
111
|
-
)
|
|
112
|
-
return resolved_tool, resolved_endpoint
|
|
113
|
-
|
|
114
|
-
|
|
115
173
|
def _unwrap(stdout: str) -> tuple[Any, int]:
|
|
116
174
|
try:
|
|
117
175
|
root = json.loads(stdout)
|
|
@@ -155,6 +213,32 @@ def _unwrap(stdout: str) -> tuple[Any, int]:
|
|
|
155
213
|
return value, depth
|
|
156
214
|
|
|
157
215
|
|
|
216
|
+
def _validate_native(tool: str, endpoint: str, native: Any) -> None:
|
|
217
|
+
canonical_tool = tool.removesuffix("-service")
|
|
218
|
+
if canonical_tool == "synbo" and endpoint == "optimize":
|
|
219
|
+
if not isinstance(native, dict):
|
|
220
|
+
raise RuntimeError("SynBO optimize returned a non-object result")
|
|
221
|
+
recommendations = native.get("recommendations")
|
|
222
|
+
if not isinstance(recommendations, list) or not recommendations:
|
|
223
|
+
raise RuntimeError("SynBO optimize returned no recommendations")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _parse_cli_error(completed: subprocess.CompletedProcess[str]) -> tuple[str, str]:
|
|
227
|
+
detail = completed.stderr.strip() or completed.stdout.strip()
|
|
228
|
+
error_type = _CLI_ERROR_TYPES.get(completed.returncode, "cli")
|
|
229
|
+
try:
|
|
230
|
+
envelope = json.loads(detail)
|
|
231
|
+
error = envelope.get("error", {}) if isinstance(envelope, dict) else {}
|
|
232
|
+
if isinstance(error, dict):
|
|
233
|
+
error_type = str(error.get("type") or error_type)
|
|
234
|
+
message = error.get("message")
|
|
235
|
+
if message:
|
|
236
|
+
detail = str(message)
|
|
237
|
+
except (AttributeError, json.JSONDecodeError):
|
|
238
|
+
pass
|
|
239
|
+
return error_type, detail
|
|
240
|
+
|
|
241
|
+
|
|
158
242
|
def _parser() -> argparse.ArgumentParser:
|
|
159
243
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
160
244
|
parser.add_argument("--tool", required=True)
|
|
@@ -164,11 +248,6 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
164
248
|
body.add_argument("--params-json")
|
|
165
249
|
parser.add_argument("--cli")
|
|
166
250
|
parser.add_argument("--science-base-url")
|
|
167
|
-
parser.add_argument(
|
|
168
|
-
"--catalog-profile",
|
|
169
|
-
choices=("auto", "canonical", "legacy"),
|
|
170
|
-
default="auto",
|
|
171
|
-
)
|
|
172
251
|
parser.add_argument("--timeout", type=float, default=120.0)
|
|
173
252
|
return parser
|
|
174
253
|
|
|
@@ -177,12 +256,15 @@ def main() -> int:
|
|
|
177
256
|
args = _parser().parse_args()
|
|
178
257
|
started = time.perf_counter()
|
|
179
258
|
stage = "arguments"
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
259
|
+
cli_exit_code: int | None = None
|
|
260
|
+
error_type = "unknown"
|
|
261
|
+
normalization: dict[str, Any] | None = None
|
|
183
262
|
try:
|
|
184
263
|
data = _json_object(args.data_json, "--data-json")
|
|
185
264
|
params = _json_object(args.params_json, "--params-json")
|
|
265
|
+
data, normalization = _normalize_synbo_payload(
|
|
266
|
+
args.tool, args.endpoint, data
|
|
267
|
+
)
|
|
186
268
|
runtime = _runtime_config()
|
|
187
269
|
stage = "cli-resolution"
|
|
188
270
|
cli = _resolve_cli(args.cli, runtime)
|
|
@@ -191,19 +273,11 @@ def main() -> int:
|
|
|
191
273
|
base_url = args.science_base_url or runtime.get("science_base_url")
|
|
192
274
|
if base_url:
|
|
193
275
|
env["DELTA_INFRA_SCIENCE_BASE_URL"] = str(base_url)
|
|
194
|
-
configured_base_url = _cli_science_base_url(cli, env, args.timeout)
|
|
195
|
-
requested_profile = str(runtime.get("catalog_profile", args.catalog_profile))
|
|
196
|
-
if requested_profile not in {"auto", "canonical", "legacy"}:
|
|
197
|
-
raise ValueError("catalog_profile must be auto, canonical, or legacy")
|
|
198
|
-
profile = _catalog_profile(configured_base_url, requested_profile)
|
|
199
|
-
resolved_tool, resolved_endpoint = _resolve_catalog_names(
|
|
200
|
-
args.tool, args.endpoint, profile
|
|
201
|
-
)
|
|
202
276
|
|
|
203
277
|
command = [
|
|
204
278
|
cli, "science", "invoke",
|
|
205
|
-
"--tool",
|
|
206
|
-
"--endpoint",
|
|
279
|
+
"--tool", args.tool,
|
|
280
|
+
"--endpoint", args.endpoint,
|
|
207
281
|
]
|
|
208
282
|
if data is not None:
|
|
209
283
|
command.extend(["--data", json.dumps(data, ensure_ascii=False, separators=(",", ":"))])
|
|
@@ -222,37 +296,46 @@ def main() -> int:
|
|
|
222
296
|
check=False,
|
|
223
297
|
)
|
|
224
298
|
if completed.returncode != 0:
|
|
225
|
-
|
|
299
|
+
cli_exit_code = completed.returncode
|
|
300
|
+
error_type, detail = _parse_cli_error(completed)
|
|
226
301
|
raise RuntimeError(f"exit_code={completed.returncode}: {detail}")
|
|
227
302
|
stage = "response-validation"
|
|
228
303
|
native, depth = _unwrap(completed.stdout)
|
|
304
|
+
_validate_native(args.tool, args.endpoint, native)
|
|
229
305
|
result = {
|
|
230
306
|
"ok": True,
|
|
231
307
|
"transport": "delta-cli",
|
|
232
308
|
"tool": args.tool,
|
|
233
309
|
"endpoint": args.endpoint,
|
|
234
|
-
"resolved_tool": resolved_tool,
|
|
235
|
-
"resolved_endpoint": resolved_endpoint,
|
|
236
|
-
"catalog_profile": profile,
|
|
237
310
|
"elapsed_seconds": round(time.perf_counter() - started, 3),
|
|
238
311
|
"envelope_depth": depth,
|
|
239
312
|
"native": native,
|
|
240
313
|
}
|
|
314
|
+
if normalization is not None:
|
|
315
|
+
result["request_normalization"] = normalization
|
|
241
316
|
print(json.dumps(result, ensure_ascii=False))
|
|
242
317
|
return 0
|
|
243
318
|
except subprocess.TimeoutExpired:
|
|
319
|
+
error_type = "timeout"
|
|
244
320
|
error = f"timeout after {args.timeout}s"
|
|
245
|
-
except
|
|
321
|
+
except ValueError as exc:
|
|
322
|
+
error_type = "validation"
|
|
323
|
+
error = str(exc)
|
|
324
|
+
except OSError as exc:
|
|
325
|
+
error_type = "cli_resolution"
|
|
326
|
+
error = str(exc)
|
|
327
|
+
except RuntimeError as exc:
|
|
328
|
+
if error_type == "unknown":
|
|
329
|
+
error_type = "response_validation"
|
|
246
330
|
error = str(exc)
|
|
247
331
|
print(json.dumps({
|
|
248
332
|
"ok": False,
|
|
249
333
|
"transport": "delta-cli",
|
|
250
334
|
"tool": args.tool,
|
|
251
335
|
"endpoint": args.endpoint,
|
|
252
|
-
"resolved_tool": resolved_tool,
|
|
253
|
-
"resolved_endpoint": resolved_endpoint,
|
|
254
|
-
"catalog_profile": profile,
|
|
255
336
|
"stage": stage,
|
|
337
|
+
"error_type": error_type,
|
|
338
|
+
"cli_exit_code": cli_exit_code,
|
|
256
339
|
"elapsed_seconds": round(time.perf_counter() - started, 3),
|
|
257
340
|
"error": error[:2000],
|
|
258
341
|
}, ensure_ascii=False))
|