|
| 1 | +--- |
| 2 | +name: create-device-skill |
| 3 | +description: Create a skill for any Uni-Lab device by extracting action schemas from the device registry. Use when the user wants to create a new device skill, add device API documentation, or set up action schemas for a device. |
| 4 | +--- |
| 5 | + |
| 6 | +# 创建设备 Skill 指南 |
| 7 | + |
| 8 | +本 meta-skill 教你如何为任意 Uni-Lab-OS 设备创建完整的 API 操作技能(参考 `unilab-device-api` 的成功案例)。 |
| 9 | + |
| 10 | +## 数据源 |
| 11 | + |
| 12 | +- **设备注册表**: `unilabos_data/req_device_registry_upload.json` |
| 13 | +- **结构**: `{ "resources": [{ "id": "<device_id>", "class": { "module": "<python_module:ClassName>", "action_value_mappings": { ... } } }] }` |
| 14 | +- **生成时机**: `unilab` 启动并完成注册表上传后自动生成 |
| 15 | +- **module 字段**: 格式 `unilabos.devices.xxx.yyy:ClassName`,可转为源码路径 `unilabos/devices/xxx/yyy.py`,阅读源码可了解参数含义和设备行为 |
| 16 | + |
| 17 | +## 创建流程 |
| 18 | + |
| 19 | +### Step 0 — 收集必备信息(缺一不可,否则询问后终止) |
| 20 | + |
| 21 | +开始前**必须**确认以下 4 项信息全部就绪。如果用户未提供任何一项,**立即询问并终止当前流程**,等用户补齐后再继续。 |
| 22 | + |
| 23 | +向用户提问:「请提供你的 unilab 启动参数,我需要以下信息:」 |
| 24 | + |
| 25 | +#### 必备项 ①:ak / sk(认证凭据) |
| 26 | + |
| 27 | +来源:启动命令的 `--ak` `--sk` 参数,或 config.py 中的 `ak = "..."` `sk = "..."`。 |
| 28 | + |
| 29 | +获取后立即生成 AUTH token: |
| 30 | + |
| 31 | +```bash |
| 32 | +python ./scripts/gen_auth.py <ak> <sk> |
| 33 | +# 或从 config.py 提取 |
| 34 | +python ./scripts/gen_auth.py --config <config.py> |
| 35 | +``` |
| 36 | + |
| 37 | +认证算法:`base64(ak:sk)` → `Authorization: Lab <token>` |
| 38 | + |
| 39 | +#### 必备项 ②:--addr(目标环境) |
| 40 | + |
| 41 | +决定 API 请求发往哪个服务器。从启动命令的 `--addr` 参数获取: |
| 42 | + |
| 43 | +| `--addr` 值 | BASE URL | |
| 44 | +|-------------|----------| |
| 45 | +| `test` | `https://uni-lab.test.bohrium.com` | |
| 46 | +| `uat` | `https://uni-lab.uat.bohrium.com` | |
| 47 | +| `local` | `http://127.0.0.1:48197` | |
| 48 | +| 不传(默认) | `https://uni-lab.bohrium.com` | |
| 49 | +| 其他自定义 URL | 直接使用该 URL | |
| 50 | + |
| 51 | +#### 必备项 ③:req_device_registry_upload.json(设备注册表) |
| 52 | + |
| 53 | +数据文件由 `unilab` 启动时自动生成,需要定位它: |
| 54 | + |
| 55 | +**推断 working_dir**(即 `unilabos_data` 所在目录): |
| 56 | + |
| 57 | +| 条件 | working_dir 取值 | |
| 58 | +|------|------------------| |
| 59 | +| 传了 `--working_dir` | `<working_dir>/unilabos_data/`(若子目录已存在则直接用) | |
| 60 | +| 仅传了 `--config` | `<config 文件所在目录>/unilabos_data/` | |
| 61 | +| 都没传 | `<当前工作目录>/unilabos_data/` | |
| 62 | + |
| 63 | +**按优先级搜索文件**: |
| 64 | + |
| 65 | +``` |
| 66 | +<推断的 working_dir>/unilabos_data/req_device_registry_upload.json |
| 67 | +<推断的 working_dir>/req_device_registry_upload.json |
| 68 | +<workspace 根目录>/unilabos_data/req_device_registry_upload.json |
| 69 | +``` |
| 70 | + |
| 71 | +也可以直接 Glob 搜索:`**/req_device_registry_upload.json` |
| 72 | + |
| 73 | +找到后**必须检查文件修改时间**并告知用户:「找到注册表文件 `<路径>`,生成于 `<时间>`。请确认这是最近一次启动生成的。」超过 1 天提醒用户是否需要重新启动 `unilab`。 |
| 74 | + |
| 75 | +**如果文件不存在** → 告知用户先运行 `unilab` 启动命令,等日志出现 `注册表响应数据已保存` 后再执行本流程。**终止。** |
| 76 | + |
| 77 | +#### 必备项 ④:目标设备 |
| 78 | + |
| 79 | +用户需要明确要为哪个设备创建 skill。可以是设备名称(如「PRCXI 移液站」)或 device_id(如 `liquid_handler.prcxi`)。 |
| 80 | + |
| 81 | +如果用户不确定,运行提取脚本列出所有设备供选择: |
| 82 | + |
| 83 | +```bash |
| 84 | +python ./scripts/extract_device_actions.py --registry <找到的文件路径> |
| 85 | +``` |
| 86 | + |
| 87 | +#### 完整示例 |
| 88 | + |
| 89 | +用户提供: |
| 90 | + |
| 91 | +``` |
| 92 | +--ak a1fd9d4e-xxxx-xxxx-xxxx-d9a69c09f0fd |
| 93 | +--sk 136ff5c6-xxxx-xxxx-xxxx-a03e301f827b |
| 94 | +--addr test |
| 95 | +--port 8003 |
| 96 | +--disable_browser |
| 97 | +``` |
| 98 | + |
| 99 | +从中提取: |
| 100 | +- ✅ ak/sk → 运行 `gen_auth.py` 得到 `AUTH="Authorization: Lab YTFmZDlk..."` |
| 101 | +- ✅ addr=test → `BASE=https://uni-lab.test.bohrium.com` |
| 102 | +- ✅ 搜索 `unilabos_data/req_device_registry_upload.json` → 找到并确认时间 |
| 103 | +- ✅ 用户指明目标设备 → 如 `liquid_handler.prcxi` |
| 104 | + |
| 105 | +**四项全部就绪后才进入 Step 1。** |
| 106 | + |
| 107 | +### Step 1 — 列出可用设备 |
| 108 | + |
| 109 | +运行提取脚本,列出所有设备及 action 数量和 Python 源码路径,让用户选择: |
| 110 | + |
| 111 | +```bash |
| 112 | +# 自动搜索(默认在 unilabos_data/ 和当前目录查找) |
| 113 | +python ./scripts/extract_device_actions.py |
| 114 | + |
| 115 | +# 指定注册表文件路径 |
| 116 | +python ./scripts/extract_device_actions.py --registry <path/to/req_device_registry_upload.json> |
| 117 | +``` |
| 118 | + |
| 119 | +脚本输出包含每个设备的 **Python 源码路径**(从 `class.module` 转换),可用于后续阅读源码理解参数含义。 |
| 120 | + |
| 121 | +### Step 2 — 提取 Action Schema |
| 122 | + |
| 123 | +用户选择设备后,运行提取脚本: |
| 124 | + |
| 125 | +```bash |
| 126 | +python ./scripts/extract_device_actions.py [--registry <path>] <device_id> ./skills/<skill-name>/actions/ |
| 127 | +``` |
| 128 | + |
| 129 | +脚本会显示设备的 Python 源码路径和类名,方便阅读源码了解参数含义。 |
| 130 | + |
| 131 | +每个 action 生成一个 JSON 文件,包含: |
| 132 | +- `type` — 作为 API 调用的 `action_type` |
| 133 | +- `schema` — 完整 JSON Schema(含 `properties.goal.properties` 参数定义) |
| 134 | +- `goal` — goal 字段映射(含占位符 `$placeholder`) |
| 135 | +- `goal_default` — 默认值 |
| 136 | + |
| 137 | +### Step 3 — 写 action-index.md |
| 138 | + |
| 139 | +按模板为每个 action 写条目: |
| 140 | + |
| 141 | +```markdown |
| 142 | +### `<action_name>` |
| 143 | + |
| 144 | +<用途描述(一句话)> |
| 145 | + |
| 146 | +- **Schema**: [`actions/<filename>.json`](actions/<filename>.json) |
| 147 | +- **核心参数**: `param1`, `param2`(从 schema.required 获取) |
| 148 | +- **可选参数**: `param3`, `param4` |
| 149 | +- **占位符字段**: `field`(需填入物料信息,值以 `$` 开头) |
| 150 | +``` |
| 151 | + |
| 152 | +描述规则: |
| 153 | +- 从 `schema.properties` 读参数列表(schema 已提升为 goal 内容) |
| 154 | +- 从 `schema.required` 区分核心/可选参数 |
| 155 | +- 按功能分类(移液、枪头、外设等) |
| 156 | +- 标注 `placeholder_keys` 中的字段类型: |
| 157 | + - `unilabos_resources` → **ResourceSlot**,填入 `{id, name, uuid}`(id 是路径格式,从资源树取物料节点) |
| 158 | + - `unilabos_devices` → **DeviceSlot**,填入路径字符串如 `"/host_node"`(从资源树筛选 type=device) |
| 159 | + - `unilabos_nodes` → **NodeSlot**,填入路径字符串如 `"/PRCXI/PRCXI_Deck"`(资源树中任意节点) |
| 160 | + - `unilabos_class` → **ClassSlot**,填入类名字符串如 `"container"`(从注册表查找) |
| 161 | +- array 类型字段 → `[{id, name, uuid}, ...]` |
| 162 | +- 特殊:`create_resource` 的 `res_id`(ResourceSlot)可填不存在的路径 |
| 163 | + |
| 164 | +### Step 4 — 写 SKILL.md |
| 165 | + |
| 166 | +直接复用 `unilab-device-api` 的 API 模板(10 个 endpoint),修改: |
| 167 | +- 设备名称 |
| 168 | +- Action 数量 |
| 169 | +- 目录列表 |
| 170 | +- Session state 中的 `device_name` |
| 171 | +- **AUTH 头** — 使用 Step 0 中 `gen_auth.py` 生成的 `Authorization: Lab <token>`(不要硬编码 `Api` 类型的 key) |
| 172 | +- **Python 源码路径** — 在 SKILL.md 开头注明设备对应的源码文件,方便参考参数含义 |
| 173 | +- **Slot 字段表** — 列出本设备哪些 action 的哪些字段需要填入 Slot(物料/设备/节点/类名) |
| 174 | + |
| 175 | +API 模板结构: |
| 176 | + |
| 177 | +```markdown |
| 178 | +## 设备信息 |
| 179 | +- device_id, Python 源码路径, 设备类名 |
| 180 | + |
| 181 | +## 前置条件(缺一不可) |
| 182 | +- ak/sk → AUTH, --addr → BASE URL |
| 183 | + |
| 184 | +## Session State |
| 185 | +- lab_uuid(通过 API #1 自动匹配,不要问用户), device_name |
| 186 | + |
| 187 | +## API Endpoints (10 个) |
| 188 | +# 注意: |
| 189 | +# - #1 获取 lab 列表 + 自动匹配 lab_uuid(遍历 is_admin 的 lab, |
| 190 | +# 调用 /lab/info/{uuid} 比对 access_key == ak) |
| 191 | +# - #2 创建工作流用 POST /lab/workflow |
| 192 | +# - #10 获取资源树路径含 lab_uuid: /lab/material/download/{lab_uuid} |
| 193 | + |
| 194 | +## Placeholder Slot 填写规则 |
| 195 | +- unilabos_resources → ResourceSlot → {"id":"/path/name","name":"name","uuid":"xxx"} |
| 196 | +- unilabos_devices → DeviceSlot → "/parent/device" 路径字符串 |
| 197 | +- unilabos_nodes → NodeSlot → "/parent/node" 路径字符串 |
| 198 | +- unilabos_class → ClassSlot → "class_name" 字符串 |
| 199 | +- 特例:create_resource 的 res_id 允许填不存在的路径 |
| 200 | +- 列出本设备所有 Slot 字段、类型及含义 |
| 201 | + |
| 202 | +## 渐进加载策略 |
| 203 | +## 完整工作流 Checklist |
| 204 | +``` |
| 205 | + |
| 206 | +### Step 5 — 验证 |
| 207 | + |
| 208 | +检查文件完整性: |
| 209 | +- [ ] `SKILL.md` 包含 10 个 API endpoint |
| 210 | +- [ ] `SKILL.md` 包含 Placeholder Slot 填写规则(ResourceSlot / DeviceSlot / NodeSlot / ClassSlot + create_resource 特例)和本设备的 Slot 字段表 |
| 211 | +- [ ] `action-index.md` 列出所有 action 并有描述 |
| 212 | +- [ ] `actions/` 目录中每个 action 有对应 JSON 文件 |
| 213 | +- [ ] JSON 文件包含 `type`, `schema`(已提升为 goal 内容), `goal`, `goal_default`, `placeholder_keys` 字段 |
| 214 | +- [ ] 描述能让 agent 判断该用哪个 action |
| 215 | + |
| 216 | +## Action JSON 文件结构 |
| 217 | + |
| 218 | +```json |
| 219 | +{ |
| 220 | + "type": "LiquidHandlerTransfer", // → API 的 action_type |
| 221 | + "goal": { // goal 字段映射 |
| 222 | + "sources": "sources", |
| 223 | + "targets": "targets", |
| 224 | + "tip_racks": "tip_racks", |
| 225 | + "asp_vols": "asp_vols" |
| 226 | + }, |
| 227 | + "schema": { // ← 直接是 goal 的 schema(已提升) |
| 228 | + "type": "object", |
| 229 | + "properties": { // 参数定义(即请求中 goal 的字段) |
| 230 | + "sources": { "type": "array", "items": { "type": "object" } }, |
| 231 | + "targets": { "type": "array", "items": { "type": "object" } }, |
| 232 | + "asp_vols": { "type": "array", "items": { "type": "number" } } |
| 233 | + }, |
| 234 | + "required": [...], |
| 235 | + "_unilabos_placeholder_info": { // ← Slot 类型标记 |
| 236 | + "sources": "unilabos_resources", |
| 237 | + "targets": "unilabos_resources", |
| 238 | + "tip_racks": "unilabos_resources" |
| 239 | + } |
| 240 | + }, |
| 241 | + "goal_default": { ... }, // 默认值 |
| 242 | + "placeholder_keys": { // ← 汇总所有 Slot 字段 |
| 243 | + "sources": "unilabos_resources", // ResourceSlot |
| 244 | + "targets": "unilabos_resources", |
| 245 | + "tip_racks": "unilabos_resources", |
| 246 | + "target_device_id": "unilabos_devices" // DeviceSlot |
| 247 | + } |
| 248 | +} |
| 249 | +``` |
| 250 | + |
| 251 | +> **注意**:`schema` 已由脚本从原始 `schema.properties.goal` 提升为顶层,直接包含参数定义。 |
| 252 | +> `schema.properties` 中的字段即为 API 请求 `param.goal` 中的字段。 |
| 253 | +
|
| 254 | +## Placeholder Slot 类型体系 |
| 255 | + |
| 256 | +`placeholder_keys` / `_unilabos_placeholder_info` 中有 4 种值,对应不同的填写方式: |
| 257 | + |
| 258 | +| placeholder 值 | Slot 类型 | 填写格式 | 选取范围 | |
| 259 | +|---------------|-----------|---------|---------| |
| 260 | +| `unilabos_resources` | ResourceSlot | `{"id": "/path/name", "name": "name", "uuid": "xxx"}` | 仅**物料**节点(不含设备) | |
| 261 | +| `unilabos_devices` | DeviceSlot | `"/parent/device_name"` | 仅**设备**节点(type=device),路径字符串 | |
| 262 | +| `unilabos_nodes` | NodeSlot | `"/parent/node_name"` | **设备 + 物料**,即所有节点,路径字符串 | |
| 263 | +| `unilabos_class` | ClassSlot | `"class_name"` | 注册表中已上报的资源类 name | |
| 264 | + |
| 265 | +### ResourceSlot(`unilabos_resources`) |
| 266 | + |
| 267 | +最常见的类型。从资源树中选取**物料**节点(孔板、枪头盒、试剂槽等): |
| 268 | + |
| 269 | +```json |
| 270 | +{"id": "/workstation/container1", "name": "container1", "uuid": "ff149a9a-2cb8-419d-8db5-d3ba056fb3c2"} |
| 271 | +``` |
| 272 | + |
| 273 | +- 单个(schema type=object):`{"id": "/path/name", "name": "name", "uuid": "xxx"}` |
| 274 | +- 数组(schema type=array):`[{"id": "/path/a", "name": "a", "uuid": "xxx"}, ...]` |
| 275 | +- `id` 本身是从 parent 计算的路径格式 |
| 276 | +- 根据 action 语义选择正确的物料(如 `sources` = 液体来源,`targets` = 目标位置) |
| 277 | + |
| 278 | +> **特例**:`create_resource` 的 `res_id` 字段,目标物料可能**尚不存在**,此时直接填写期望的路径(如 `"/workstation/container1"`),不需要 uuid。 |
| 279 | +
|
| 280 | +### DeviceSlot(`unilabos_devices`) |
| 281 | + |
| 282 | +填写**设备路径字符串**。从资源树中筛选 type=device 的节点,从 parent 计算路径: |
| 283 | + |
| 284 | +``` |
| 285 | +"/host_node" |
| 286 | +"/bioyond_cell/reaction_station" |
| 287 | +``` |
| 288 | + |
| 289 | +- 只填路径字符串,不需要 `{id, uuid}` 对象 |
| 290 | +- 根据 action 语义选择正确的设备(如 `target_device_id` = 目标设备) |
| 291 | + |
| 292 | +### NodeSlot(`unilabos_nodes`) |
| 293 | + |
| 294 | +范围 = 设备 + 物料。即资源树中**所有节点**都可以选,填写**路径字符串**: |
| 295 | + |
| 296 | +``` |
| 297 | +"/PRCXI/PRCXI_Deck" |
| 298 | +``` |
| 299 | + |
| 300 | +- 使用场景:当参数既可能指向物料也可能指向设备时(如 `PumpTransferProtocol` 的 `from_vessel`/`to_vessel`,`create_resource` 的 `parent`) |
| 301 | + |
| 302 | +### ClassSlot(`unilabos_class`) |
| 303 | + |
| 304 | +填写注册表中已上报的**资源类 name**。从本地 `req_resource_registry_upload.json` 中查找: |
| 305 | + |
| 306 | +``` |
| 307 | +"container" |
| 308 | +``` |
| 309 | + |
| 310 | +### 通过 API #10 获取资源树 |
| 311 | + |
| 312 | +```bash |
| 313 | +curl -s -X GET "$BASE/api/v1/lab/material/download/$lab_uuid" -H "$AUTH" |
| 314 | +``` |
| 315 | + |
| 316 | +注意 `lab_uuid` 在路径中(不是查询参数)。资源树返回所有节点,每个节点包含 `id`(路径格式)、`name`、`uuid`、`type`、`parent` 等字段。填写 Slot 时需根据 placeholder 类型筛选正确的节点。 |
| 317 | + |
| 318 | +## 最终目录结构 |
| 319 | + |
| 320 | +``` |
| 321 | +./<skill-name>/ |
| 322 | +├── SKILL.md # API 端点 + 渐进加载指引 |
| 323 | +├── action-index.md # 动作索引:描述/用途/核心参数 |
| 324 | +└── actions/ # 每个 action 的完整 JSON Schema |
| 325 | + ├── action1.json |
| 326 | + ├── action2.json |
| 327 | + └── ... |
| 328 | +``` |
0 commit comments