Merge branch 'wsh_dev' into 'main'
refactor: split submit_sft_via_cml.sh into submit_sft.sh + submit_cml_eval.sh See merge request wuyang6/zk-data-agent!2
This commit is contained in:
@@ -7537,6 +7537,30 @@ def _compute_kpis(
|
||||
if run_max is not None
|
||||
else None
|
||||
)
|
||||
# Fallback: max runDic 可能命中 R{n} retry 轮(cml 还在 running,lark_template
|
||||
# 没产出),让 KPI 全部空白。这时回退到 state 里最新一个 `cml=complete` 的
|
||||
# 那一轮的 runDic — baseline / 上一轮成功 eval 的指标至少能显示出来。
|
||||
if metrics is None:
|
||||
per_round = _resolve_run_dic_per_round(state_entries)
|
||||
for entry in reversed(state_entries):
|
||||
if entry.get('step') != 'cml' or entry.get('status') != 'complete':
|
||||
continue
|
||||
rid = entry.get('run_id')
|
||||
if not isinstance(rid, str):
|
||||
continue
|
||||
fallback_run = per_round.get(rid.strip())
|
||||
if fallback_run is None or fallback_run == run_max:
|
||||
continue
|
||||
metrics = _read_workflow_metric_diff(
|
||||
fallback_run,
|
||||
agent_state=agent_state,
|
||||
account_id=account_id,
|
||||
session_id=session_id,
|
||||
)
|
||||
if metrics is not None:
|
||||
run_max = fallback_run
|
||||
run_id_value = str(fallback_run)
|
||||
break
|
||||
_enrich_metrics_from_eval_output(metrics, target_set)
|
||||
target_metric = _format_metric_value(metrics, 'target_set_pass_rate')
|
||||
overall_metric = _format_metric_value(metrics, 'overall_car_pass_rate')
|
||||
|
||||
@@ -535,7 +535,7 @@ type IterationEntry = {
|
||||
iteration?: number;
|
||||
runDic?: number;
|
||||
timestamp?: string;
|
||||
hypothesis?: string;
|
||||
hypothesis?: unknown;
|
||||
intervention?: { type?: string; summary?: string } | string;
|
||||
prediction?: Record<string, number | string>;
|
||||
results?: Record<string, number | string>;
|
||||
@@ -546,7 +546,7 @@ type IterationEntry = {
|
||||
cases?: number;
|
||||
}>;
|
||||
error_delta?: { persistent?: number; new?: number; fixed?: number };
|
||||
next_hypothesis?: string;
|
||||
next_hypothesis?: unknown;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
@@ -648,7 +648,7 @@ function IterationCard({
|
||||
<div className="space-y-3 px-4 py-3 text-[13px]">
|
||||
{entry.hypothesis ? (
|
||||
<Field label="假设" tone="primary">
|
||||
{entry.hypothesis}
|
||||
<RichText value={entry.hypothesis} />
|
||||
</Field>
|
||||
) : null}
|
||||
{intervention.summary || intervention.type ? (
|
||||
@@ -737,7 +737,7 @@ function IterationCard({
|
||||
) : null}
|
||||
{entry.next_hypothesis ? (
|
||||
<Field label="下一步假设" tone="accent">
|
||||
{entry.next_hypothesis}
|
||||
<RichText value={entry.next_hypothesis} />
|
||||
</Field>
|
||||
) : null}
|
||||
{otherKeys.length > 0 ? (
|
||||
@@ -759,6 +759,18 @@ function IterationCard({
|
||||
);
|
||||
}
|
||||
|
||||
function RichText({ value }: { value: unknown }) {
|
||||
if (value == null) return null;
|
||||
if (typeof value === "string" || typeof value === "number") {
|
||||
return <>{String(value)}</>;
|
||||
}
|
||||
return (
|
||||
<pre className="overflow-x-auto whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed">
|
||||
{JSON.stringify(value, null, 2)}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({
|
||||
label,
|
||||
children,
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
---
|
||||
name: model-iteration
|
||||
description: 小爱中控模型自主迭代框架(autoresearch-zk)。用于对小爱同学中控理解调度模型进行假设驱动的自主 SFT+评测迭代循环。触发信号:用户发送"开始,需求集合名"(如"开始,icl_test")时,必须立即使用此 skill 启动迭代,不得自行发挥。任何涉及 cml 评测、zk 模型训练、数据增强、badcase 分析、augment_*.jsonl 生成的任务,也应使用此 skill。
|
||||
description: 小爱中控模型自主迭代框架(autoresearch-zk)。用于对小爱同学中控理解调度模型进行假设驱动的自主 SFT+评测迭代循环。
|
||||
when_to_use: |
|
||||
用户表达以下任一意图时启用此 skill:
|
||||
- 直接的核心触发短语:「开始,<需求集合名>」(如「开始,icl_test」)
|
||||
- 自然语言的训练/迭代请求:「我要进行模型训练」「开始模型训练」「目标集合是 xxx」「跑一下需求集合 xxx」「迭代 xxx」「针对 xxx 做模型迭代」
|
||||
- 平台运维:「评测当前模型」「分析 badcase」「Gold drift 检查」「数据增强 augment_*.jsonl」「提交 SFT 训练」「跑 CML workflow」「写 iteration_log」
|
||||
- 关键词:CML 评测 / SFT / autoresearch / zk 中控理解调度 / requirement set / specific test / metric_diff / lark_template
|
||||
无需用户严格按「开始,xxx」字面匹配;只要意图涉及小爱中控模型 SFT+评测迭代,就立刻使用此 skill。
|
||||
仅在用户通过 UI 明确点击使用 model-iteration skill 时触发。不要从自然语言推断意图自动启用。
|
||||
---
|
||||
|
||||
# autoresearch-zk — 小爱中控模型自主迭代框架
|
||||
@@ -25,6 +20,9 @@ when_to_use: |
|
||||
| "我把现状停在这里,把决策摘要给你 review" | 不准。摘要可以写,但不准停 |
|
||||
| "Step 4 augment 完成 ✅,等回调后续做 SFT" | augment=complete 那一刻就是 SFT 启动那一刻——**同一轮 bash** 紧接着跑 §4.5 label-master 复核 + 提交 SFT,不许写简报、不许等回调(详见 program.md "Step 4 → Step 5 硬连接")|
|
||||
| "R1 评测发现 regression,先把诊断给你看,等你拍板再决定要不要回滚" | dist-analysis 发现 regression **也算 dist-analysis 完成**——**同轮 bash** 紧接着写 `results/workflow<runDic>.md`(包含完整 delta 表 + new_fail/new_fix 子集 + 病灶定位 + 回滚建议)+ append `iteration_log.jsonl` R{n} entry。文件落完了再用 chat reply 给人提回滚选项。**不许把诊断只写在 chat 里、不落盘**——前端「分层结果分析」卡片读的是 `results/workflow<runDic>.md`,你不写卡片永远停在上轮。 |
|
||||
| "SFT 用哪个 basemodel?(a) 用户 eval 给的路径 (b) 默认 Qwen3-4B" | program.md §5.0 写死 `--model_path = /mnt/wangsenhao/verl_zk/Qwen3-4B-Instruct-2507`,**每轮强制 basemodel**——这是规则不是选项。eval 阶段的 `model_path_new` 跟 SFT 的 `--model_path` 是两件事,不要混。直接用 Qwen3-4B-Instruct-2507。 |
|
||||
| "zk_trainer 仓库 URL 是什么?" | program.md §5.0 line 1323 写死 `git clone git@git.n.xiaomi.com:wangsenhao/zk_trainer.git`,**直接用**。clone 失败先看 ssh key(program.md 「SSH Key 检查」),**不要**自己脑补 `nlp/`、`xiaoai/`、`autoresearch/` 等命名空间问用户。 |
|
||||
| "label-master 复核 100 条仿写,5 条全 pass,按比例外推 100 条 OK" | **`X/X pass` 缩写禁用**——§4.5 写死「H2 augment_<runDic>.jsonl 必须逐条过 label-master」,review jsonl 行数必须 == augment.jsonl 行数(1:1 覆盖)。抽样外推 = 协议违反,§5.0 准入门会用 `wc -l` 拦住。状态日志里写"100 条复核完成"也必须真的是 100 条 review entry。 |
|
||||
|
||||
**合法暂停只有**:
|
||||
|
||||
@@ -65,6 +63,7 @@ when_to_use: |
|
||||
- [ ] 写本轮假设到 iteration_log.jsonl 的 hypothesis 字段
|
||||
- [ ] 假设必须有依据(指向 §2.3 / §2.4 的具体发现)
|
||||
- [ ] **`step:"hypothesis"` entry 写在当前 round(R{n})名下**——不要写成 R{n+1}。后端把 hypothesis 归类为 analysis 类,是 R{n}·Baseline / R{n}·Analysis 的最后一张卡,不是 R{n+1}·Train 的开头。这样 gate(Human Check / Review)会插在 hypothesis 卡之后、R{n+1}·Train 之前,**用户看到假设内容再拍板是否进 train**。
|
||||
- [ ] **`hypothesis=complete` 之前,必须 append `iteration_log.jsonl` 一条 R{n} entry**,schema 至少包含 `iteration:n / runDic:<本轮编号> / timestamp / results:{<指标 key>:<float>...} / hypothesis`。**前端 metrics 折线图唯一数据源就是这个文件**,没写 → 图表彻底空白(state file 写得再全也不行)。已踩坑:R0 走完 dist-analysis + hypothesis 但漏了 iteration_log,前端 metrics 卡看不到任何点,指标空了一整轮。`results` 必须用 program.md 「§iteration_log schema」里固定的 metric key(`req_set_car / dapan_car / specific_test / triage_err_rate / bvt_nav / talkable_controllable` 等),key 拼错前端按缺失处理。
|
||||
|
||||
### Step 1 → Step 3 边界(**NEVER STOP 硬连接**,R0 / R1+ regression 都适用)
|
||||
- [ ] dist-analysis 算完 delta(new_fail / new_fix / persistent / 子集分布)那一刻起,**同一轮 bash 不许结束**:紧接着写 `results/workflow<runDic>.md`(包含完整指标表 + 病灶定位 + 假设 + 回滚/继续建议)→ append `iteration_log.jsonl` R{n} entry(`results` 字段填本轮 metric,`hypothesis` 字段填下一动作)
|
||||
@@ -80,10 +79,10 @@ when_to_use: |
|
||||
- [ ] §4.0.1 Step B:量级判定基于 label-master 处理后的**残留候选量**走对应支线(≤50 自动按 label-master 推荐改 / 51-200 全量人审 / >200 触发 #3,全量交付)
|
||||
- [ ] §4.0.1 Step C:备份 `.bak` 文件
|
||||
- [ ] §4.1 输入准备 / §4.2 GPT 调用 / §4.3 sanity check / §4.4 写入
|
||||
- [ ] **§4.5 label-master 标签复核(落盘后必做,强制)**:H1 `modified_samples.jsonl` 只跑层 1(格式)—— 语义已在 §4.0.1 Step A.5 完成;H2 `augment_<runDic>.jsonl` 跑两层(`validate_label_output.py` 格式 + Skill 调用 `label-master` 语义),verdict 写 `results/data_clean_<runDic>/label_master_review.jsonl`,不通过比例 ≤ 5% 才能进 Step 5
|
||||
- [ ] **§4.5 label-master 标签复核(落盘后必做,强制)**:H1 `modified_samples.jsonl` 只跑层 1(格式)—— 语义已在 §4.0.1 Step A.5 完成;H2 `augment_<runDic>.jsonl` 跑两层(`validate_label_output.py` 格式 + Skill 调用 `label-master` 语义),**逐条 1:1 全量覆盖**(`augment` 多少行 `label_master_review.jsonl` H2 部分就多少行,禁止抽样 / spot-check / X/X pass 外推),verdict 写 `results/data_clean_<runDic>/label_master_review.jsonl`,不通过比例 ≤ 5% 才能进 Step 5
|
||||
|
||||
### Step 4 → Step 5 边界(**NEVER STOP 硬连接**,反复踩坑)
|
||||
- [ ] 写完 `augment=complete` 那一刻,**同一轮 bash 不许结束**:紧接着跑 §4.5 label-master 复核 → 写 `sft=running` → 调 `submit_sft_via_cml.sh` → 挂 watcher
|
||||
- [ ] 写完 `augment=complete` 那一刻,**同一轮 bash 不许结束**:紧接着跑 §4.5 label-master 复核 → 写 `sft=running` → 调 `submit_sft.sh` → 挂 watcher(评测在 SFT `_SUCCESS` 落盘后的下一轮单独用 `submit_cml_eval.sh` 起,不要在 SFT bg 里串接评测)
|
||||
- [ ] **不许写"Step 4 完成"进度简报后 turn 结束**,不许"等回调后续做 SFT"
|
||||
- [ ] H2 后台任务回调(`[system] 后台任务 ... exit_code=0`)**不是 turn 结束信号**——它只是 augment 子流程的一个中间节拍,agent 必须在同轮里继续走完 §4.5 → Step 5
|
||||
|
||||
@@ -163,7 +162,7 @@ bg 任务(`bash(run_in_background=true)`)的合法范围是**一个 step 内
|
||||
|
||||
**为什么是死规则**:bg 脚本本身不会 append `program-state.jsonl`,状态只活在 bg 进程内存。一个 bg 跨 step 跑起来后,前端从第一个 step complete 之后就完全感知不到后续 step——卡片永远停在第一个 step,watcher 也没注册新的。**bg 进程死了 / session 重连 / 中途想插手** 都补不回来,因为 program-state.jsonl 上没任何线索。
|
||||
|
||||
**自查判据**:bg 脚本里出现新 step 的关键词(`cml workflow run` 提评测、`submit_sft_via_cml.sh`、对应新 step 的 `echo running`),就是越界。bg 只该做"提一个远端 job 然后等它的产物",**绝不能跨 step 边界**。
|
||||
**自查判据**:bg 脚本里出现新 step 的关键词(`cml workflow run` 提评测、`submit_cml_eval.sh`、对应新 step 的 `echo running`),就是越界。bg 只该做"提一个远端 job 然后等它的产物",**绝不能跨 step 边界**。
|
||||
|
||||
**watcher fire → 当轮 4 件套**(漏一件就断片):
|
||||
1. echo 上一 step `complete`(watcher 已自动写也算)
|
||||
@@ -389,7 +388,8 @@ echo '{"log":{"ts":"11:55:10","iter":"R1","text":"Step 1 dist-analysis: 写 work
|
||||
|---|---|---|
|
||||
| `prepare_and_train_sft.py` | `$AUTORESEARCH_CHAT_ROOT/scripts/` | 后端 bind 时从 skill bundle 推过来,每次刷新最新版 |
|
||||
| `ai-planning/`(含 corpus + augment) | `$AUTORESEARCH_CHAT_ROOT/ai-planning/` | **首次访问前你自己 git clone**(见 program.md 「ai-planning bootstrap」) |
|
||||
| `zk_trainer/` | `$AUTORESEARCH_CHAT_ROOT/zk_trainer/` | **Step 5 首次 SFT 前你自己 git clone**(见 program.md §5.0) |
|
||||
| `zk_trainer/` | `$AUTORESEARCH_CHAT_ROOT/zk_trainer/` | **Step 5 首次 SFT 前你自己 git clone**:`git clone git@git.n.xiaomi.com:wangsenhao/zk_trainer.git`(见 program.md §5.0 line 1323)。**URL 写死了,不要问用户也不要换命名空间**;clone 失败先看 ssh key |
|
||||
| basemodel(每轮 SFT 的 `--model_path`) | `/mnt/wangsenhao/verl_zk/Qwen3-4B-Instruct-2507` | **写死、强制**(见 program.md §5.0 line 1330 / 1345)。每轮 SFT 都从 basemodel 起,**不许从 sft_output 续训**,**不要问用户用哪个**——eval 阶段的 `model_path_new` 是另一回事 |
|
||||
| `results/iteration_log.jsonl` | `$AUTORESEARCH_CHAT_ROOT/results/` | 每 chat 独立累积;后端 KPI 也读这里 |
|
||||
| `results/error_registry.jsonl` | 同上 | 每 chat 独立 |
|
||||
| `results/workflow<runDic>.md / data_clean_<runDic>/ / augment_raw/` | 同上 | 每 chat 独立 |
|
||||
@@ -454,7 +454,7 @@ LOOP:
|
||||
Step 1: 分层结果分析 + 问题分析 & 报告(需求集合 → 大盘车载 → specific test;根因归类 / 跨轮 diff / 天花板诊断 / 写 workflow<runDic>.md)
|
||||
Step 3: 形成假设(写入 iteration_log.jsonl)
|
||||
Step 4: 数据生成(仅 data 归因时做)
|
||||
Step 5: SFT 训练(submit_sft_via_cml.sh)
|
||||
Step 5: SFT 训练(submit_sft.sh)→ 训练成功后下一轮评测用 submit_cml_eval.sh
|
||||
Step 6: 记录结果(iteration_log.jsonl / error_registry.jsonl)
|
||||
→ 回 Step 0
|
||||
```
|
||||
|
||||
@@ -1262,7 +1262,9 @@ python skills/label-master/scripts/validate_label_output.py \
|
||||
|
||||
H1 在 §4.0.1 Step A.5 已经过 label-master 推荐覆盖,这里**不再重跑**。H2 仿写是 §4.2 GPT 新增的样本,没经过 Step A.5,必须在此补一次语义判定:
|
||||
|
||||
1. 从 `augment_<runDic>.jsonl` 抽出 `(query, output)` 对
|
||||
🚨 **覆盖率硬规则**:`label_master_review.jsonl` 里 H2 部分的行数**必须等于** `augment_<runDic>.jsonl` 的行数(1:1 全量覆盖)。**禁止抽样、禁止"前 N 条 spot-check"、禁止"5/5 pass 推 100 OK"**——§5.0 准入门会用 `wc -l` 拦。已踩坑:R17817 跑了 5 条样本就上报"100 条 pass",被 §5.0 退回。
|
||||
|
||||
1. 逐行读 `augment_<runDic>.jsonl` **全部** `(query, output)` 对(N 条就是 N 条,不省略、不抽样)
|
||||
2. **逐条**调 `Skill(skill="label-master", args=...)`——每次只传**一条** `(query, 当前 label)`:
|
||||
- 让 label-master 按 §决策流程 / §候选召回索引 / §高频混淆边界 判定该条
|
||||
- 输出格式:`{verdict: 通过 | 不通过, 推荐标签, 排除理由, 易混淆边界}`
|
||||
@@ -1280,7 +1282,7 @@ H1 在 §4.0.1 Step A.5 已经过 label-master 推荐覆盖,这里**不再重
|
||||
|
||||
**写完复核报告才允许进 Step 5。**verdict 文件不存在 / 没跑层 2 → Step 5 拒绝启动(5.0 的准入检查会查 `label_master_review.jsonl` 存在)。
|
||||
|
||||
🚨 **注意**:label-master 是"知识 + Agent 推理"型 Skill,不是黑盒分类器。批量调用时严禁要求它返回 1/0 标量;必须输出 `verdict + 推荐标签 + 排除理由` 三件套,便于人类回查。
|
||||
🚨 **注意**:label-master 是"知识 + Agent 推理"型 Skill,不是黑盒分类器。每一次调用都必须输出 `verdict + 推荐标签 + 排除理由` 三件套,便于人类回查;严禁要求它返回 1/0 标量。(批量调用本身已在第 2 步禁掉,这里再强调一次输出 schema。)
|
||||
|
||||
### 5. SFT 训练
|
||||
|
||||
@@ -1290,7 +1292,7 @@ H1 在 §4.0.1 Step A.5 已经过 label-master 推荐覆盖,这里**不再重
|
||||
|
||||
1. `echo '{"step":"sft","status":"running",...}' >> $SESSION_OUTPUT/program-state.jsonl`
|
||||
2. 跑层 1 + 层 2 label-master 复核(§4.5)
|
||||
3. 如果复核全过:在 SAME bash turn 里调 `bash scripts/submit_sft_via_cml.sh <RUNDIC> <PREV_RUNDIC>` 并立刻挂 watcher(§5.2 的 cml custom_train + workflow chain)
|
||||
3. 如果复核全过:在 SAME bash turn 里调 `bash scripts/submit_sft.sh <SFT_RUNDIC> [PREV_RUNDIC]` 并立刻挂 watcher(§5.2)。训练 `_SUCCESS` 落盘后的下一轮 step 单独用 `bash scripts/submit_cml_eval.sh <EVAL_RUNDIC>` 起评测,**不要**把评测串到 SFT bg 任务里——跨 step bg 是禁区
|
||||
|
||||
**反模式**(被反复踩坑,永远禁止):
|
||||
|
||||
@@ -1318,7 +1320,20 @@ if [ "$TOTAL" -gt 0 ] && [ $((NOT_PASS * 100 / TOTAL)) -gt 5 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. zk_trainer 仓库已 clone
|
||||
# 1b. coverage 断言:augment 行数必须被 review 完整覆盖(杜绝抽样外推)
|
||||
# review.jsonl 同时包含 §4.0.1 Step A.5 的 mislabel candidate review + §4.5 的 augment 仿写 review
|
||||
# 所以行数下界 = augment.jsonl 行数(mislabel 部分多出来的那批不影响下界)
|
||||
AUG="$AUTORESEARCH_CHAT_ROOT/ai-planning/data/train_set/zk_intent/augment_${RUNDIC}.jsonl"
|
||||
if [ -f "$AUG" ]; then
|
||||
AUG_N=$(wc -l < "$AUG")
|
||||
REV_N=$(wc -l < "$REVIEW")
|
||||
if [ "$REV_N" -lt "$AUG_N" ]; then
|
||||
echo "label-master review 行数 $REV_N < augment 条数 $AUG_N — §4.5 必须逐条覆盖,禁止抽样外推(如 5/5 pass 推 100 OK),回 §4.5 把 augment.jsonl 的每一条都 append 一行 review entry"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. zk_trainer 仓库已 clone(URL 写死,不要换命名空间,clone 失败先看 ssh key)
|
||||
cd "$AUTORESEARCH_CHAT_ROOT"
|
||||
[ -d zk_trainer ] || git clone git@git.n.xiaomi.com:wangsenhao/zk_trainer.git
|
||||
```
|
||||
@@ -1373,11 +1388,11 @@ R23-R28 期间累积了 `sft_output_r23` ~ `sft_output_r28`,多次需要回退
|
||||
|
||||
**自动评测 watcher**:
|
||||
|
||||
R29 起 SFT 走 cml custom_train submit(见 5.2),watcher 直接复用 `submit_sft_via_cml.sh` 内嵌的轮询逻辑——`cml custom_train describe` 检测 succeed → 验产出 → 自动起 `cml workflow run` 评测。本地无 PID 可监控。
|
||||
R29 起 SFT 走 cml custom_train submit(见 5.2),训练用 `submit_sft.sh` 提交、评测用 `submit_cml_eval.sh` 单独起。这两步**不再串接**——agent 用 watcher 监听 `sft_output/_SUCCESS`,watcher fire 后下一轮主动调 `submit_cml_eval.sh`。本地无 PID 可监控,全靠 watcher 串两个 step。
|
||||
|
||||
##### Claude-side 双 watcher 强制要求(R17775 经验沉淀)
|
||||
|
||||
`submit_sft_via_cml.sh` 内嵌的 bash watcher **只负责"训练完 → 起评测"**,它不会通知 Claude。Claude 如果只挂一个"等评测产物"的 watcher,训练完成事件会被漏报(R17775 训练 19:57 完成,Claude 40+min 不知道,直到用户问)。
|
||||
旧版 `submit_sft_via_cml.sh` 在脚本内嵌 watcher 自动起评测——已废弃(违反"bg 任务作用域 = 单 step"原则)。现在 `submit_sft.sh` 只负责提 SFT、立刻退出;起评测必须由 agent 在 SFT watcher fire 后**新一轮**手动调 `submit_cml_eval.sh`。Claude 如果只挂一个"等评测产物"的 watcher,训练完成事件会被漏报(R17775 训练 19:57 完成,Claude 40+min 不知道,直到用户问)。
|
||||
|
||||
**Claude 调用 submit 脚本后,必须立刻用 `Bash run_in_background` 起两个 watcher task**(一个都不能少):
|
||||
|
||||
@@ -1476,26 +1491,33 @@ R23-R28 期间用本地 `nohup python3 prepare_and_train_sft.py train ...` 启
|
||||
|
||||
##### 一键提交脚本
|
||||
|
||||
⚠️ **runDic 不许手敲**:先 `eval` 一下 `resolve_run_ids.sh` 拿到这一轮的 `$SFT_RUNDIC` / `$EVAL_RUNDIC`,再原样传给 `submit_sft_via_cml.sh`。R2 踩过的坑就是手算 runDic 时把 SFT yaml 的命名也跟着 +1,导致 `data_clean_<EVAL_RUNDIC>/` 跳过了 R1 命名空间。
|
||||
⚠️ **runDic 不许手敲**:先 `eval` 一下 `resolve_run_ids.sh` 拿到这一轮的 `$SFT_RUNDIC` / `$EVAL_RUNDIC`,再原样传给两个独立脚本。R2 踩过的坑就是手算 runDic 时把 SFT yaml 的命名也跟着 +1,导致 `data_clean_<EVAL_RUNDIC>/` 跳过了 R1 命名空间。
|
||||
|
||||
提交流程拆成两步(**两个独立脚本,两个 step**):
|
||||
|
||||
```bash
|
||||
cd "$AUTORESEARCH_CHAT_ROOT"
|
||||
eval "$(./scripts/resolve_run_ids.sh)" # 解析出 $SFT_RUNDIC / $EVAL_RUNDIC
|
||||
./scripts/submit_sft_via_cml.sh "$SFT_RUNDIC" "$EVAL_RUNDIC"
|
||||
|
||||
# 等价示例(仅供阅读,**实际调用一律走 eval 上面那行**):
|
||||
# R29 上一轮已落盘的 workflow 是 17755,因此 SFT_RUNDIC=17755, EVAL_RUNDIC=17756
|
||||
# ./scripts/submit_sft_via_cml.sh 17755 17756
|
||||
# Step 5:提交 SFT 训练(不附带任何评测逻辑)
|
||||
./scripts/submit_sft.sh "$SFT_RUNDIC"
|
||||
|
||||
# 训练 _SUCCESS 落盘后的下一轮 step 才起评测:
|
||||
./scripts/submit_cml_eval.sh "$EVAL_RUNDIC"
|
||||
```
|
||||
|
||||
`submit_sft_via_cml.sh` 接受三个位置参数:`<SFT_RUNDIC> <EVAL_RUNDIC> [PREV_RUNDIC]`。`PREV_RUNDIC` 默认 `SFT_RUNDIC-1`,仅用作上一轮 `sft_output_r<PREV_RUNDIC>` 改名。脚本内部断言 `EVAL_RUNDIC > SFT_RUNDIC`,否则直接报错退出。
|
||||
`submit_sft.sh <SFT_RUNDIC> [PREV_RUNDIC]`:
|
||||
|
||||
脚本做的事:
|
||||
1. 渲染 `scripts/sft_train_job.yaml.tpl` 为本轮 yaml(替换 `{RUNDIC}` → `$SFT_RUNDIC`、`{PREV_RUNDIC}` → `$PREV_RUNDIC`)
|
||||
2. `cml custom_train submit --filename <yaml>` 提交任务,拿 `JobID`
|
||||
3. 后台 watcher 进程:每 60s `cml custom_train describe` 轮询任务状态
|
||||
4. 任务 succeed → 验证 `sft_output/_SUCCESS` 存在 → 自动起 cml workflow run,**评测的 `runDic=$EVAL_RUNDIC`**(这是整条流水线唯一一次 +1)
|
||||
5. 任务 failed/killed → 写日志报警
|
||||
2. `cml custom_train submit --filename <yaml>` 提交任务,打印 `JobID`,立刻退出
|
||||
3. **不再内嵌 watcher**——agent 自己用 watcher 监听 `$AUTORESEARCH_CHAT_ROOT/sft_output/_SUCCESS`,watcher fire 后下一轮再调 `submit_cml_eval.sh`
|
||||
|
||||
`submit_cml_eval.sh <EVAL_RUNDIC> [MODEL_NEW] [MODEL_OLD]`:
|
||||
|
||||
1. 从 `skills/model-iteration/assets/config.yaml` 读 `cml_eval.workflow_id` 和 `cml_eval.version`(版本不再硬编码,调版本就改 config.yaml)
|
||||
2. `MODEL_NEW` 默认 `$AUTORESEARCH_ROOT/sft_output`、`MODEL_OLD` 默认旧基线(也可以通过 env / 参数覆盖)
|
||||
3. `cml workflow run --workflow_id ... --version ... --global_inputs runDic=$EVAL_RUNDIC ...` 起评测,**评测 runDic=$EVAL_RUNDIC**(整条流水线唯一一次 +1)
|
||||
4. 立刻退出,UI 卡片状态走 program-state.jsonl 的 watcher 通道
|
||||
|
||||
##### yaml 模板要点(`scripts/sft_train_job.yaml.tpl`)
|
||||
|
||||
@@ -1521,8 +1543,7 @@ cml custom_train logs <JOB_ID> --follow
|
||||
# 停任务
|
||||
cml custom_train kill <JOB_ID>
|
||||
|
||||
# 查 watcher(自动评测)日志
|
||||
tail -f /tmp/r<RUNDIC>_logs/cml_watcher.log
|
||||
# SFT 不再内嵌 watcher,自动评测改成 agent watcher → 单独调 submit_cml_eval.sh
|
||||
```
|
||||
|
||||
##### 与 nohup 路径的对比
|
||||
@@ -1534,7 +1555,7 @@ tail -f /tmp/r<RUNDIC>_logs/cml_watcher.log
|
||||
| 节点故障重试 | 无 | 自动 2 次 |
|
||||
| 失败告警 | 无 | 飞书 P2 |
|
||||
| 任务历史 | 仅本地 log | cml 平台可查 |
|
||||
| 启动复杂度 | 单行 nohup | 单行 submit_sft_via_cml.sh |
|
||||
| 启动复杂度 | 单行 nohup | submit_sft.sh + watcher + submit_cml_eval.sh |
|
||||
|
||||
##### 何时仍用 nohup 本地训
|
||||
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
# 起 CML evaluation workflow(**只做这一件事**)。
|
||||
# workflow_id / version 都从 skills/model-iteration/assets/config.yaml 读,不在这里硬编码。
|
||||
#
|
||||
# 用法:
|
||||
# ./submit_cml_eval.sh <EVAL_RUNDIC> [MODEL_NEW] [MODEL_OLD]
|
||||
#
|
||||
# 默认值:
|
||||
# MODEL_NEW = $AUTORESEARCH_ROOT/sft_output(最新一轮 SFT 产物)
|
||||
# MODEL_OLD = /mnt/wangsenhao/verl_zk/qwen4b_cispo_wokl_add_bvt_2/global_step_5/actor/huggingface
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
EVAL_RUNDIC=${1:?usage: $0 <EVAL_RUNDIC> [MODEL_NEW] [MODEL_OLD]}
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT=${AUTORESEARCH_ROOT:-/mnt/wangsenhao/autoresearch-zk}
|
||||
MODEL_NEW=${2:-${MODEL_NEW:-$ROOT/sft_output}}
|
||||
MODEL_OLD=${3:-${MODEL_OLD:-/mnt/wangsenhao/verl_zk/qwen4b_cispo_wokl_add_bvt_2/global_step_5/actor/huggingface}}
|
||||
|
||||
# 定位 config.yaml:先看脚本旁边的 ../assets/,再看 skills/ 树
|
||||
find_config() {
|
||||
local candidates=(
|
||||
"$SCRIPT_DIR/../assets/config.yaml"
|
||||
"${ZK_AGENT_SKILLS_ROOT:-}/model-iteration/assets/config.yaml"
|
||||
"${AUTORESEARCH_CHAT_ROOT:-}/skills/model-iteration/assets/config.yaml"
|
||||
)
|
||||
local c
|
||||
for c in "${candidates[@]}"; do
|
||||
if [ -n "$c" ] && [ -f "$c" ]; then
|
||||
echo "$c"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
CONFIG=$(find_config) || {
|
||||
echo "[eval] ❌ 找不到 config.yaml(尝试过:$SCRIPT_DIR/../assets/、\$ZK_AGENT_SKILLS_ROOT/model-iteration/assets/、\$AUTORESEARCH_CHAT_ROOT/skills/...)" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 极简 yaml 解析:只提 cml_eval.workflow_id / cml_eval.version(与后端 _read_workflow_version 风格保持一致)
|
||||
read_cml_eval_field() {
|
||||
local field=$1
|
||||
awk -v field="$field" '
|
||||
/^[^[:space:]#]/ { in_cml = ($1 == "cml_eval:") }
|
||||
in_cml && $1 == field { gsub(/^["\x27]|["\x27]$/, "", $2); print $2; exit }
|
||||
' "$CONFIG"
|
||||
}
|
||||
|
||||
EVAL_WORKFLOW_ID=$(read_cml_eval_field "workflow_id:")
|
||||
EVAL_VERSION=$(read_cml_eval_field "version:")
|
||||
|
||||
if [ -z "$EVAL_WORKFLOW_ID" ] || [ -z "$EVAL_VERSION" ]; then
|
||||
echo "[eval] ❌ 从 $CONFIG 读到的 cml_eval 不完整:workflow_id='$EVAL_WORKFLOW_ID' version='$EVAL_VERSION'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f ~/.cloudml-cli/.profile ]; then
|
||||
source ~/.cloudml-cli/.profile
|
||||
else
|
||||
echo "[eval] ❌ 未找到 ~/.cloudml-cli/.profile,请先安装并初始化 cml" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[eval] EVAL_RUNDIC=$EVAL_RUNDIC workflow_id=$EVAL_WORKFLOW_ID version=$EVAL_VERSION (config: $CONFIG)"
|
||||
echo "[eval] MODEL_NEW=$MODEL_NEW"
|
||||
echo "[eval] MODEL_OLD=$MODEL_OLD"
|
||||
|
||||
cml workflow run \
|
||||
--workflow_id "$EVAL_WORKFLOW_ID" --version "$EVAL_VERSION" \
|
||||
--global_inputs runDic="$EVAL_RUNDIC" \
|
||||
--global_inputs model_path_new="$MODEL_NEW" \
|
||||
--global_inputs model_path_old="$MODEL_OLD"
|
||||
|
||||
echo "[eval] ✅ 已提交 workflow$EVAL_RUNDIC,产物在 /mnt/xiaoai-zk-model-train-tj5/workflow5/workflow${EVAL_RUNDIC}/"
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
# 用 cml custom_train submit 提交 SFT 训练(**只做这一件事**)。
|
||||
# 训练完成后请用 scripts/submit_cml_eval.sh 单独起评测,watcher 由 agent 自己挂。
|
||||
#
|
||||
# 用法:
|
||||
# ./submit_sft.sh <SFT_RUNDIC> [PREV_RUNDIC]
|
||||
#
|
||||
# 推荐调用方式(runDic 由 resolve_run_ids.sh 决定,不要手敲):
|
||||
# eval "$(./scripts/resolve_run_ids.sh)"
|
||||
# ./scripts/submit_sft.sh "$SFT_RUNDIC"
|
||||
#
|
||||
# 语义(与 program.md §746 + §5.2 对齐):
|
||||
# SFT_RUNDIC = R{n-1}.runDic(augment / yaml / 归档全用这个,不 +1)
|
||||
# PREV_RUNDIC = SFT_RUNDIC - 1(默认;只用作上一轮 sft_output 改名)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SFT_RUNDIC=${1:?usage: $0 <SFT_RUNDIC> [PREV_RUNDIC]}
|
||||
PREV_RUNDIC=${2:-$((SFT_RUNDIC-1))}
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT=${AUTORESEARCH_ROOT:-/mnt/wangsenhao/autoresearch-zk}
|
||||
TPL=${SFT_TRAIN_JOB_TEMPLATE:-$SCRIPT_DIR/sft_train_job.yaml.tpl}
|
||||
YAML=/tmp/sft_train_job_r${SFT_RUNDIC}.yaml
|
||||
|
||||
if [ -f ~/.cloudml-cli/.profile ]; then
|
||||
source ~/.cloudml-cli/.profile
|
||||
else
|
||||
echo "[sft] ❌ 未找到 ~/.cloudml-cli/.profile,请先安装并初始化 cml" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. 渲染 yaml 模板
|
||||
sed \
|
||||
-e "s|{RUNDIC}|${SFT_RUNDIC}|g" \
|
||||
-e "s|{PREV_RUNDIC}|${PREV_RUNDIC}|g" \
|
||||
-e "s|{AUTORESEARCH_ROOT}|${ROOT}|g" \
|
||||
"$TPL" > "$YAML"
|
||||
echo "[sft] yaml: $YAML (SFT_RUNDIC=$SFT_RUNDIC PREV_RUNDIC=$PREV_RUNDIC)"
|
||||
|
||||
# 2. 提交训练任务
|
||||
SUBMIT_OUT=$(cml custom_train submit --filename "$YAML" 2>&1)
|
||||
echo "$SUBMIT_OUT"
|
||||
JOB_ID=$(echo "$SUBMIT_OUT" | grep -oE 't-[0-9]+-[a-z0-9]+' | head -1)
|
||||
if [ -z "$JOB_ID" ]; then
|
||||
echo "[sft] ❌ 提交失败" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[sft] ✅ JobID: $JOB_ID"
|
||||
|
||||
cat <<EOF
|
||||
|
||||
[sft] 任务提交完成,关键命令:
|
||||
查看任务状态: cml custom_train describe $JOB_ID
|
||||
查看实时日志: cml custom_train logs $JOB_ID --follow
|
||||
停止任务: cml custom_train kill $JOB_ID
|
||||
|
||||
训练成功后产物在: $ROOT/sft_output/_SUCCESS
|
||||
随后用 scripts/submit_cml_eval.sh <EVAL_RUNDIC> 起评测(不要在本脚本里串接,避免跨 step bg 任务)。
|
||||
|
||||
JobID: $JOB_ID
|
||||
SFT_RUNDIC: $SFT_RUNDIC (yaml / sft_output 命名)
|
||||
EOF
|
||||
@@ -1,124 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 用 cml custom_train submit 提交 SFT 训练,训练完成后自动起 cml workflow run 评测
|
||||
#
|
||||
# 用法:
|
||||
# ./submit_sft_via_cml.sh <SFT_RUNDIC> <EVAL_RUNDIC> [PREV_RUNDIC]
|
||||
#
|
||||
# 推荐调用方式(runDic 由 resolve_run_ids.sh 决定,不要手敲):
|
||||
# eval "$(./scripts/resolve_run_ids.sh)"
|
||||
# ./scripts/submit_sft_via_cml.sh "$SFT_RUNDIC" "$EVAL_RUNDIC"
|
||||
#
|
||||
# 语义(与 program.md §746 + §5.2 对齐):
|
||||
# SFT_RUNDIC = R{n-1}.runDic(augment / yaml / 归档全用这个,不 +1)
|
||||
# EVAL_RUNDIC = R{n-1}.runDic + 1(仅本步起 eval 时使用,仅在这一处 +1)
|
||||
# PREV_RUNDIC = SFT_RUNDIC - 1(默认;只用作上一轮 sft_output 改名)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SFT_RUNDIC=${1:?usage: $0 <SFT_RUNDIC> <EVAL_RUNDIC> [PREV_RUNDIC]}
|
||||
EVAL_RUNDIC=${2:?usage: $0 <SFT_RUNDIC> <EVAL_RUNDIC> [PREV_RUNDIC]}
|
||||
PREV_RUNDIC=${3:-$((SFT_RUNDIC-1))}
|
||||
|
||||
# 防御:EVAL_RUNDIC 必须严格大于 SFT_RUNDIC,否则一定是调用方算错
|
||||
if [ "$EVAL_RUNDIC" -le "$SFT_RUNDIC" ]; then
|
||||
echo "[cml-sft] ❌ EVAL_RUNDIC ($EVAL_RUNDIC) 必须 > SFT_RUNDIC ($SFT_RUNDIC)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT=${AUTORESEARCH_ROOT:-/mnt/wangsenhao/autoresearch-zk}
|
||||
MODEL_OLD=${MODEL_OLD:-/mnt/wangsenhao/verl_zk/qwen4b_cispo_wokl_add_bvt_2/global_step_5/actor/huggingface}
|
||||
TPL=${SFT_TRAIN_JOB_TEMPLATE:-$SCRIPT_DIR/sft_train_job.yaml.tpl}
|
||||
YAML=/tmp/sft_train_job_r${SFT_RUNDIC}.yaml
|
||||
|
||||
if [ -f ~/.cloudml-cli/.profile ]; then
|
||||
source ~/.cloudml-cli/.profile
|
||||
else
|
||||
echo "[cml-sft] ❌ 未找到 ~/.cloudml-cli/.profile,请先安装并初始化 cml" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. 渲染 yaml 模板(用 SFT_RUNDIC,不是 EVAL_RUNDIC)
|
||||
sed \
|
||||
-e "s|{RUNDIC}|${SFT_RUNDIC}|g" \
|
||||
-e "s|{PREV_RUNDIC}|${PREV_RUNDIC}|g" \
|
||||
-e "s|{AUTORESEARCH_ROOT}|${ROOT}|g" \
|
||||
"$TPL" > "$YAML"
|
||||
echo "[cml-sft] yaml: $YAML (SFT_RUNDIC=$SFT_RUNDIC EVAL_RUNDIC=$EVAL_RUNDIC PREV_RUNDIC=$PREV_RUNDIC)"
|
||||
|
||||
# 2. 提交训练任务
|
||||
SUBMIT_OUT=$(cml custom_train submit --filename "$YAML" 2>&1)
|
||||
echo "$SUBMIT_OUT"
|
||||
JOB_ID=$(echo "$SUBMIT_OUT" | grep -oE 't-[0-9]+-[a-z0-9]+' | head -1)
|
||||
if [ -z "$JOB_ID" ]; then
|
||||
echo "[cml-sft] ❌ 提交失败" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[cml-sft] ✅ JobID: $JOB_ID"
|
||||
|
||||
# 3. 后台 watcher:等训练成功 → 自动起评测(用 EVAL_RUNDIC)
|
||||
WATCHER_LOG=/tmp/r${SFT_RUNDIC}_logs/cml_watcher.log
|
||||
mkdir -p "$(dirname "$WATCHER_LOG")"
|
||||
|
||||
nohup bash -c '
|
||||
JOB_ID='"$JOB_ID"'
|
||||
SFT_RUNDIC='"$SFT_RUNDIC"'
|
||||
EVAL_RUNDIC='"$EVAL_RUNDIC"'
|
||||
LOG='"$WATCHER_LOG"'
|
||||
ROOT='"$ROOT"'
|
||||
MODEL_NEW="$ROOT/sft_output"
|
||||
MODEL_OLD='"$MODEL_OLD"'
|
||||
EVAL_WORKFLOW_ID=f-20260408161444-wu3pz
|
||||
EVAL_VERSION=v28
|
||||
|
||||
source ~/.cloudml-cli/.profile
|
||||
|
||||
echo "[$(date)] 等待 cml job $JOB_ID 完成..." >> "$LOG"
|
||||
while true; do
|
||||
STATE=$(cml custom_train describe "$JOB_ID" 2>/dev/null | grep -o "\"state\": \"[a-z]*\"" | head -1 | sed "s/.*: \"//;s/\"//")
|
||||
case "$STATE" in
|
||||
succeed)
|
||||
echo "[$(date)] 训练成功" >> "$LOG"
|
||||
break ;;
|
||||
failed|killed)
|
||||
echo "[$(date)] ❌ 训练 $STATE" >> "$LOG"
|
||||
exit 1 ;;
|
||||
*)
|
||||
sleep 60 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 验证产出
|
||||
[ ! -f "$MODEL_NEW/_SUCCESS" ] && [ ! -f "$MODEL_NEW/config.json" ] && {
|
||||
echo "[$(date)] ❌ 产出缺失" >> "$LOG"; exit 1
|
||||
}
|
||||
|
||||
# 起评测(这里且仅这里用 EVAL_RUNDIC)
|
||||
echo "[$(date)] 启动 CML 评测 EVAL_RUNDIC=$EVAL_RUNDIC" >> "$LOG"
|
||||
cml workflow run \
|
||||
--workflow_id $EVAL_WORKFLOW_ID --version $EVAL_VERSION \
|
||||
--global_inputs runDic=$EVAL_RUNDIC \
|
||||
--global_inputs model_path_new=$MODEL_NEW \
|
||||
--global_inputs model_path_old=$MODEL_OLD >> "$LOG" 2>&1
|
||||
echo "[$(date)] cml workflow run 提交完成 (workflow$EVAL_RUNDIC)" >> "$LOG"
|
||||
' > /tmp/r${SFT_RUNDIC}_logs/watcher_runner.log 2>&1 &
|
||||
|
||||
WATCHER_PID=$!
|
||||
echo "[cml-sft] watcher PID: $WATCHER_PID(log: $WATCHER_LOG)"
|
||||
|
||||
# 4. 立即输出可查看命令
|
||||
cat <<EOF
|
||||
|
||||
[cml-sft] 任务提交完成,关键命令:
|
||||
查看任务状态: cml custom_train describe $JOB_ID
|
||||
查看实时日志: cml custom_train logs $JOB_ID --follow
|
||||
停止任务: cml custom_train kill $JOB_ID
|
||||
watcher log: tail -f $WATCHER_LOG
|
||||
|
||||
训练完成后,evaluation workflow 会自动启动;评测产物在
|
||||
/mnt/xiaoai-zk-model-train-tj5/workflow5/workflow${EVAL_RUNDIC}/
|
||||
|
||||
JobID: $JOB_ID
|
||||
SFT_RUNDIC: $SFT_RUNDIC (yaml / sft_output 命名)
|
||||
EVAL_RUNDIC: $EVAL_RUNDIC (workflow 评测目录)
|
||||
EOF
|
||||
+96
-26
@@ -796,34 +796,104 @@ else:
|
||||
max_output_chars: int,
|
||||
) -> str:
|
||||
target = self.resolve_workspace_path(path)
|
||||
if newline_at_end and content and not content.endswith('\n'):
|
||||
content += '\n'
|
||||
mode = 'a' if append else 'w'
|
||||
code = r'''
|
||||
from pathlib import Path
|
||||
target = Path(PATH)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
content = CONTENT
|
||||
with target.open(MODE, encoding="utf-8") as fh:
|
||||
fh.write(content)
|
||||
print(f"{'appended' if MODE == 'a' else 'wrote'} {target} ({len(content)} chars)")
|
||||
'''
|
||||
script = (
|
||||
code.replace('PATH', python_literal(target))
|
||||
.replace('CONTENT', python_literal(content))
|
||||
.replace('MODE', python_literal(mode))
|
||||
api_path = api_path_for_absolute_path(target)
|
||||
if not api_path:
|
||||
raise JupyterRuntimeError(f'Invalid remote path: {target}')
|
||||
|
||||
parent_api = '/'.join(api_path.split('/')[:-1])
|
||||
if parent_api:
|
||||
self._ensure_remote_dir(parent_api)
|
||||
|
||||
final_content = content
|
||||
if append:
|
||||
final_content = self._read_remote_text_for_append(api_path) + content
|
||||
if newline_at_end and final_content and not final_content.endswith('\n'):
|
||||
final_content += '\n'
|
||||
|
||||
encoded = base64.b64encode(final_content.encode('utf-8')).decode('ascii')
|
||||
url = f'{self.binding.base_url}/api/contents/{quote(api_path, safe="/")}'
|
||||
response = self._session.put(
|
||||
url,
|
||||
json={
|
||||
'type': 'file',
|
||||
'format': 'base64',
|
||||
'content': encoded,
|
||||
},
|
||||
headers={
|
||||
'X-XSRFToken': self._xsrf_token,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout=60,
|
||||
)
|
||||
result = self.run_python(
|
||||
code=script,
|
||||
script_path=None,
|
||||
args=[],
|
||||
stdin=None,
|
||||
timeout_seconds=20.0,
|
||||
max_output_chars=max_output_chars,
|
||||
if response.status_code >= 400:
|
||||
raise JupyterRuntimeError(
|
||||
f'Unable to write remote file: HTTP {response.status_code} {response.text[:500]}'
|
||||
)
|
||||
if result.exit_code != 0:
|
||||
raise JupyterRuntimeError(result.stdout.strip() or 'remote write_file failed')
|
||||
return result.stdout.strip()
|
||||
verb = 'appended' if append else 'wrote'
|
||||
return f'{verb} {target} ({len(final_content)} chars)'
|
||||
|
||||
def _ensure_remote_dir(self, api_path: str) -> None:
|
||||
parts = [segment for segment in api_path.split('/') if segment]
|
||||
cumulative = ''
|
||||
for segment in parts:
|
||||
cumulative = f'{cumulative}/{segment}' if cumulative else segment
|
||||
url = f'{self.binding.base_url}/api/contents/{quote(cumulative, safe="/")}'
|
||||
resp = self._session.get(
|
||||
url,
|
||||
params={'content': 0},
|
||||
headers={'X-XSRFToken': self._xsrf_token},
|
||||
timeout=30,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
kind = resp.json().get('type')
|
||||
if kind == 'directory':
|
||||
continue
|
||||
raise JupyterRuntimeError(
|
||||
f'Cannot create directory at {cumulative}: path exists as {kind}'
|
||||
)
|
||||
if resp.status_code != 404:
|
||||
raise JupyterRuntimeError(
|
||||
f'Unable to inspect remote path {cumulative}: HTTP {resp.status_code} {resp.text[:300]}'
|
||||
)
|
||||
create = self._session.put(
|
||||
url,
|
||||
json={'type': 'directory'},
|
||||
headers={
|
||||
'X-XSRFToken': self._xsrf_token,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
if create.status_code >= 400:
|
||||
raise JupyterRuntimeError(
|
||||
f'Unable to create remote directory {cumulative}: HTTP {create.status_code} {create.text[:300]}'
|
||||
)
|
||||
|
||||
def _read_remote_text_for_append(self, api_path: str) -> str:
|
||||
url = f'{self.binding.base_url}/api/contents/{quote(api_path, safe="/")}'
|
||||
resp = self._session.get(
|
||||
url,
|
||||
params={'content': 1},
|
||||
headers={'X-XSRFToken': self._xsrf_token},
|
||||
timeout=60,
|
||||
)
|
||||
if resp.status_code == 404:
|
||||
return ''
|
||||
if resp.status_code >= 400:
|
||||
raise JupyterRuntimeError(
|
||||
f'Unable to read existing remote file for append: HTTP {resp.status_code} {resp.text[:500]}'
|
||||
)
|
||||
payload = resp.json()
|
||||
if payload.get('type') != 'file':
|
||||
raise JupyterRuntimeError(
|
||||
f'Cannot append: remote path {api_path} is not a file (type={payload.get("type")})'
|
||||
)
|
||||
existing = payload.get('content', '')
|
||||
if not isinstance(existing, str):
|
||||
return ''
|
||||
if payload.get('format') == 'base64':
|
||||
return base64.b64decode(existing).decode('utf-8', errors='replace')
|
||||
return existing
|
||||
|
||||
def resolve_workspace_path(self, raw_path: str) -> str:
|
||||
value = raw_path.strip() or '.'
|
||||
|
||||
Reference in New Issue
Block a user