mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-27 20:46:44 +00:00
add:skill&agent
This commit is contained in:
323
.cursor/skills/add-protocol/SKILL.md
Normal file
323
.cursor/skills/add-protocol/SKILL.md
Normal file
@@ -0,0 +1,323 @@
|
||||
---
|
||||
name: add-protocol
|
||||
description: Guide for adding new experiment protocols to Uni-Lab-OS (添加新实验操作协议). Walks through ROS Action definition, Pydantic model creation, protocol generator implementation, and registration. Use when the user wants to add a new protocol, create a compile function, implement an experiment operation, or mentions 协议/protocol/编译/compile/实验操作.
|
||||
---
|
||||
|
||||
# 添加新实验操作协议(Protocol)
|
||||
|
||||
Protocol 是对实验有意义的完整动作(如泵转移、过滤、溶解),需要多设备协同。`compile/` 中的生成函数根据设备连接图将抽象操作"编译"为设备指令序列。
|
||||
|
||||
添加一个 Protocol 需修改 **6 个文件**,按以下流程执行。
|
||||
|
||||
---
|
||||
|
||||
## 第一步:确认协议信息
|
||||
|
||||
向用户确认:
|
||||
|
||||
| 信息 | 示例 |
|
||||
|------|------|
|
||||
| 协议英文名 | `MyNewProtocol` |
|
||||
| 操作描述 | 将固体样品研磨至目标粒径 |
|
||||
| Goal 参数(必需 + 可选) | `vessel: dict`, `time: float = 300.0` |
|
||||
| Result 字段 | `success: bool`, `message: str` |
|
||||
| 需要哪些设备协同 | 研磨器、搅拌器 |
|
||||
|
||||
---
|
||||
|
||||
## 第二步:创建 ROS Action 定义
|
||||
|
||||
路径:`unilabos_msgs/action/<ActionName>.action`
|
||||
|
||||
三段式结构(Goal / Result / Feedback),用 `---` 分隔:
|
||||
|
||||
```
|
||||
# Goal
|
||||
Resource vessel
|
||||
float64 time
|
||||
string mode
|
||||
---
|
||||
# Result
|
||||
bool success
|
||||
string return_info
|
||||
---
|
||||
# Feedback
|
||||
string status
|
||||
string current_device
|
||||
builtin_interfaces/Duration time_spent
|
||||
builtin_interfaces/Duration time_remaining
|
||||
```
|
||||
|
||||
**类型映射:**
|
||||
|
||||
| Python 类型 | ROS 类型 | 说明 |
|
||||
|------------|----------|------|
|
||||
| `dict` | `Resource` | 容器/设备引用,自定义消息类型 |
|
||||
| `float` | `float64` | |
|
||||
| `int` | `int32` | |
|
||||
| `str` | `string` | |
|
||||
| `bool` | `bool` | |
|
||||
|
||||
> `Resource` 是 `unilabos_msgs/msg/Resource.msg` 中定义的自定义消息类型。
|
||||
|
||||
---
|
||||
|
||||
## 第三步:注册 Action 到 CMakeLists
|
||||
|
||||
在 `unilabos_msgs/CMakeLists.txt` 的 `set(action_files ...)` 块中添加:
|
||||
|
||||
```cmake
|
||||
"action/MyNewAction.action"
|
||||
```
|
||||
|
||||
> 调试时需编译:`cd unilabos_msgs && colcon build && source ./install/local_setup.sh && cd ..`
|
||||
> PR 合并后 CI/CD 自动发布,`mamba update ros-humble-unilabos-msgs` 即可。
|
||||
|
||||
---
|
||||
|
||||
## 第四步:创建 Pydantic 模型
|
||||
|
||||
在 `unilabos/messages/__init__.py` 中添加(位于 `# Start Protocols` 和 `# End Protocols` 之间):
|
||||
|
||||
```python
|
||||
class MyNewProtocol(BaseModel):
|
||||
# === 必需参数 ===
|
||||
vessel: dict = Field(..., description="目标容器")
|
||||
|
||||
# === 可选参数 ===
|
||||
time: float = Field(300.0, description="操作时间 (秒)")
|
||||
mode: str = Field("default", description="操作模式")
|
||||
|
||||
def model_post_init(self, __context):
|
||||
"""参数验证和修正"""
|
||||
if self.time <= 0:
|
||||
self.time = 300.0
|
||||
```
|
||||
|
||||
**规则:**
|
||||
- 参数名必须与 `.action` 文件中 Goal 字段完全一致
|
||||
- `dict` 类型对应 `.action` 中的 `Resource`
|
||||
- 将类名加入文件末尾的 `__all__` 列表
|
||||
|
||||
---
|
||||
|
||||
## 第五步:实现协议生成函数
|
||||
|
||||
路径:`unilabos/compile/<protocol_name>_protocol.py`
|
||||
|
||||
```python
|
||||
import networkx as nx
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
def generate_my_new_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: dict,
|
||||
time: float = 300.0,
|
||||
mode: str = "default",
|
||||
**kwargs,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""将 MyNewProtocol 编译为设备动作序列。
|
||||
|
||||
Args:
|
||||
G: 设备连接图(NetworkX),节点为设备/容器,边为物理连接
|
||||
vessel: 目标容器 {"id": "reactor_1"}
|
||||
time: 操作时间(秒)
|
||||
mode: 操作模式
|
||||
|
||||
Returns:
|
||||
动作列表,每个元素为:
|
||||
- dict: 单步动作
|
||||
- list[dict]: 并行动作
|
||||
"""
|
||||
from unilabos.compile.utils.vessel_parser import get_vessel
|
||||
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
actions = []
|
||||
|
||||
# 查找相关设备(通过图的连接关系)
|
||||
# 生成动作序列
|
||||
actions.append({
|
||||
"device_id": "target_device_id",
|
||||
"action_name": "some_action",
|
||||
"action_kwargs": {"param": "value"}
|
||||
})
|
||||
|
||||
# 等待
|
||||
actions.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": time}
|
||||
})
|
||||
|
||||
return actions
|
||||
```
|
||||
|
||||
### 动作字典格式
|
||||
|
||||
```python
|
||||
# 单步动作(发给子设备)
|
||||
{"device_id": "pump_1", "action_name": "set_position", "action_kwargs": {"position": 10.0}}
|
||||
|
||||
# 发给工作站自身
|
||||
{"device_id": "self", "action_name": "my_action", "action_kwargs": {...}}
|
||||
|
||||
# 等待
|
||||
{"action_name": "wait", "action_kwargs": {"time": 5.0}}
|
||||
|
||||
# 并行动作(列表嵌套)
|
||||
[
|
||||
{"device_id": "pump_1", "action_name": "set_position", "action_kwargs": {"position": 10.0}},
|
||||
{"device_id": "stirrer_1", "action_name": "start_stir", "action_kwargs": {"stir_speed": 300}}
|
||||
]
|
||||
```
|
||||
|
||||
### 关于 `vessel` 参数类型
|
||||
|
||||
现有协议的 `vessel` 参数类型不统一:
|
||||
- 新协议趋势:使用 `dict`(如 `{"id": "reactor_1"}`)
|
||||
- 旧协议:使用 `str`(如 `"reactor_1"`)
|
||||
- 兼容写法:`Union[str, dict]`
|
||||
|
||||
**建议新协议统一使用 `dict` 类型**,通过 `get_vessel()` 兼容两种输入。
|
||||
|
||||
### 公共工具函数(`unilabos/compile/utils/`)
|
||||
|
||||
| 函数 | 用途 |
|
||||
|------|------|
|
||||
| `get_vessel(vessel)` | 解析容器参数为 `(vessel_id, vessel_data)`,兼容 dict 和 str |
|
||||
| `find_solvent_vessel(G, solvent)` | 根据溶剂名查找容器(精确→命名规则→模糊→液体类型) |
|
||||
| `find_reagent_vessel(G, reagent)` | 根据试剂名查找容器(支持固体和液体) |
|
||||
| `find_connected_stirrer(G, vessel)` | 查找与容器相连的搅拌器 |
|
||||
| `find_solid_dispenser(G)` | 查找固体加样器 |
|
||||
|
||||
### 协议内专属查找函数
|
||||
|
||||
许多协议在自己的文件内定义了专属的 `find_*` 函数(不在 `utils/` 中)。编写新协议时,优先复用 `utils/` 中的公共函数;如需特殊查找逻辑,在协议文件内部定义即可:
|
||||
|
||||
```python
|
||||
def find_my_special_device(G: nx.DiGraph, vessel: str) -> str:
|
||||
"""查找与容器相关的特殊设备"""
|
||||
for node in G.nodes():
|
||||
if 'my_device_type' in G.nodes[node].get('class', '').lower():
|
||||
return node
|
||||
raise ValueError("未找到特殊设备")
|
||||
```
|
||||
|
||||
### 复用已有协议
|
||||
|
||||
复杂协议通常组合已有协议:
|
||||
|
||||
```python
|
||||
from unilabos.compile.pump_protocol import generate_pump_protocol_with_rinsing
|
||||
|
||||
actions.extend(generate_pump_protocol_with_rinsing(
|
||||
G, from_vessel=solvent_vessel, to_vessel=vessel, volume=volume
|
||||
))
|
||||
```
|
||||
|
||||
### 图查询模式
|
||||
|
||||
```python
|
||||
# 查找与容器相连的特定类型设备
|
||||
for neighbor in G.neighbors(vessel_id):
|
||||
node_data = G.nodes[neighbor]
|
||||
if "heater" in node_data.get("class", ""):
|
||||
heater_id = neighbor
|
||||
break
|
||||
|
||||
# 查找最短路径(泵转移)
|
||||
path = nx.shortest_path(G, source=from_vessel_id, target=to_vessel_id)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第六步:注册协议生成函数
|
||||
|
||||
在 `unilabos/compile/__init__.py` 中:
|
||||
|
||||
1. 顶部添加导入:
|
||||
|
||||
```python
|
||||
from .my_new_protocol import generate_my_new_protocol
|
||||
```
|
||||
|
||||
2. 在 `action_protocol_generators` 字典中添加映射:
|
||||
|
||||
```python
|
||||
action_protocol_generators = {
|
||||
# ... 已有协议
|
||||
MyNewProtocol: generate_my_new_protocol,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第七步:配置图文件
|
||||
|
||||
在工作站的图文件中,将协议名加入 `protocol_type`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "my_station",
|
||||
"class": "workstation",
|
||||
"config": {
|
||||
"protocol_type": ["PumpTransferProtocol", "MyNewProtocol"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第八步:验证
|
||||
|
||||
```bash
|
||||
# 1. 模块可导入
|
||||
python -c "from unilabos.messages import MyNewProtocol; print(MyNewProtocol.model_fields)"
|
||||
|
||||
# 2. 生成函数可导入
|
||||
python -c "from unilabos.compile import action_protocol_generators; print(list(action_protocol_generators.keys()))"
|
||||
|
||||
# 3. 启动测试(可选)
|
||||
unilab -g <graph>.json --complete_registry
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工作流清单
|
||||
|
||||
```
|
||||
协议接入进度:
|
||||
- [ ] 1. 确认协议名、参数、涉及设备
|
||||
- [ ] 2. 创建 .action 文件 (unilabos_msgs/action/<Name>.action)
|
||||
- [ ] 3. 注册到 CMakeLists.txt
|
||||
- [ ] 4. 创建 Pydantic 模型 (unilabos/messages/__init__.py) + 更新 __all__
|
||||
- [ ] 5. 实现生成函数 (unilabos/compile/<name>_protocol.py)
|
||||
- [ ] 6. 注册到 compile/__init__.py
|
||||
- [ ] 7. 配置图文件 protocol_type
|
||||
- [ ] 8. 验证
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 高级模式
|
||||
|
||||
实现复杂协议时,详见 [reference.md](reference.md):协议运行时数据流、mock graph 测试模式、单位解析工具(`unit_parser.py`)、复杂协议组合模式(以 dissolve 为例)。
|
||||
|
||||
---
|
||||
|
||||
## 现有协议速查
|
||||
|
||||
| 协议 | Pydantic 类 | 生成函数 | 核心参数 |
|
||||
|------|-------------|---------|---------|
|
||||
| 泵转移 | `PumpTransferProtocol` | `generate_pump_protocol_with_rinsing` | `from_vessel, to_vessel, volume` |
|
||||
| 简单转移 | `TransferProtocol` | `generate_pump_protocol` | `from_vessel, to_vessel, volume` |
|
||||
| 加样 | `AddProtocol` | `generate_add_protocol` | `vessel, reagent, volume` |
|
||||
| 过滤 | `FilterProtocol` | `generate_filter_protocol` | `vessel, filtrate_vessel` |
|
||||
| 溶解 | `DissolveProtocol` | `generate_dissolve_protocol` | `vessel, solvent, volume` |
|
||||
| 加热/冷却 | `HeatChillProtocol` | `generate_heat_chill_protocol` | `vessel, temp, time` |
|
||||
| 搅拌 | `StirProtocol` | `generate_stir_protocol` | `vessel, time` |
|
||||
| 分离 | `SeparateProtocol` | `generate_separate_protocol` | `from_vessel, separation_vessel, solvent` |
|
||||
| 蒸发 | `EvaporateProtocol` | `generate_evaporate_protocol` | `vessel, pressure, temp, time` |
|
||||
| 清洗 | `CleanProtocol` | `generate_clean_protocol` | `vessel, solvent, volume` |
|
||||
| 离心 | `CentrifugeProtocol` | `generate_centrifuge_protocol` | `vessel, speed, time` |
|
||||
| 抽气充气 | `EvacuateAndRefillProtocol` | `generate_evacuateandrefill_protocol` | `vessel, gas` |
|
||||
207
.cursor/skills/add-protocol/reference.md
Normal file
207
.cursor/skills/add-protocol/reference.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# 协议高级参考
|
||||
|
||||
本文件是 SKILL.md 的补充,包含协议运行时数据流、测试模式、单位解析工具和复杂协议组合模式。Agent 在需要实现这些功能时按需阅读。
|
||||
|
||||
---
|
||||
|
||||
## 1. 协议运行时数据流
|
||||
|
||||
从图文件到协议执行的完整链路:
|
||||
|
||||
```
|
||||
实验图 JSON
|
||||
↓ graphio.read_node_link_json()
|
||||
physical_setup_graph (NetworkX DiGraph)
|
||||
↓ ROS2WorkstationNode._setup_protocol_names(protocol_type)
|
||||
为每个 protocol_name 创建 ActionServer
|
||||
↓ 收到 Action Goal
|
||||
_create_protocol_execute_callback()
|
||||
↓ convert_from_ros_msg_with_mapping(goal, mapping)
|
||||
protocol_kwargs (Python dict)
|
||||
↓ 向 Host 查询 Resource 类型参数的当前状态
|
||||
protocol_kwargs 更新(vessel 带上 children、data 等)
|
||||
↓ protocol_steps_generator(G=physical_setup_graph, **protocol_kwargs)
|
||||
List[Dict] 动作序列
|
||||
↓ 逐步 execute_single_action / 并行 create_task
|
||||
子设备 ActionClient 执行
|
||||
```
|
||||
|
||||
### `_setup_protocol_names` 核心逻辑
|
||||
|
||||
```python
|
||||
def _setup_protocol_names(self, protocol_type):
|
||||
if isinstance(protocol_type, str):
|
||||
self.protocol_names = [p.strip() for p in protocol_type.split(",")]
|
||||
else:
|
||||
self.protocol_names = protocol_type
|
||||
self.protocol_action_mappings = {}
|
||||
for protocol_name in self.protocol_names:
|
||||
protocol_type = globals()[protocol_name] # 从 messages 模块取 Pydantic 类
|
||||
self.protocol_action_mappings[protocol_name] = get_action_type(protocol_type)
|
||||
```
|
||||
|
||||
### `_create_protocol_execute_callback` 关键步骤
|
||||
|
||||
1. `convert_from_ros_msg_with_mapping(goal, action_value_mapping["goal"])` — ROS Goal → Python dict
|
||||
2. 对 `Resource` 类型字段,通过 `resource_get` Service 查询 Host 的最新资源状态
|
||||
3. `protocol_steps_generator(G=physical_setup_graph, **protocol_kwargs)` — 调用编译函数
|
||||
4. 遍历 steps:`dict` 串行执行,`list` 并行执行
|
||||
5. `execute_single_action` 通过 `_action_clients[device_id]` 向子设备发送 Action Goal
|
||||
6. 执行完毕后通过 `resource_update` Service 更新资源状态
|
||||
|
||||
---
|
||||
|
||||
## 2. 测试模式
|
||||
|
||||
### 2.1 协议文件内测试函数
|
||||
|
||||
许多协议文件末尾有 `test_*` 函数,主要测试参数解析工具:
|
||||
|
||||
```python
|
||||
def test_dissolve_protocol():
|
||||
"""测试溶解协议的各种参数解析"""
|
||||
volumes = ["10 mL", "?", 10.0, "1 L", "500 μL"]
|
||||
for vol in volumes:
|
||||
result = parse_volume_input(vol)
|
||||
print(f"体积解析: {vol} → {result}mL")
|
||||
|
||||
masses = ["2.9 g", "?", 2.5, "500 mg"]
|
||||
for mass in masses:
|
||||
result = parse_mass_input(mass)
|
||||
print(f"质量解析: {mass} → {result}g")
|
||||
```
|
||||
|
||||
### 2.2 使用 mock graph 测试协议生成器
|
||||
|
||||
推荐的端到端测试模式:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
import networkx as nx
|
||||
from unilabos.compile.stir_protocol import generate_stir_protocol
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def topology_graph():
|
||||
"""创建测试拓扑图"""
|
||||
G = nx.DiGraph()
|
||||
G.add_node("flask_1", **{"class": "flask", "type": "container"})
|
||||
G.add_node("stirrer_1", **{"class": "virtual_stirrer", "type": "device"})
|
||||
G.add_edge("stirrer_1", "flask_1")
|
||||
return G
|
||||
|
||||
|
||||
def test_generate_stir_protocol(topology_graph):
|
||||
"""测试搅拌协议生成"""
|
||||
actions = generate_stir_protocol(
|
||||
G=topology_graph,
|
||||
vessel="flask_1",
|
||||
time="5 min",
|
||||
stir_speed=300.0
|
||||
)
|
||||
assert len(actions) >= 1
|
||||
assert actions[0]["device_id"] == "stirrer_1"
|
||||
```
|
||||
|
||||
**要点:**
|
||||
- 用 `nx.DiGraph()` 构建最小拓扑
|
||||
- `add_node(id, **attrs)` 设置 `class`、`type`、`data` 等
|
||||
- `add_edge(src, dst)` 建立物理连接
|
||||
- 协议内的 `find_*` 函数依赖这些节点和边
|
||||
|
||||
---
|
||||
|
||||
## 3. 单位解析工具
|
||||
|
||||
路径:`unilabos/compile/utils/unit_parser.py`
|
||||
|
||||
| 函数 | 输入 | 返回 | 默认值 |
|
||||
|------|------|------|--------|
|
||||
| `parse_volume_input(input, default_unit)` | `"100 mL"`, `"2.5 L"`, `"500 μL"`, `10.0`, `"?"` | mL (float) | 50.0 |
|
||||
| `parse_mass_input(input)` | `"19.3 g"`, `"500 mg"`, `2.5`, `"?"` | g (float) | 1.0 |
|
||||
| `parse_time_input(input)` | `"30 min"`, `"1 h"`, `"300"`, `60.0`, `"?"` | 秒 (float) | 60.0 |
|
||||
|
||||
支持的单位:
|
||||
|
||||
- **体积**: mL, L, μL/uL, milliliter, liter, microliter
|
||||
- **质量**: g, mg, kg, gram, milligram, kilogram
|
||||
- **时间**: s/sec/second, min/minute, h/hr/hour, d/day
|
||||
|
||||
特殊值 `"?"`、`"unknown"`、`"tbd"` 返回默认值。
|
||||
|
||||
---
|
||||
|
||||
## 4. 复杂协议组合模式
|
||||
|
||||
以 `dissolve_protocol` 为例,展示如何组合多个子操作:
|
||||
|
||||
### 整体流程
|
||||
|
||||
```
|
||||
1. 解析参数 (parse_volume_input, parse_mass_input, parse_time_input)
|
||||
2. 设备发现 (find_connected_heatchill, find_connected_stirrer, find_solid_dispenser)
|
||||
3. 判断溶解类型 (液体 vs 固体)
|
||||
4. 组合动作序列:
|
||||
a. heat_chill_start / start_stir (启动加热/搅拌)
|
||||
b. wait (等待温度稳定)
|
||||
c. pump_protocol_with_rinsing (液体转移, 通过 extend 拼接)
|
||||
或 add_solid (固体加样)
|
||||
d. heat_chill / stir / wait (溶解等待)
|
||||
e. heat_chill_stop (停止加热)
|
||||
```
|
||||
|
||||
### 关键代码模式
|
||||
|
||||
**设备发现 → 条件组合:**
|
||||
|
||||
```python
|
||||
heatchill_id = find_connected_heatchill(G, vessel_id)
|
||||
stirrer_id = find_connected_stirrer(G, vessel_id)
|
||||
solid_dispenser_id = find_solid_dispenser(G)
|
||||
|
||||
actions = []
|
||||
|
||||
# 启动阶段
|
||||
if heatchill_id and temp > 25.0:
|
||||
actions.append({
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill_start",
|
||||
"action_kwargs": {"vessel": {"id": vessel_id}, "temp": temp}
|
||||
})
|
||||
actions.append({"action_name": "wait", "action_kwargs": {"time": 30}})
|
||||
elif stirrer_id:
|
||||
actions.append({
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "start_stir",
|
||||
"action_kwargs": {"vessel": {"id": vessel_id}, "stir_speed": stir_speed}
|
||||
})
|
||||
|
||||
# 转移阶段(复用已有协议)
|
||||
pump_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G, from_vessel=solvent_vessel, to_vessel=vessel_id, volume=volume
|
||||
)
|
||||
actions.extend(pump_actions)
|
||||
|
||||
# 等待阶段
|
||||
if heatchill_id:
|
||||
actions.append({
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill",
|
||||
"action_kwargs": {"vessel": {"id": vessel_id}, "temp": temp, "time": time}
|
||||
})
|
||||
else:
|
||||
actions.append({"action_name": "wait", "action_kwargs": {"time": time}})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键路径
|
||||
|
||||
| 内容 | 路径 |
|
||||
|------|------|
|
||||
| 协议执行回调 | `unilabos/ros/nodes/presets/workstation.py` |
|
||||
| ROS 消息映射 | `unilabos/ros/msgs/message_converter.py` |
|
||||
| 物理拓扑图 | `unilabos/resources/graphio.py` (`physical_setup_graph`) |
|
||||
| 单位解析 | `unilabos/compile/utils/unit_parser.py` |
|
||||
| 容器解析 | `unilabos/compile/utils/vessel_parser.py` |
|
||||
| 溶解协议(组合示例) | `unilabos/compile/dissolve_protocol.py` |
|
||||
Reference in New Issue
Block a user