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
22 KiB
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_id(如 liquid_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_typeschema— 完整 JSON Schema(含properties.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_resources→ ResourceSlot,填入{id, name, uuid}(id 是路径格式,从资源树取物料节点)unilabos_devices→ DeviceSlot,填入路径字符串如"/host_node"(从资源树筛选 type=device)unilabos_nodes→ NodeSlot,填入路径字符串如"/PRCXI/PRCXI_Deck"(资源树中任意节点)unilabos_class→ ClassSlot,填入类名字符串如"container"(从注册表查找)unilabos_formulation→ FormulationSlot,填入配方数组[{well_name, liquids: [{name, volume}]}](well_name 为目标物料的 name)
- array 类型字段 →
[{id, name, uuid}, ...] - 特殊:
create_resource的res_id(ResourceSlot)可填不存在的路径
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值(从 JSONtype字段提取),方便 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,配合液体配方 |
ResourceSlot(unilabos_resources)
最常见的类型。从资源树中选取物料节点(孔板、枪头盒、试剂槽等):
- 单个:
{"id": "/workstation/container1", "name": "container1", "uuid": "ff149a9a-..."} - 数组:
[{"id": "/path/a", "name": "a", "uuid": "xxx"}, ...] id从 parent 计算的路径格式,根据 action 语义选择正确的物料
特例:
create_resource的res_id,目标物料可能尚不存在,直接填期望路径,不需要 uuid。
DeviceSlot / NodeSlot / ClassSlot
- DeviceSlot(
unilabos_devices):路径字符串如"/host_node",仅 type=device 的节点 - NodeSlot(
unilabos_nodes):路径字符串如"/PRCXI/PRCXI_Deck",设备 + 物料均可选 - ClassSlot(
unilabos_class):类名字符串如"container",从req_resource_registry_upload.json查找
FormulationSlot(unilabos_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[]— 所有节点(设备 + 物料),每个节点含name、uuid、type、parenttype区分设备(device)和物料(deck、container、resource等)parent为父节点名称(空字符串表示顶级)- 填写 Slot 时根据 placeholder 类型筛选:ResourceSlot 取非 device 节点,DeviceSlot 取 device 节点
- 创建/更新物料时:
parent_uuid取父节点的uuid,更新目标的uuid取节点自身的uuid
物料管理 API
设备 Skill 除了设备动作外,还需支持物料节点的创建和参数设定,用于在资源树中动态管理物料。
典型流程:先通过 #14 按名称查询模板 获取 res_template_uuid → 再通过 #15 创建物料 → 之后可通过 #16 更新物料 修改属性。更新时需要的 uuid 和 parent_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 创建物料。返回还包含 name、resource_type、handles、config_infos 等模板元信息。
模板不存在时返回 code: 10002,data 为空对象。模板名称来自资源注册表中已注册的资源类型。
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_volume、liquids中的 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
└── ...