Files
Uni-Lab-OS/.cursor/skills/create-device-skill/SKILL.md
Xuwznln ccbf5378dd update workbench example
update aksk desc

print res query logs

Fix skills exec error with action type

Update Skills

Update Skills addr

Change uni-lab. to leap-lab.
Support unit in pylabrobot

Support async func.

change to leap-lab backend. Support feedback interval. Reduce cocurrent lags.

fix create_resource_with_slot

update unilabos_formulation & batch-submit-exp

scale multi exec thread up to 48

update handle creation api

fit cocurrent gap

add running status debounce

allow non @topic_config support

update skill

add placeholder keys

always free

提交实验技能

disable samples

correct sample demo ret value

新增试剂reagent

update registry

新增manual_confirm

add workstation creation skill

add virtual_sample_demo 样品追踪测试设备

add external devices param
fix registry upload missing type

fast registry load

minor fix on skill & registry

stripe ros2 schema desc
add create-device-skill

new registry system backwards to yaml

remove not exist resource

new registry sys
exp. support with add device

correct raise create resource error

ret info fix revert

ret info fix

fix prcxi check

add create_resource schema

re signal host ready event

add websocket connection timeout and improve reconnection logic

add open_timeout parameter to websocket connection
add TimeoutError and InvalidStatus exception handling
implement exponential backoff for reconnection attempts
simplify reconnection logic flow
2026-04-16 01:33:36 +08:00

22 KiB
Raw Blame History

name, description
name description
create-device-skill 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.

创建设备 Skill 指南

本 meta-skill 教你如何为任意 Uni-Lab-OS 设备创建完整的 API 操作技能(参考 unilab-device-api 的成功案例)。

数据源

  • 设备注册表: unilabos_data/req_device_registry_upload.json
  • 结构: { "resources": [{ "id": "<device_id>", "class": { "module": "<python_module:ClassName>", "action_value_mappings": { ... } } }] }
  • 生成时机: unilab 启动并完成注册表上传后自动生成
  • module 字段: 格式 unilabos.devices.xxx.yyy:ClassName,可转为源码路径 unilabos/devices/xxx/yyy.py,阅读源码可了解参数含义和设备行为

创建流程

Step 0 — 收集必备信息(缺一不可,否则询问后终止)

开始前必须确认以下 4 项信息全部就绪。如果用户未提供任何一项,立即询问并终止当前流程,等用户补齐后再继续。

向用户提问:「请提供你的 unilab 启动参数,我需要以下信息:」

必备项 ①ak / sk认证凭据

来源:启动命令的 --ak --sk 参数,或 config.py 中的 ak = "..." sk = "..."

获取后立即生成 AUTH token

python ./scripts/gen_auth.py <ak> <sk>
# 或从 config.py 提取
python ./scripts/gen_auth.py --config <config.py>

认证算法:base64(ak:sk)Authorization: Lab <token>

必备项 ②:--addr目标环境

决定 API 请求发往哪个服务器。从启动命令的 --addr 参数获取:

--addr BASE URL
test https://leap-lab.test.bohrium.com
uat https://leap-lab.uat.bohrium.com
local http://127.0.0.1:48197
不传(默认) https://leap-lab.bohrium.com
其他自定义 URL 直接使用该 URL

必备项 ③req_device_registry_upload.json设备注册表

数据文件由 unilab 启动时自动生成,需要定位它:

推断 working_dir(即 unilabos_data 所在目录):

条件 working_dir 取值
传了 --working_dir <working_dir>/unilabos_data/(若子目录已存在则直接用)
仅传了 --config <config 文件所在目录>/unilabos_data/
都没传 <当前工作目录>/unilabos_data/

按优先级搜索文件

<推断的 working_dir>/unilabos_data/req_device_registry_upload.json
<推断的 working_dir>/req_device_registry_upload.json
<workspace 根目录>/unilabos_data/req_device_registry_upload.json

也可以直接 Glob 搜索:**/req_device_registry_upload.json

找到后必须检查文件修改时间并告知用户:「找到注册表文件 <路径>,生成于 <时间>。请确认这是最近一次启动生成的。」超过 1 天提醒用户是否需要重新启动 unilab

如果文件不存在 → 告知用户先运行 unilab 启动命令,等日志出现 注册表响应数据已保存 后再执行本流程。终止。

必备项 ④:目标设备

用户需要明确要为哪个设备创建 skill。可以是设备名称如「PRCXI 移液站」)或 device_idliquid_handler.prcxi)。

如果用户不确定,运行提取脚本列出所有设备供选择:

python ./scripts/extract_device_actions.py --registry <找到的文件路径>

四项全部就绪后才进入 Step 1。

Step 1 — 列出可用设备

运行提取脚本,列出所有设备及 action 数量和 Python 源码路径,让用户选择:

# 自动搜索(默认在 unilabos_data/ 和当前目录查找)
python ./scripts/extract_device_actions.py

# 指定注册表文件路径
python ./scripts/extract_device_actions.py --registry <path/to/req_device_registry_upload.json>

脚本输出包含每个设备的 Python 源码路径(从 class.module 转换),可用于后续阅读源码理解参数含义。

Step 2 — 提取 Action Schema

用户选择设备后,运行提取脚本:

python ./scripts/extract_device_actions.py [--registry <path>] <device_id> ./skills/<skill-name>/actions/

脚本会显示设备的 Python 源码路径和类名,方便阅读源码了解参数含义。

每个 action 生成一个 JSON 文件,包含:

  • type — 作为 API 调用的 action_type
  • schema — 完整 JSON Schemaproperties.goal.properties 参数定义)
  • goal — goal 字段映射(含占位符 $placeholder
  • goal_default — 默认值

Step 3 — 写 action-index.md

按模板为每个 action 写条目(必须包含 action_type

### `<action_name>`

<用途描述(一句话)>

- **action_type**: `<从 actions/<name>.json 的 type 字段获取>`
- **Schema**: [`actions/<filename>.json`](actions/<filename>.json)
- **核心参数**: `param1`, `param2`(从 schema.required 获取)
- **可选参数**: `param3`, `param4`
- **占位符字段**: `field`(需填入物料信息,值以 `$` 开头)

描述规则:

  • 每个 action 必须标注 action_type(从 JSON 的 type 字段读取),这是 API #9 调用时的必填参数,传错会导致任务永远卡住
  • schema.properties 读参数列表schema 已提升为 goal 内容)
  • schema.required 区分核心/可选参数
  • 按功能分类(移液、枪头、外设等)
  • 标注 placeholder_keys 中的字段类型:
    • unilabos_resourcesResourceSlot,填入 {id, name, uuid}id 是路径格式,从资源树取物料节点)
    • unilabos_devicesDeviceSlot,填入路径字符串如 "/host_node"(从资源树筛选 type=device
    • unilabos_nodesNodeSlot,填入路径字符串如 "/PRCXI/PRCXI_Deck"(资源树中任意节点)
    • unilabos_classClassSlot,填入类名字符串如 "container"(从注册表查找)
    • unilabos_formulationFormulationSlot,填入配方数组 [{well_name, liquids: [{name, volume}]}]well_name 为目标物料的 name
  • array 类型字段 → [{id, name, uuid}, ...]
  • 特殊:create_resourceres_idResourceSlot可填不存在的路径

Step 4 — 写 SKILL.md

直接复用 unilab-device-api 的 API 模板,修改:

  • 设备名称
  • Action 数量
  • 目录列表
  • Session state 中的 device_name
  • AUTH 头 — 使用 Step 0 中 gen_auth.py 生成的 Authorization: Lab <token>(不要硬编码 Api 类型的 key
  • Python 源码路径 — 在 SKILL.md 开头注明设备对应的源码文件,方便参考参数含义
  • Slot 字段表 — 列出本设备哪些 action 的哪些字段需要填入 Slot物料/设备/节点/类名)
  • action_type 速查表 — 在 API #9 说明后面紧跟一个表格,列出每个 action 对应的 action_type 值(从 JSON type 字段提取),方便 agent 快速查找而无需打开 JSON 文件

API 模板结构:

## 设备信息

- device_id, Python 源码路径, 设备类名

## 前置条件(缺一不可)

- ak/sk → AUTH, --addr → BASE URL

## 请求约定

- Windows 平台必须用 curl.exe非 PowerShell 的 curl 别名)

## Session State

- lab_uuid通过 GET /edge/lab/info 直接获取,不要问用户), device_name

## API Endpoints

# - #1 GET /edge/lab/info → 直接拿到 lab_uuid

# - #2 创建工作流 POST /lab/workflow/owner → 拼 URL 告知用户

# - #3 创建节点 POST /edge/workflow/node

# body: {workflow_uuid, resource_template_name: "<device_id>", node_template_name: "<action_name>"}

# - #4 删除节点 DELETE /lab/workflow/nodes

# - #5 更新节点参数 PATCH /lab/workflow/node

# - #6 查询节点 handles POST /lab/workflow/node-handles

# body: {node_uuids: ["uuid1","uuid2"]} → 返回各节点的 handle_uuid

# - #7 批量创建边 POST /lab/workflow/edges

# body: {edges: [{source_node_uuid, target_node_uuid, source_handle_uuid, target_handle_uuid}]}

# - #8 启动工作流 POST /lab/workflow/{uuid}/run

# - #9 运行设备单动作 POST /lab/mcp/run/action action_type 必须从 action-index.md 或 actions/<name>.json 的 type 字段获取,传错会导致任务永远卡住)

# - #10 查询任务状态 GET /lab/mcp/task/{task_uuid}

# - #11 运行工作流单节点 POST /lab/mcp/run/workflow/action

# - #12 获取资源树 GET /lab/material/download/{lab_uuid}

# - #13 获取工作流模板详情 GET /lab/workflow/template/detail/{workflow_uuid}

# 返回 workflow 完整结构data.nodes[] 含每个节点的 uuid、name、param、device_name、handles

# - #14 按名称查询物料模板 GET /lab/material/template/by-name?lab_uuid=&name=

# 返回 res_template_uuid用于 #15 创建物料时的必填字段

# - #15 创建物料节点 POST /edge/material/node

# body: {res_template_uuid(从#14获取), name(自定义), display_name, parent_uuid?(从#12获取), ...}

# - #16 更新物料节点 PUT /edge/material/node

# body: {uuid(从#12获取), display_name?, description?, init_param_data?, data?, ...}

## Placeholder Slot 填写规则

- unilabos_resources → ResourceSlot → {"id":"/path/name","name":"name","uuid":"xxx"}
- unilabos_devices → DeviceSlot → "/parent/device" 路径字符串
- unilabos_nodes → NodeSlot → "/parent/node" 路径字符串
- unilabos_class → ClassSlot → "class_name" 字符串
- unilabos_formulation → FormulationSlot → [{well_name, liquids: [{name, volume}]}] 配方数组
- 特例create_resource 的 res_id 允许填不存在的路径
- 列出本设备所有 Slot 字段、类型及含义

## 渐进加载策略

## 完整工作流 Checklist

Step 5 — 验证

检查文件完整性:

  • SKILL.md 包含 API endpoint#1 获取 lab_uuid、#2-#7 工作流/节点/边、#8-#11 运行/查询、#12 资源树、#13 工作流模板详情、#14-#16 物料管理)
  • SKILL.md 包含 Placeholder Slot 填写规则ResourceSlot / DeviceSlot / NodeSlot / ClassSlot / FormulationSlot + create_resource 特例)和本设备的 Slot 字段表
  • action-index.md 列出所有 action 并有描述
  • actions/ 目录中每个 action 有对应 JSON 文件
  • JSON 文件包含 type, schema(已提升为 goal 内容), goal, goal_default, placeholder_keys 字段
  • 描述能让 agent 判断该用哪个 action

Action JSON 文件结构

{
  "type": "LiquidHandlerTransfer",    // → API 的 action_type
  "goal": {                           // goal 字段映射
    "sources": "sources",
    "targets": "targets",
    "tip_racks": "tip_racks",
    "asp_vols": "asp_vols"
  },
  "schema": {                         // ← 直接是 goal 的 schema已提升
    "type": "object",
    "properties": {                   // 参数定义(即请求中 goal 的字段)
      "sources": { "type": "array", "items": { "type": "object" } },
      "targets": { "type": "array", "items": { "type": "object" } },
      "asp_vols": { "type": "array", "items": { "type": "number" } }
    },
    "required": [...],
    "_unilabos_placeholder_info": {   // ← Slot 类型标记
      "sources": "unilabos_resources",
      "targets": "unilabos_resources",
      "tip_racks": "unilabos_resources"
    }
  },
  "goal_default": { ... },            // 默认值
  "placeholder_keys": {               // ← 汇总所有 Slot 字段
    "sources": "unilabos_resources",  //    ResourceSlot
    "targets": "unilabos_resources",
    "tip_racks": "unilabos_resources",
    "target_device_id": "unilabos_devices"  // DeviceSlot
  }
}

注意schema 已由脚本从原始 schema.properties.goal 提升为顶层,直接包含参数定义。 schema.properties 中的字段即为 API 创建节点返回的 data.param 中的字段PATCH 更新时直接修改 param 即可。

Placeholder Slot 类型体系

placeholder_keys / _unilabos_placeholder_info 中有 5 种值,对应不同的填写方式:

placeholder 值 Slot 类型 填写格式 选取范围
unilabos_resources ResourceSlot {"id": "/path/name", "name": "name", "uuid": "xxx"} 物料节点(不含设备)
unilabos_devices DeviceSlot "/parent/device_name" 设备节点type=device路径字符串
unilabos_nodes NodeSlot "/parent/node_name" 设备 + 物料,即所有节点,路径字符串
unilabos_class ClassSlot "class_name" 注册表中已上报的资源类 name
unilabos_formulation FormulationSlot [{well_name, liquids: [{name, volume}]}] 资源树中物料节点的 name,配合液体配方

ResourceSlotunilabos_resources

最常见的类型。从资源树中选取物料节点(孔板、枪头盒、试剂槽等):

  • 单个:{"id": "/workstation/container1", "name": "container1", "uuid": "ff149a9a-..."}
  • 数组:[{"id": "/path/a", "name": "a", "uuid": "xxx"}, ...]
  • id 从 parent 计算的路径格式,根据 action 语义选择正确的物料

特例create_resourceres_id,目标物料可能尚不存在,直接填期望路径,不需要 uuid。

DeviceSlot / NodeSlot / ClassSlot

  • DeviceSlotunilabos_devices):路径字符串如 "/host_node",仅 type=device 的节点
  • NodeSlotunilabos_nodes):路径字符串如 "/PRCXI/PRCXI_Deck",设备 + 物料均可选
  • ClassSlotunilabos_class):类名字符串如 "container",从 req_resource_registry_upload.json 查找

FormulationSlotunilabos_formulation

描述液体配方:向哪些容器中加入哪些液体及体积。

[
  {
    "sample_uuid": "",
    "well_name": "bottle_A1",
    "liquids": [{ "name": "LiPF6", "volume": 0.6 }]
  }
]
  • well_name — 目标物料的 name(从资源树取,不是 id 路径)
  • liquids[] — 液体列表,每条含 name(试剂名)和 volume体积单位由上下文决定pylabrobot 内部统一 uL
  • sample_uuid — 样品 UUID无样品传 ""
  • 与 ResourceSlot 的区别ResourceSlot 指向物料本身FormulationSlot 引用物料名并附带配方信息

通过 API #12 获取资源树

curl -s -X GET "$BASE/api/v1/lab/material/download/$lab_uuid" -H "$AUTH"

注意 lab_uuid 在路径中(不是查询参数)。返回结构:

{
  "code": 0,
  "data": {
    "nodes": [
      {"name": "host_node", "uuid": "c3ec1e68-...", "type": "device", "parent": ""},
      {"name": "PRCXI", "uuid": "e249c9a6-...", "type": "device", "parent": ""},
      {"name": "PRCXI_Deck", "uuid": "fb6a8b71-...", "type": "deck", "parent": "PRCXI"}
    ],
    "edges": [...]
  }
}
  • data.nodes[] — 所有节点(设备 + 物料),每个节点含 nameuuidtypeparent
  • type 区分设备(device)和物料(deckcontainerresource 等)
  • parent 为父节点名称(空字符串表示顶级)
  • 填写 Slot 时根据 placeholder 类型筛选ResourceSlot 取非 device 节点DeviceSlot 取 device 节点
  • 创建/更新物料时:parent_uuid 取父节点的 uuid,更新目标的 uuid 取节点自身的 uuid

物料管理 API

设备 Skill 除了设备动作外,还需支持物料节点的创建和参数设定,用于在资源树中动态管理物料。

典型流程:先通过 #14 按名称查询模板 获取 res_template_uuid → 再通过 #15 创建物料 → 之后可通过 #16 更新物料 修改属性。更新时需要的 uuidparent_uuid 均从 #12 资源树下载 获取。

API #14 — 按名称查询物料模板

创建物料前,需要先获取物料模板的 UUID。通过模板名称查询

curl -s -X GET "$BASE/api/v1/lab/material/template/by-name?lab_uuid=$lab_uuid&name=<template_name>" -H "$AUTH"
参数 必填 说明
lab_uuid 实验室 UUID从 API #1 获取)
name 物料模板名称(如 "container"

返回 code: 0 时,data.uuid 即为 res_template_uuid,用于 API #15 创建物料。返回还包含 nameresource_typehandlesconfig_infos 等模板元信息。

模板不存在时返回 code: 10002data 为空对象。模板名称来自资源注册表中已注册的资源类型。

API #15 — 创建物料节点

curl -s -X POST "$BASE/api/v1/edge/material/node" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '<request_body>'

请求体:

{
  "res_template_uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "name": "my_custom_bottle",
  "display_name": "自定义瓶子",
  "parent_uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "type": "",
  "init_param_data": {},
  "schema": {},
  "data": {
    "liquids": [["water", 1000, "uL"]],
    "max_volume": 50000
  },
  "plate_well_datas": {},
  "plate_reagent_datas": {},
  "pose": {},
  "model": {}
}
字段 必填 类型 数据来源 说明
res_template_uuid string (UUID) API #14 按名称查询获取 物料模板 UUID
name string 用户自定义 节点名称(标识符),可自由命名
display_name string 用户自定义 显示名称UI 展示用)
parent_uuid string (UUID) API #12 资源树中父节点的 uuid 父节点,为空则创建顶级节点
type string 从模板继承 节点类型
init_param_data object 用户指定 初始化参数,覆盖模板默认值
data object 用户指定 节点数据container 见下方 data 格式
plate_well_datas object 用户指定 孔板子节点数据(创建带孔位的板时使用)
plate_reagent_datas object 用户指定 试剂关联数据
schema object 从模板继承 自定义 schema不传则从模板继承
pose object 用户指定 位姿信息
model object 用户指定 3D 模型信息

container 的 data 格式

体积单位统一为 uL微升。pylabrobot 体系中所有体积值(max_volumeliquids 中的 volume均为 uL。外部如果是 mL 需乘 1000 转换。

{
  "liquids": [["water", 1000, "uL"], ["ethanol", 500, "uL"]],
  "max_volume": 50000
}
  • liquids — 液体列表,每条为 [液体名称, 体积(uL), 单位字符串]
  • max_volume — 容器最大容量uL如 50 mL = 50000 uL

API #16 — 更新物料节点

curl -s -X PUT "$BASE/api/v1/edge/material/node" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '<request_body>'

请求体:

{
  "uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "parent_uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "display_name": "新显示名称",
  "description": "新描述",
  "init_param_data": {},
  "data": {},
  "pose": {},
  "schema": {},
  "extra": {}
}
字段 必填 类型 数据来源 说明
uuid string (UUID) API #12 资源树中目标节点的 uuid 要更新的物料节点
parent_uuid string (UUID) API #12 资源树 移动到新父节点
display_name string 用户指定 更新显示名称
description string 用户指定 更新描述
init_param_data object 用户指定 更新初始化参数
data object 用户指定 更新节点数据
pose object 用户指定 更新位姿
schema object 用户指定 更新 schema
extra object 用户指定 更新扩展数据

只传需要更新的字段,未传的字段保持不变。

最终目录结构

./<skill-name>/
├── SKILL.md              # API 端点 + 渐进加载指引
├── action-index.md       # 动作索引:描述/用途/核心参数
└── actions/              # 每个 action 的完整 JSON Schema
    ├── action1.json
    ├── action2.json
    └── ...