diff --git a/backend/api/server.py b/backend/api/server.py index 0ddf49e..bf357bc 100644 --- a/backend/api/server.py +++ b/backend/api/server.py @@ -2031,6 +2031,9 @@ def create_app(state: AgentState) -> FastAPI: session_id, ) run_active = _is_run_active(state, account_id, session_id) + state_entries = _maybe_inject_gate_fallback( + state_path, state_entries, sessions_dir, session_id, run_active, + ) _apply_program_state( payload, state_entries, @@ -2104,6 +2107,9 @@ def create_app(state: AgentState) -> FastAPI: session_id, ) run_active = _is_run_active(state, account_id, session_id) + state_entries = _maybe_inject_gate_fallback( + state_path, state_entries, sessions_dir, session_id, run_active, + ) _apply_program_state( payload, state_entries, @@ -6606,6 +6612,85 @@ def _compute_in_flight( } +_QUESTION_PATTERN = re.compile( + r'[??]|拍板|选择.*哪|决定|确认.*吗|建议.*哪|要不要|是否|请.*选|你.*决定|三个选项|哪个方案|哪条路' +) + + +def _last_assistant_text(sessions_dir: Path, session_id: str | None) -> str | None: + if not session_id: + return None + safe_id = _safe_session_id(session_id) + if safe_id is None: + return None + path = sessions_dir / safe_id / 'session.json' + try: + data = json.loads(path.read_text(encoding='utf-8')) + except (OSError, json.JSONDecodeError): + return None + messages = data.get('messages') + if not isinstance(messages, list): + return None + for msg in reversed(messages): + if not isinstance(msg, dict): + continue + if msg.get('role') != 'assistant': + continue + content = msg.get('content') + if isinstance(content, str) and content.strip(): + return content.strip() + if isinstance(content, list): + for block in content: + if isinstance(block, dict) and block.get('type') == 'text': + text = block.get('text', '').strip() + if text: + return text + return None + + +def _maybe_inject_gate_fallback( + state_path: Path, + state_entries: list[dict[str, Any]], + sessions_dir: Path, + session_id: str | None, + run_active: bool | None, +) -> list[dict[str, Any]]: + """If agent's last message asks a question but no gate is running, + append a synthetic human-review entry to program-state.jsonl.""" + if run_active is not False: + return state_entries + for entry in reversed(state_entries): + step = entry.get('step') + if step in ('human-check', 'human-review') and entry.get('status') == 'running': + return state_entries + last_text = _last_assistant_text(sessions_dir, session_id) + if not last_text: + return state_entries + if not _QUESTION_PATTERN.search(last_text[-500:]): + return state_entries + run_id = None + for entry in reversed(state_entries): + rid = entry.get('run_id') + if isinstance(rid, str) and rid.strip(): + run_id = rid.strip() + break + gate_entry: dict[str, Any] = { + 'step': 'human-review', + 'status': 'running', + 'run_id': run_id or 'R0', + 'reason': last_text[-300:], + 'ts': datetime_utc_iso(), + '_synthetic': True, + } + try: + with open(state_path, 'a', encoding='utf-8') as f: + f.write(json.dumps(gate_entry, ensure_ascii=False) + '\n') + except OSError: + pass + state_entries.append(gate_entry) + return state_entries + + def _apply_program_state( payload: dict[str, Any], state_entries: list[dict[str, Any]], diff --git a/frontend/app/components/assistant-ui/thread.tsx b/frontend/app/components/assistant-ui/thread.tsx index 7972a4f..93d25be 100644 --- a/frontend/app/components/assistant-ui/thread.tsx +++ b/frontend/app/components/assistant-ui/thread.tsx @@ -1926,6 +1926,9 @@ function SkillInsertDialog() { className="min-w-0 flex-1 text-left" onClick={() => { insertComposerText(`Use the ${skill.name} skill.\n\n`); + if (skill.name === "model-iteration") { + dispatchSkillToggled({ skill: skill.name, enabled: true }); + } setOpen(false); }} > diff --git a/skills/model-iteration/references/program.md b/skills/model-iteration/references/program.md index 18b08b0..c8cb201 100644 --- a/skills/model-iteration/references/program.md +++ b/skills/model-iteration/references/program.md @@ -219,8 +219,10 @@ for k in prev: | 字段 | 含义 | | --- | --- | -| `origin_predict_base` | 旧模型原始输出。具体格式(是否含 `complex=` 前缀、tag 包装、自定义 class 名等)以当前评测产出为准,**分析前 head 一下实物** | -| `origin_predict_dev` | 新模型原始输出。同上,格式以当前产出为准 | +| `origin_predict_base` | 旧模型(baseline)原始输出。具体格式(是否含 `complex=` 前缀、tag 包装、自定义 class 名等)以当前评测产出为准,**分析前 head 一下实物** | +| `origin_predict_dev` | 新模型(迭代模型)原始输出。同上,格式以当前产出为准 | +| `complex_dev` | 迭代模型的 complex 标签(true/false),从 `origin_predict_dev` 解析出的复杂度判定 | +| `complex_base` | baseline 模型的 complex 标签(true/false),从 `origin_predict_base` 解析出的复杂度判定 | | `cleaned_predict_*` | 清洗后输出,用于 GSB 对比 | | `label` / `code_label_base` | ground truth;`label` 为空时回退解析 `code_label_base` | diff --git a/skills/model-iteration/scripts/submit_sft.sh b/skills/model-iteration/scripts/submit_sft.sh index 697669f..561eba4 100755 --- a/skills/model-iteration/scripts/submit_sft.sh +++ b/skills/model-iteration/scripts/submit_sft.sh @@ -19,7 +19,9 @@ SFT_RUNDIC=${1:?usage: $0 [PREV_RUNDIC]} PREV_RUNDIC=${2:-$((SFT_RUNDIC-1))} SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT=${AUTORESEARCH_ROOT:-/mnt/wangsenhao/autoresearch-zk} +# chat-isolated 时代 AUTORESEARCH_ROOT == AUTORESEARCH_CHAT_ROOT;优先用 chat +# root,避免 agent 没 export AUTORESEARCH_ROOT 时回退到全局共享路径(已废弃)。 +ROOT=${AUTORESEARCH_ROOT:-${AUTORESEARCH_CHAT_ROOT:-$(dirname "$SCRIPT_DIR")}} TPL=${SFT_TRAIN_JOB_TEMPLATE:-$SCRIPT_DIR/sft_train_job.yaml.tpl} YAML=/tmp/sft_train_job_r${SFT_RUNDIC}.yaml diff --git a/src/jupyter_runtime.py b/src/jupyter_runtime.py index 1f4697a..831cd98 100644 --- a/src/jupyter_runtime.py +++ b/src/jupyter_runtime.py @@ -297,14 +297,32 @@ class JupyterRuntimeSession: if not self.skills_root: return chat_root = self.chat_workspace_root - src = f'{self.skills_root}/model-iteration/scripts/prepare_and_train_sft.py' - dst = f'{chat_root}/scripts/prepare_and_train_sft.py' - result = self.run_command( - ( + # SKILL/program §5 让 agent 直接 `bash scripts/` 调下面这些脚本, + # 不同步过去 → agent 当 "脚本不存在" 处理后会肉手写 yaml,常把 + # imageCommand 里的 `/scripts/prepare_and_train_sft.py` 写丢前缀, + # 导致训练 pod 报 "No such file or directory"。每次 bind 全量覆盖 + # 一份,跟 prepare_and_train_sft.py 同样 "刷最新版"。 + script_names = ( + 'prepare_and_train_sft.py', + 'submit_sft.sh', + 'sft_train_job.yaml.tpl', + 'submit_cml_eval.sh', + 'resolve_run_ids.sh', + ) + src_dir = f'{self.skills_root}/model-iteration/scripts' + dst_dir = f'{chat_root}/scripts' + copy_cmds = [] + for name in script_names: + src = f'{src_dir}/{name}' + dst = f'{dst_dir}/{name}' + copy_cmds.append( f'if [ -f {shlex.quote(src)} ]; then ' - f'cp {shlex.quote(src)} {shlex.quote(dst)}; ' + f'cp {shlex.quote(src)} {shlex.quote(dst)} && ' + f'chmod +x {shlex.quote(dst)} 2>/dev/null || true; ' f'fi' - ), + ) + result = self.run_command( + ' && '.join(copy_cmds), timeout_seconds=timeout_seconds, max_output_chars=2000, )