Merge branch 'wsh_dev' into 'main'
fix: training panel only opens on explicit skill click See merge request wuyang6/zk-data-agent!3
This commit is contained in:
+11
-141
@@ -4,7 +4,6 @@ import type { ExportedMessageRepository } from "@assistant-ui/core";
|
||||
import {
|
||||
AssistantRuntimeProvider,
|
||||
useAui,
|
||||
useAuiState,
|
||||
} from "@assistant-ui/react";
|
||||
import {
|
||||
AssistantChatTransport,
|
||||
@@ -42,6 +41,8 @@ import { ClawSessionReplayProvider } from "@/lib/claw-session-replay";
|
||||
import { pushSessionUrl } from "@/lib/claw-session-url";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
SKILL_TOGGLED_EVENT,
|
||||
type SkillToggledDetail,
|
||||
useAppliedTraining,
|
||||
useModelIterationEnabled,
|
||||
} from "@/lib/use-training-mode";
|
||||
@@ -195,28 +196,6 @@ function getDraftKey(activeSessionId: string | null): string {
|
||||
return activeSessionId;
|
||||
}
|
||||
|
||||
// 用户在对话里表达「我要开始/进行模型训练(这一轮迭代)」时命中的关键短语。
|
||||
// 要求动词或专属触发词必须存在;避免 "什么是模型训练" 之类无意义匹配。
|
||||
const APPLY_INTENT_PATTERNS: RegExp[] = [
|
||||
// 触发短语:开始,xxx / 开始, xxx
|
||||
/(?:^|\s|[,,。.!?])开始\s*[,,]\s*\S+/,
|
||||
// 单纯训练动作:进行/开始/启动/开启/执行/发起/做 + (模型) + 训练
|
||||
/(?:进行|开始|启动|开启|执行|发起|做)\s*(?:模型)?\s*训练/i,
|
||||
// 训练 + 目标集合
|
||||
/训练[\s\S]{0,60}?(?:目标集合|需求集合|specific[\s_-]?test|集合)/i,
|
||||
// 「(模型)训练,目标是 X」「训练,基模 X」「训练 ... csv」之类自然表达
|
||||
/(?:^|[\s,,。.!?])(?:模型)?训练[\s,,][\s\S]{0,80}?(?:目标|基模|specific[\s_-]?test|\.csv)/i,
|
||||
// 跟着 program.md 的旧表达保留
|
||||
/(?:应用|加载|载入|启用|装载|使用|跑|按照?|根据|用)\s*\S*program\.?md/i,
|
||||
/program\.?md[\s\S]{0,120}?(?:训练|应用|启用|跑起来|跑一下)/i,
|
||||
/apply\s+program(?:\.md)?/i,
|
||||
/load\s+program(?:\.md)?/i,
|
||||
/(?:start|begin|run)\s+(?:the\s+)?training/i,
|
||||
];
|
||||
|
||||
function detectApplyIntent(text: string): boolean {
|
||||
return APPLY_INTENT_PATTERNS.some((re) => re.test(text));
|
||||
}
|
||||
|
||||
function AssistantWorkspace() {
|
||||
const aui = useAui();
|
||||
@@ -227,8 +206,6 @@ function AssistantWorkspace() {
|
||||
const { applied, setApplied } = useAppliedTraining(activeSessionId);
|
||||
const { close: closeActivityPanel } = useActivityPanel();
|
||||
const showPipeline = skill.enabled && applied;
|
||||
const messages = useAuiState((s) => s.thread.messages);
|
||||
const lastProcessedUserMsgId = useRef<string | null>(null);
|
||||
const pipelineContainerRef = useRef<HTMLDivElement>(null);
|
||||
const lastPipelineYRef = useRef<number | null>(null);
|
||||
const [exitButtonVisible, setExitButtonVisible] = useState(false);
|
||||
@@ -354,109 +331,17 @@ function AssistantWorkspace() {
|
||||
closeActivityPanel();
|
||||
}, [showPipeline, closeActivityPanel]);
|
||||
|
||||
// Skill 关掉时撤销 applied,避免重新启用就立刻弹开。
|
||||
// 面板只通过用户在 SkillInsertDialog 里显式点击开/关 model-iteration 来控制。
|
||||
// 监听 SkillInsertDialog 发出的自定义事件,不再从 poll 状态变化推断。
|
||||
useEffect(() => {
|
||||
if (!skill.enabled && applied) {
|
||||
setApplied(false);
|
||||
}
|
||||
}, [skill.enabled, applied, setApplied]);
|
||||
|
||||
// 自动应用:skill 启用 + 当前 session 后端的 pipeline 已经有进展(任何
|
||||
// 卡 running 或 complete)→ applied=true。这样关 tab 重开 / 刷新 / 切回
|
||||
// 已在跑的会话都能自动恢复 monitor 面板,不依赖触发词。
|
||||
// 30 秒轮询一次,覆盖「页面打开时 pipeline 还没起、之后才被外部脚本启动」
|
||||
// 这类情形——一次性 fetch 容易错过。
|
||||
useEffect(() => {
|
||||
if (!skill.enabled) return;
|
||||
if (applied) return;
|
||||
if (!activeSessionId) return;
|
||||
let cancelled = false;
|
||||
const pipelineUrl = `/api/claw/training/pipeline?session_id=${encodeURIComponent(activeSessionId)}`;
|
||||
const runUrl = `/api/claw/runs/latest?session_id=${encodeURIComponent(activeSessionId)}`;
|
||||
const tick = async () => {
|
||||
try {
|
||||
const res = await fetch(pipelineUrl, { cache: "no-store" });
|
||||
if (res.ok) {
|
||||
const payload = await res.json();
|
||||
if (cancelled) return;
|
||||
if (payload) {
|
||||
const items = Array.isArray(payload.items) ? payload.items : [];
|
||||
const hasItemProgress = items.some(
|
||||
(it: {
|
||||
type?: string;
|
||||
status?: string;
|
||||
cards?: Array<{ status?: string }>;
|
||||
}) => {
|
||||
if (it.type === "gate") {
|
||||
return it.status === "running" || it.status === "complete";
|
||||
}
|
||||
return (
|
||||
Array.isArray(it.cards) &&
|
||||
it.cards.some(
|
||||
(c) =>
|
||||
c.status === "running" || c.status === "complete",
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
// items[] 走 phase-complete-only 过滤;R0 只有一张卡在跑时
|
||||
// items 是空的,进展信号在 in_flight 字段里——也算进展。
|
||||
const hasInFlight = Boolean(payload.in_flight);
|
||||
if (hasItemProgress || hasInFlight) {
|
||||
setApplied(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore, fall through to runs/latest fallback */
|
||||
}
|
||||
// Pipeline 还没写出 items/in_flight(agent 处于 prep 阶段,比如在 watcher
|
||||
// 里 sleep 等远端文件)时,pipeline 端点会返回 state=pending、items=[],
|
||||
// 但后端 run 其实已经在跑。直接看 run 状态兜底,免得用户起了训练但
|
||||
// monitor 面板等到第一个 phase 才弹。
|
||||
try {
|
||||
const res = await fetch(runUrl, { cache: "no-store" });
|
||||
if (!res.ok) return;
|
||||
const payload = await res.json();
|
||||
if (cancelled || !payload) return;
|
||||
if (payload.status === "running" || payload.status === "queued") {
|
||||
setApplied(true);
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const handler = (e: Event) => {
|
||||
const detail = (e as CustomEvent<SkillToggledDetail>).detail;
|
||||
if (detail.skill !== "model-iteration") return;
|
||||
setApplied(detail.enabled);
|
||||
};
|
||||
tick();
|
||||
const id = window.setInterval(tick, 30000);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearInterval(id);
|
||||
};
|
||||
}, [skill.enabled, applied, activeSessionId, setApplied]);
|
||||
|
||||
// 监听用户消息:skill 启用 + 检测到训练触发短语 → applied = true。
|
||||
useEffect(() => {
|
||||
if (!skill.enabled) return;
|
||||
if (applied) return;
|
||||
if (!Array.isArray(messages) || messages.length === 0) return;
|
||||
// 找最新一条 user 消息
|
||||
let latestUser: (typeof messages)[number] | null = null;
|
||||
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
||||
const m = messages[i];
|
||||
if (m.role === "user") {
|
||||
latestUser = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!latestUser) return;
|
||||
if (lastProcessedUserMsgId.current === latestUser.id) return;
|
||||
lastProcessedUserMsgId.current = latestUser.id;
|
||||
const text = collectUserMessageText(latestUser);
|
||||
if (text && detectApplyIntent(text)) {
|
||||
setApplied(true);
|
||||
}
|
||||
}, [messages, skill.enabled, applied, setApplied]);
|
||||
window.addEventListener(SKILL_TOGGLED_EVENT, handler);
|
||||
return () => window.removeEventListener(SKILL_TOGGLED_EVENT, handler);
|
||||
}, [setApplied]);
|
||||
|
||||
// 注意:用同一棵树 + 条件渲染,保证 <Thread /> 和 <ActivityPanel /> 在
|
||||
// pipeline 切换前后都保持挂载,否则它们会 remount,
|
||||
@@ -503,21 +388,6 @@ function AssistantWorkspace() {
|
||||
);
|
||||
}
|
||||
|
||||
function collectUserMessageText(message: { content?: unknown }): string {
|
||||
const content = message.content;
|
||||
if (typeof content === "string") return content;
|
||||
if (!Array.isArray(content)) return "";
|
||||
const parts: string[] = [];
|
||||
for (const part of content) {
|
||||
if (typeof part === "string") {
|
||||
parts.push(part);
|
||||
} else if (part && typeof part === "object") {
|
||||
const text = (part as { text?: unknown }).text;
|
||||
if (typeof text === "string") parts.push(text);
|
||||
}
|
||||
}
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
function ActivityDrawerTrigger() {
|
||||
const { open, openActivity } = useActivityPanel();
|
||||
|
||||
@@ -93,6 +93,7 @@ import {
|
||||
} from "@/lib/claw-active-session";
|
||||
import { useClawSessionReplay } from "@/lib/claw-session-replay";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { dispatchSkillToggled } from "@/lib/use-training-mode";
|
||||
|
||||
type ComposerInsertEvent = CustomEvent<{ text: string }>;
|
||||
|
||||
@@ -1800,6 +1801,7 @@ function SkillInsertDialog() {
|
||||
);
|
||||
}
|
||||
setSkills(payload);
|
||||
dispatchSkillToggled({ skill: skill.name, enabled });
|
||||
setStatus(enabled ? `已启用 ${skill.name}` : `已停用 ${skill.name}`);
|
||||
} catch (err) {
|
||||
setSkills(previous);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export const SESSION_UPDATED_EVENT = "claw-session-updated";
|
||||
export const SKILL_TOGGLED_EVENT = "claw-skill-toggled";
|
||||
const APPLIED_STORAGE_PREFIX = "claw.training.applied:";
|
||||
|
||||
const MODEL_ITERATION_SKILL_NAMES = new Set(["model-iteration"]);
|
||||
@@ -71,18 +72,10 @@ export function useModelIterationEnabled(): {
|
||||
// - 刷新页面也保持
|
||||
// 不走后端 PATCH 是为了避开 __LOCALID_* 临时 session id 导致 404
|
||||
export function useAppliedTraining(sessionId: string | null) {
|
||||
// 不再从 localStorage 恢复 applied 状态——面板只通过用户本次会话中
|
||||
// 显式点击 skill 按钮打开,避免页面加载时闪烁。
|
||||
const [applied, setApplied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sessionId || typeof window === "undefined") {
|
||||
setApplied(false);
|
||||
return;
|
||||
}
|
||||
const stored =
|
||||
window.localStorage.getItem(APPLIED_STORAGE_PREFIX + sessionId) === "1";
|
||||
setApplied(stored);
|
||||
}, [sessionId]);
|
||||
|
||||
const setAppliedPersist = useCallback(
|
||||
(next: boolean) => {
|
||||
setApplied(next);
|
||||
@@ -166,6 +159,15 @@ export function dispatchSessionUpdated(detail: SessionUpdatedDetail) {
|
||||
);
|
||||
}
|
||||
|
||||
export type SkillToggledDetail = { skill: string; enabled: boolean };
|
||||
|
||||
export function dispatchSkillToggled(detail: SkillToggledDetail) {
|
||||
if (typeof window === "undefined") return;
|
||||
window.dispatchEvent(
|
||||
new CustomEvent<SkillToggledDetail>(SKILL_TOGGLED_EVENT, { detail }),
|
||||
);
|
||||
}
|
||||
|
||||
export function useTrainingMode(sessionId: string | null) {
|
||||
const [isTraining, setIsTraining] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
@@ -73,13 +73,14 @@ when_to_use: |
|
||||
- [ ] **凡是要让用户拍板的检查点(R0 baseline 之后、R{n} regression 之后、>200 候选要定 pattern、Gold drift 复核 等),必须先 append `step:"human-check"` 或 `step:"human-review"` running entry,再用 chat reply 提问**。**禁止只在 chat 里问而不写 gate entry**——UI 看不到拦截 = 视为没做这步,用户面板上看不到任何卡。详见下方 "Human-in-the-Loop 信号 → 写 gate 节点"。
|
||||
|
||||
### Step 4 准入(`augment`,需要修改训练集才进)
|
||||
- [ ] **runDic 必须在当轮 eval `lark_template.json` 落盘后重新 `eval "$(./scripts/resolve_run_ids.sh)"` 取**——禁止复用之前缓存值或手算,eval 期间 resolve 会得到上一轮 workflow id 导致偏移 -1
|
||||
- [ ] **§4.0 原始训练数据清洗(增强前必做)**——按 4 种动作走完逐 pattern 判定
|
||||
- [ ] §4.0.1 Step A:候选定位输出 csv,**不直接改**
|
||||
- [ ] §4.0.1 Step A.5:**先走 label-master 预审**(用推荐标签覆盖 new_label,剔除 label-master 不认同要改的条目)
|
||||
- [ ] §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` 语义),**逐条 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
|
||||
- [ ] **§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`,**不通过必须 = 0**(任何不通过必须修正后重新 review 直到全 pass 才能进 Step 5)
|
||||
|
||||
### Step 4 → Step 5 边界(**NEVER STOP 硬连接**,反复踩坑)
|
||||
- [ ] 写完 `augment=complete` 那一刻,**同一轮 bash 不许结束**:紧接着跑 §4.5 label-master 复核 → 写 `sft=running` → 调 `submit_sft.sh` → 挂 watcher(评测在 SFT `_SUCCESS` 落盘后的下一轮单独用 `submit_cml_eval.sh` 起,不要在 SFT bg 里串接评测)
|
||||
@@ -87,7 +88,7 @@ when_to_use: |
|
||||
- [ ] H2 后台任务回调(`[system] 后台任务 ... exit_code=0`)**不是 turn 结束信号**——它只是 augment 子流程的一个中间节拍,agent 必须在同轮里继续走完 §4.5 → Step 5
|
||||
|
||||
### Step 5 准入(`sft`)
|
||||
- [ ] §4.5 label-master 复核报告 `label_master_review.jsonl` 存在且不通过 ≤ 5%
|
||||
- [ ] §4.5 label-master 复核报告 `label_master_review.jsonl` 存在、行数 = augment 行数(全量不抽样)、不通过 = 0(任何不通过必须修正后重新 review 直到全 pass)
|
||||
- [ ] 旧 sft_output 已 `mv sft_output sft_output_r{prev}` 备份
|
||||
- [ ] 训练参数从 config.yaml 读,model_path = basemodel(**不从上轮 ckpt 续训**)
|
||||
|
||||
|
||||
@@ -755,6 +755,8 @@ with open(out_csv, 'w', encoding='utf-8-sig') as fp:
|
||||
> ```
|
||||
>
|
||||
> 已踩坑:R2 augment 把 `data_clean_<N>/` 写成了 `EVAL_RUNDIC`(多 +1 一次),跳过了 R1.runDic 整段命名空间,前端 augment 卡片直接读不到。**写 augment / SFT / 归档脚本时一律用 `$SFT_RUNDIC`,不要写 `${RUNDIC}` 也不要写 `${EVAL_RUNDIC}`**。
|
||||
>
|
||||
> 🚨 **调用时序(CRITICAL)**:`resolve_run_ids.sh` **只能在当轮 CML eval 的 `lark_template.json` 已落盘之后调用**。禁止在 eval 提交前或 watcher 等待期间预先调用——此时 max workflow 仍是上一轮的值,会导致 SFT_RUNDIC 偏移 -1(已踩坑:R0 workflow17823 评测中提前 resolve 得到 17822,augment 文件全部错位)。正确时序:`cml step=complete` → `resolve_run_ids.sh` → augment/SFT。每次进入 augment/SFT 步骤前**必须重新调用**,不许复用之前缓存的值。
|
||||
|
||||
**Step A.5:label-master 预审(量级判定之前必跑,强制)**
|
||||
|
||||
@@ -1276,9 +1278,8 @@ H1 在 §4.0.1 Step A.5 已经过 label-master 推荐覆盖,这里**不再重
|
||||
|
||||
| 比例 | 处置 |
|
||||
|---|---|
|
||||
| 不通过 ≤ 5% | 自动丢弃这些样本(H2 → 从 augment_<N>.jsonl 删行;H1 → 回滚改动到 .bak)+ 写明丢弃数到 iteration_log |
|
||||
| 不通过 5%~20% | **触发 H-i-T-L #6**(gold 可疑),把不通过样本导出飞书 sheet 让人审 |
|
||||
| 不通过 > 20% | **强制暂停**,Step 4 假设本身有问题,回 Step 3 重新形成假设 |
|
||||
| 不通过 = 0 | ✅ 全量通过,允许进入 Step 5 |
|
||||
| 不通过 > 0 | ❌ **必须修正**(H2 → 修正 augment 样本后重新 review;H1 → 回滚或修正后重新 review),循环直到不通过 = 0 才能进 Step 5。禁止"自动丢弃不通过样本继续"——每条都必须过 |
|
||||
|
||||
**写完复核报告才允许进 Step 5。**verdict 文件不存在 / 没跑层 2 → Step 5 拒绝启动(5.0 的准入检查会查 `label_master_review.jsonl` 存在)。
|
||||
|
||||
@@ -1314,9 +1315,8 @@ H1 在 §4.0.1 Step A.5 已经过 label-master 推荐覆盖,这里**不再重
|
||||
REVIEW="$AUTORESEARCH_CHAT_ROOT/results/data_clean_${RUNDIC}/label_master_review.jsonl"
|
||||
[ -f "$REVIEW" ] || { echo "label-master 复核未完成,回 §4.5"; exit 1; }
|
||||
NOT_PASS=$(grep -c '"verdict":"不通过"' "$REVIEW" || echo 0)
|
||||
TOTAL=$(wc -l < "$REVIEW")
|
||||
if [ "$TOTAL" -gt 0 ] && [ $((NOT_PASS * 100 / TOTAL)) -gt 5 ]; then
|
||||
echo "不通过比例 $NOT_PASS/$TOTAL > 5%,按 §4.5 处置规则走 H-i-T-L 或回 Step 3"
|
||||
if [ "$NOT_PASS" -gt 0 ]; then
|
||||
echo "label-master 不通过 $NOT_PASS 条(要求 = 0),必须修正后重新 review 直到全 pass"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -44,11 +44,11 @@ imageConfig:
|
||||
rm -rf sft_output
|
||||
|
||||
# 1. 组装数据
|
||||
python3 {AUTORESEARCH_ROOT}/prepare_and_train_sft.py prepare \
|
||||
python3 {AUTORESEARCH_ROOT}/scripts/prepare_and_train_sft.py prepare \
|
||||
--output_dir {AUTORESEARCH_ROOT}/sft_data
|
||||
|
||||
# 2. 启动训练(绝对路径,避免相对路径在容器内找不到 zk_trainer)
|
||||
python3 {AUTORESEARCH_ROOT}/prepare_and_train_sft.py train \
|
||||
python3 {AUTORESEARCH_ROOT}/scripts/prepare_and_train_sft.py train \
|
||||
--data_dir {AUTORESEARCH_ROOT}/sft_data \
|
||||
--model_path /mnt/wangsenhao/verl_zk/Qwen3-4B-Instruct-2507 \
|
||||
--model_type qwen3 \
|
||||
|
||||
Reference in New Issue
Block a user