diff --git a/.cursor/skills/create-device-skill/SKILL.md b/.cursor/skills/create-device-skill/SKILL.md index 8f524141..c01a2e37 100644 --- a/.cursor/skills/create-device-skill/SKILL.md +++ b/.cursor/skills/create-device-skill/SKILL.md @@ -163,7 +163,7 @@ python ./scripts/extract_device_actions.py [--registry ] ./ski ### Step 4 — 写 SKILL.md -直接复用 `unilab-device-api` 的 API 模板(10 个 endpoint),修改: +直接复用 `unilab-device-api` 的 API 模板,修改: - 设备名称 - Action 数量 - 目录列表 @@ -181,15 +181,18 @@ API 模板结构: ## 前置条件(缺一不可) - ak/sk → AUTH, --addr → BASE URL -## Session State -- lab_uuid(通过 API #1 自动匹配,不要问用户), device_name +## 请求约定 +- Windows 平台必须用 curl.exe(非 PowerShell 的 curl 别名) -## API Endpoints (10 个) -# 注意: -# - #1 获取 lab 列表 + 自动匹配 lab_uuid(遍历 is_admin 的 lab, -# 调用 /lab/info/{uuid} 比对 access_key == ak) -# - #2 创建工作流用 POST /lab/workflow -# - #10 获取资源树路径含 lab_uuid: /lab/material/download/{lab_uuid} +## 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: "", node_template_name: ""} +# - #10 获取资源树 GET /lab/material/download/{lab_uuid} ## Placeholder Slot 填写规则 - unilabos_resources → ResourceSlot → {"id":"/path/name","name":"name","uuid":"xxx"} @@ -206,7 +209,7 @@ API 模板结构: ### Step 5 — 验证 检查文件完整性: -- [ ] `SKILL.md` 包含 10 个 API endpoint +- [ ] `SKILL.md` 包含 API endpoint(#1 获取 lab_uuid、#2-#9 工作流/动作、#10 资源树) - [ ] `SKILL.md` 包含 Placeholder Slot 填写规则(ResourceSlot / DeviceSlot / NodeSlot / ClassSlot + create_resource 特例)和本设备的 Slot 字段表 - [ ] `action-index.md` 列出所有 action 并有描述 - [ ] `actions/` 目录中每个 action 有对应 JSON 文件 @@ -249,7 +252,7 @@ API 模板结构: ``` > **注意**:`schema` 已由脚本从原始 `schema.properties.goal` 提升为顶层,直接包含参数定义。 -> `schema.properties` 中的字段即为 API 请求 `param.goal` 中的字段。 +> `schema.properties` 中的字段即为 API 创建节点返回的 `data.param` 中的字段,PATCH 更新时直接修改 `param` 即可。 ## Placeholder Slot 类型体系 diff --git a/.cursor/skills/submit-agent-result/SKILL.md b/.cursor/skills/submit-agent-result/SKILL.md new file mode 100644 index 00000000..18923711 --- /dev/null +++ b/.cursor/skills/submit-agent-result/SKILL.md @@ -0,0 +1,275 @@ +--- +name: submit-agent-result +description: Submit historical experiment results (agent_result) to Uni-Lab notebook — read data files, assemble JSON payload, PUT to cloud API. Use when the user wants to submit experiment results, upload agent results, report experiment data, or mentions agent_result/实验结果/历史记录/notebook结果. +--- + +# 提交历史实验记录指南 + +通过云端 API 向已创建的 notebook 提交实验结果数据(agent_result)。支持从 JSON / CSV 文件读取数据,整合后提交。 + +## 前置条件(缺一不可) + +使用本指南前,**必须**先确认以下信息。如果缺少任何一项,**立即向用户询问并终止**,等补齐后再继续。 + +### 1. ak / sk → AUTH + +询问用户的启动参数,从 `--ak` `--sk` 或 config.py 中获取。 + +生成 AUTH token: + +```bash +python -c "import base64,sys; print(base64.b64encode(f'{sys.argv[1]}:{sys.argv[2]}'.encode()).decode())" +``` + +输出即为 token 值,拼接为 `Authorization: Lab `。 + +### 2. --addr → BASE URL + +| `--addr` 值 | BASE | +|-------------|------| +| `test` | `https://uni-lab.test.bohrium.com` | +| `uat` | `https://uni-lab.uat.bohrium.com` | +| `local` | `http://127.0.0.1:48197` | +| 不传(默认) | `https://uni-lab.bohrium.com` | + +确认后设置: +```bash +BASE="<根据 addr 确定的 URL>" +AUTH="Authorization: Lab <上面命令输出的 token>" +``` + +### 3. notebook_uuid(**必须询问用户**) + +**必须主动询问用户**:「请提供要提交结果的 notebook UUID。」 + +notebook_uuid 来自之前通过「批量提交实验」创建的实验批次,即 `POST /api/v1/lab/notebook` 返回的 `data.uuid`。 + +如果用户不记得,可提示: +- 查看之前的对话记录中创建 notebook 时返回的 UUID +- 或通过平台页面查找对应的 notebook + +**绝不能跳过此步骤,没有 notebook_uuid 无法提交。** + +### 4. 实验结果数据 + +用户需要提供实验结果数据,支持以下方式: + +| 方式 | 说明 | +|------|------| +| JSON 文件 | 直接作为 `agent_result` 的内容合并 | +| CSV 文件 | 转为 `{"文件名": [行数据...]}` 格式 | +| 手动指定 | 用户直接告知 key-value 数据,由 agent 构建 JSON | + +**四项全部就绪后才可开始。** + +## Session State + +在整个对话过程中,agent 需要记住以下状态: + +- `lab_uuid` — 实验室 UUID(通过 API #1 自动获取,**不需要问用户**) +- `notebook_uuid` — 目标 notebook UUID(**必须询问用户**) + +## 请求约定 + +所有请求使用 `curl -s`,PUT 需加 `Content-Type: application/json`。 + +> **Windows 平台**必须使用 `curl.exe`(而非 PowerShell 的 `curl` 别名),示例中的 `curl` 均指 `curl.exe`。 +> +> **PowerShell JSON 传参**:PowerShell 中 `-d '{"key":"value"}'` 会因引号转义失败。请将 JSON 写入临时文件,用 `-d '@tmp_body.json'`(单引号包裹 `@`,否则 `@` 会被 PowerShell 解析为 splatting 运算符导致报错)。 + +--- + +## API Endpoints + +### 1. 获取实验室信息(自动获取 lab_uuid) + +```bash +curl -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH" +``` + +返回: + +```json +{"code": 0, "data": {"uuid": "xxx", "name": "实验室名称"}} +``` + +记住 `data.uuid` 为 `lab_uuid`。 + +### 2. 提交实验结果(agent_result) + +```bash +curl -s -X PUT "$BASE/api/v1/lab/notebook/agent-result" \ + -H "$AUTH" -H "Content-Type: application/json" \ + -d '' +``` + +请求体结构: + +```json +{ + "notebook_uuid": "", + "agent_result": { + "": "", + "": 123, + "": {"a": 1, "b": 2}, + "": [{"col1": "v1", "col2": "v2"}, ...] + } +} +``` + +> **注意**:HTTP 方法是 **PUT**(不是 POST)。 + +#### 必要字段 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `notebook_uuid` | string (UUID) | 目标 notebook 的 UUID,从批量提交实验时获取 | +| `agent_result` | object | 实验结果数据,任意 JSON 对象 | + +#### agent_result 内容格式 + +`agent_result` 接受**任意 JSON 对象**,常见格式: + +**简单键值对**: +```json +{ + "avg_rtt_ms": 12.5, + "status": "success", + "test_count": 5 +} +``` + +**包含嵌套结构**: +```json +{ + "summary": {"total": 100, "passed": 98, "failed": 2}, + "measurements": [ + {"sample_id": "S001", "value": 3.14, "unit": "mg/mL"}, + {"sample_id": "S002", "value": 2.71, "unit": "mg/mL"} + ] +} +``` + +**从 CSV 文件导入**(脚本自动转换): +```json +{ + "experiment_data": [ + {"温度": 25, "压力": 101.3, "产率": 0.85}, + {"温度": 30, "压力": 101.3, "产率": 0.91} + ] +} +``` + +--- + +## 整合脚本 + +本文档同级目录下的 `scripts/prepare_agent_result.py` 可自动读取文件并构建请求体。 + +### 用法 + +```bash +python scripts/prepare_agent_result.py \ + --notebook-uuid \ + --files data1.json data2.csv \ + [--auth ] \ + [--base ] \ + [--submit] \ + [--output ] +``` + +| 参数 | 必选 | 说明 | +|------|------|------| +| `--notebook-uuid` | 是 | 目标 notebook UUID | +| `--files` | 是 | 输入文件路径(支持多个,JSON / CSV) | +| `--auth` | 提交时必选 | Lab token(base64(ak:sk)) | +| `--base` | 提交时必选 | API base URL | +| `--submit` | 否 | 加上此标志则直接提交到云端 | +| `--output` | 否 | 输出 JSON 路径(默认 `agent_result_body.json`) | + +### 文件合并规则 + +| 文件类型 | 合并方式 | +|----------|----------| +| `.json`(dict) | 字段直接合并到 `agent_result` 顶层 | +| `.json`(list/other) | 以文件名为 key 放入 `agent_result` | +| `.csv` | 以文件名(不含扩展名)为 key,值为行对象数组 | + +多个文件的字段会合并。JSON dict 中的重复 key 后者覆盖前者。 + +### 示例 + +```bash +# 仅生成请求体文件(不提交) +python scripts/prepare_agent_result.py \ + --notebook-uuid 73c67dca-c8cc-4936-85a0-329106aa7cca \ + --files results.json measurements.csv + +# 生成并直接提交 +python scripts/prepare_agent_result.py \ + --notebook-uuid 73c67dca-c8cc-4936-85a0-329106aa7cca \ + --files results.json \ + --auth YTFmZDlkNGUt... \ + --base https://uni-lab.test.bohrium.com \ + --submit +``` + +--- + +## 手动构建方式 + +如果不使用脚本,也可手动构建请求体: + +1. 将实验结果数据组装为 JSON 对象 +2. 写入临时文件: + +```json +{ + "notebook_uuid": "", + "agent_result": { ... } +} +``` + +3. 用 curl 提交: + +```bash +curl -s -X PUT "$BASE/api/v1/lab/notebook/agent-result" \ + -H "$AUTH" -H "Content-Type: application/json" \ + -d '@tmp_body.json' +``` + +--- + +## 完整工作流 Checklist + +``` +Task Progress: +- [ ] Step 1: 确认 ak/sk → 生成 AUTH token +- [ ] Step 2: 确认 --addr → 设置 BASE URL +- [ ] Step 3: GET /edge/lab/info → 获取 lab_uuid +- [ ] Step 4: **询问用户** notebook_uuid(必须,不可跳过) +- [ ] Step 5: 确认实验结果数据来源(文件路径或手动数据) +- [ ] Step 6: 运行 prepare_agent_result.py 或手动构建请求体 +- [ ] Step 7: PUT /lab/notebook/agent-result 提交 +- [ ] Step 8: 检查返回结果,确认提交成功 +``` + +--- + +## 常见问题 + +### Q: notebook_uuid 从哪里获取? + +从之前「批量提交实验」时 `POST /api/v1/lab/notebook` 的返回值 `data.uuid` 获取。也可以在平台 UI 中查找对应的 notebook。 + +### Q: agent_result 有固定的 schema 吗? + +没有严格 schema,接受任意 JSON 对象。但建议包含有意义的字段名和结构化数据,方便后续分析。 + +### Q: 可以多次提交同一个 notebook 的结果吗? + +可以,后续提交会覆盖之前的 agent_result。 + +### Q: 认证方式是 Lab 还是 Api? + +本指南统一使用 `Authorization: Lab ` 方式。如果用户有独立的 API Key,也可用 `Authorization: Api ` 替代。 diff --git a/.cursor/skills/submit-agent-result/scripts/prepare_agent_result.py b/.cursor/skills/submit-agent-result/scripts/prepare_agent_result.py new file mode 100644 index 00000000..2ee4e17f --- /dev/null +++ b/.cursor/skills/submit-agent-result/scripts/prepare_agent_result.py @@ -0,0 +1,133 @@ +""" +读取实验结果文件(JSON / CSV),整合为 agent_result 请求体并可选提交。 + +用法: + python prepare_agent_result.py \ + --notebook-uuid \ + --files data1.json data2.csv \ + [--auth ] \ + [--base ] \ + [--submit] \ + [--output ] + +支持的输入文件格式: + - .json → 直接作为 dict 合并 + - .csv → 转为 {"filename": [row_dict, ...]} 格式 +""" + +import argparse +import base64 +import csv +import json +import os +import sys +from pathlib import Path +from typing import Any, Dict, List + + +def read_json_file(filepath: str) -> Dict[str, Any]: + with open(filepath, "r", encoding="utf-8") as f: + return json.load(f) + + +def read_csv_file(filepath: str) -> List[Dict[str, Any]]: + rows = [] + with open(filepath, "r", encoding="utf-8-sig") as f: + reader = csv.DictReader(f) + for row in reader: + converted = {} + for k, v in row.items(): + try: + converted[k] = int(v) + except (ValueError, TypeError): + try: + converted[k] = float(v) + except (ValueError, TypeError): + converted[k] = v + rows.append(converted) + return rows + + +def merge_files(filepaths: List[str]) -> Dict[str, Any]: + """将多个文件合并为一个 agent_result dict""" + merged: Dict[str, Any] = {} + for fp in filepaths: + path = Path(fp) + ext = path.suffix.lower() + key = path.stem + + if ext == ".json": + data = read_json_file(fp) + if isinstance(data, dict): + merged.update(data) + else: + merged[key] = data + elif ext == ".csv": + merged[key] = read_csv_file(fp) + else: + print(f"[警告] 不支持的文件格式: {fp},跳过", file=sys.stderr) + + return merged + + +def build_request_body(notebook_uuid: str, agent_result: Dict[str, Any]) -> Dict[str, Any]: + return { + "notebook_uuid": notebook_uuid, + "agent_result": agent_result, + } + + +def submit(base: str, auth: str, body: Dict[str, Any]) -> Dict[str, Any]: + try: + import requests + except ImportError: + print("[错误] 提交需要 requests 库: pip install requests", file=sys.stderr) + sys.exit(1) + + url = f"{base}/api/v1/lab/notebook/agent-result" + headers = { + "Content-Type": "application/json", + "Authorization": f"Lab {auth}", + } + resp = requests.put(url, json=body, headers=headers, timeout=30) + return {"status_code": resp.status_code, "body": resp.json() if resp.headers.get("content-type", "").startswith("application/json") else resp.text} + + +def main(): + parser = argparse.ArgumentParser(description="整合实验结果文件并构建 agent_result 请求体") + parser.add_argument("--notebook-uuid", required=True, help="目标 notebook UUID") + parser.add_argument("--files", nargs="+", required=True, help="输入文件路径(JSON / CSV)") + parser.add_argument("--auth", help="Lab token(base64(ak:sk))") + parser.add_argument("--base", help="API base URL") + parser.add_argument("--submit", action="store_true", help="直接提交到云端") + parser.add_argument("--output", default="agent_result_body.json", help="输出 JSON 文件路径") + + args = parser.parse_args() + + for fp in args.files: + if not os.path.exists(fp): + print(f"[错误] 文件不存在: {fp}", file=sys.stderr) + sys.exit(1) + + agent_result = merge_files(args.files) + body = build_request_body(args.notebook_uuid, agent_result) + + with open(args.output, "w", encoding="utf-8") as f: + json.dump(body, f, ensure_ascii=False, indent=2) + print(f"[完成] 请求体已保存: {args.output}") + print(f" notebook_uuid: {args.notebook_uuid}") + print(f" agent_result 字段数: {len(agent_result)}") + print(f" 合并文件数: {len(args.files)}") + + if args.submit: + if not args.auth or not args.base: + print("[错误] 提交需要 --auth 和 --base 参数", file=sys.stderr) + sys.exit(1) + print(f"\n[提交] PUT {args.base}/api/v1/lab/notebook/agent-result ...") + result = submit(args.base, args.auth, body) + print(f" HTTP {result['status_code']}") + print(f" 响应: {json.dumps(result['body'], ensure_ascii=False)}") + + +if __name__ == "__main__": + main()