feat(api+frontend): D7 落成边亮灯——假设→因子边可见性三处 [vps]
体验稿(8823)逐条过审拍板后动工,不造新机器只做可见性:
件① 三件套(spec §7):
- 1a 卡片已产因子清单:列表项内联 factors(registry hypothesis
血统反查:名/表达式/origin/落成日);单因子名 factorId 前端退役
- 1b 分解批次历史:GET /pipeline/hypotheses/{id}/decompose-jobs
台账新→旧(QA tasks/list 端点形状照抄),卡片折叠区按需拉取
- 1c 轮次进度:run_decompose 每轮 on_progress 回调→内存 job
progress(currentRound/totalRounds/passed/regen;真轮数非 QA 摆设),
台账仍只记起跑+终态;worker 协议改收 job_id
件② 工厂来源列:factors 端点带 origin/hypothesis 血统,
⚡decomposer/✍manual 徽标+回链假设池
件③ 琥珀待办:todos 聚合加 queued/data_check 卡(已确认未分解),
分解转 building 即消行——人工卡点②显性化
件④ 结果持久落卡:前端一次性弹层退役,终态摘要+清单+批次全在卡
测试:后端 TestD7Visibility 六件(历史端点/内联清单/进度中飞可见/
终态不带过程态/琥珀消行/工厂血统)+前端三件套三测;8 目录 2780 绿
This commit is contained in:
@@ -41,9 +41,12 @@ def is_running(hyp_id: str) -> bool:
|
||||
return job_id is not None and _JOBS.get(job_id, {}).get("status") == "running"
|
||||
|
||||
|
||||
def start(hyp_id: str, worker: Callable[[], Awaitable[dict]],
|
||||
def start(hyp_id: str, worker: Callable[[str], Awaitable[dict]],
|
||||
db_path: str) -> dict[str, Any]:
|
||||
"""登记 running 行(内存+台账)并起后台任务;同卡在飞由调用方前置拦(409)."""
|
||||
"""登记 running 行(内存+台账)并起后台任务;同卡在飞由调用方前置拦(409).
|
||||
|
||||
worker 收 job_id(D7 件①1c:体内每轮回调 update_progress 写在飞进度).
|
||||
"""
|
||||
job: dict[str, Any] = {
|
||||
"jobId": f"job-{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
||||
f"-{uuid.uuid4().hex[:8]}",
|
||||
@@ -60,11 +63,22 @@ def start(hyp_id: str, worker: Callable[[], Awaitable[dict]],
|
||||
return dict(job)
|
||||
|
||||
|
||||
async def _run(job_id: str, worker: Callable[[], Awaitable[dict]],
|
||||
def update_progress(job_id: str, *, round_no: int, total_rounds: int,
|
||||
passed: int, regen: int, message: str) -> None:
|
||||
"""在飞轮次进度(D7 件①1c,QA task.progress 同位):只写内存 job 字典,
|
||||
台账仍只记起跑+终态(过程态不落库,与 QA 同纪律)."""
|
||||
job = _JOBS.get(job_id)
|
||||
if job is None or job.get("status") != "running":
|
||||
return
|
||||
job["progress"] = {"currentRound": round_no, "totalRounds": total_rounds,
|
||||
"passed": passed, "regen": regen, "message": message}
|
||||
|
||||
|
||||
async def _run(job_id: str, worker: Callable[[str], Awaitable[dict]],
|
||||
db_path: str) -> None:
|
||||
job = _JOBS[job_id]
|
||||
try:
|
||||
result = await worker()
|
||||
result = await worker(job_id)
|
||||
except LLMError as e:
|
||||
# P3-8 同款不泄露:真因只进服务端日志,job.error 固定文案
|
||||
logger.warning("llm decompose upstream error: %s", e)
|
||||
@@ -79,6 +93,7 @@ async def _run(job_id: str, worker: Callable[[], Awaitable[dict]],
|
||||
else:
|
||||
job.update(status="completed", rounds=result["rounds"],
|
||||
registered=result["registered"], failed=result["failed"])
|
||||
job.pop("progress", None) # 终态不再带过程态
|
||||
job["finishedAt"] = _now()
|
||||
from sanguo_portfolio import pipeline_store
|
||||
pipeline_store.upsert_decompose_job(db_path, job)
|
||||
|
||||
@@ -106,13 +106,22 @@ async def run_decompose(client: LLMClient, card: dict, *,
|
||||
existing_names: set[str],
|
||||
library_exprs: dict[str, str],
|
||||
unwired_domains: tuple[str, ...] = (),
|
||||
max_rounds: int = MAX_ROUNDS) -> dict:
|
||||
max_rounds: int = MAX_ROUNDS,
|
||||
on_progress=None) -> dict:
|
||||
"""反馈循环:初代→逐条硬校验→失败者渲染违规重生成→只重验新一轮.
|
||||
|
||||
名字批次内占有:通过者名字进 claimed 防同批重名(失败者名字释放可重用).
|
||||
on_progress(D7 件①1c):每轮校验完以进程内真轮数回调(通过/重生成计数),
|
||||
QA task.progress 字段结构同位但其 currentRound 是摆设(只初始化无更新)
|
||||
——这里的数据源是循环本身.
|
||||
"""
|
||||
from sanguo_factor.factor_guard import check_candidate
|
||||
|
||||
def _report(msg: str, regen: int) -> None:
|
||||
if on_progress is not None:
|
||||
on_progress(round_no=rounds, total_rounds=max_rounds,
|
||||
passed=len(passed), regen=regen, message=msg)
|
||||
|
||||
unwired = tuple(d for d in unwired_domains if d)
|
||||
passed: list[dict] = []
|
||||
failed: list[dict] = []
|
||||
@@ -138,8 +147,10 @@ async def run_decompose(client: LLMClient, card: dict, *,
|
||||
(newly_failed.append({**cand, "violations": violations})
|
||||
if violations else passed.append(cand))
|
||||
failed = newly_failed
|
||||
_report(f"第 {rounds}/{max_rounds} 轮校验完成", len(failed))
|
||||
if not failed or rounds >= max_rounds:
|
||||
break
|
||||
_report(f"第 {rounds}/{max_rounds} 轮失败者反馈重生成中", len(failed))
|
||||
pending = normalize_candidates(await client.chat_json(
|
||||
build_decompose_messages(card, feedback=failed,
|
||||
existing_names=existing_names),
|
||||
|
||||
@@ -373,6 +373,17 @@ def todos() -> dict:
|
||||
"severity": "info", "updatedAt": now})
|
||||
except Exception:
|
||||
pass
|
||||
# ⑤ 假设卡琥珀待办(D7 件③,人工卡点②显性化):已确认未分解的卡
|
||||
# (queued/data_check=尚未成功过分解;分解成功即转 building 消行)
|
||||
for it in pipeline_store.list_hypotheses(_pipeline_db()):
|
||||
if it["state"] not in ("queued", "data_check"):
|
||||
continue
|
||||
items.append({"id": f"decompose-{it['id']}",
|
||||
"touchpoint": "hypothesis",
|
||||
"title": f"{it['title'][:24]}——已确认未分解",
|
||||
"detail": "人工卡点②:你点「AI 分解」,机器落册孵化因子",
|
||||
"path": "/pipeline/hypotheses", "severity": "warn",
|
||||
"updatedAt": now})
|
||||
order = {"critical": 0, "warn": 1, "info": 2}
|
||||
items.sort(key=lambda i: order.get(i["severity"], 9))
|
||||
return {"items": items}
|
||||
@@ -437,6 +448,9 @@ def factors() -> dict:
|
||||
items.append({"id": name, "category": _factor_category(source),
|
||||
"version": str(versions[-1]["v"]) if versions else "1",
|
||||
"status": e["status"],
|
||||
# D7 件②:origin 身份戳+来源卡回链血统(manual=缺省)
|
||||
"origin": e.get("origin") or "manual",
|
||||
"hypothesis": e.get("hypothesis"),
|
||||
"icRecentT": t, # 最近批(12M 滚动窗)即近窗 t
|
||||
"icFullT": None, # eval_store 每批单窗 t,无全史批数据源,如实 null
|
||||
"promotedAtT": e.get("promotion_t") or None,
|
||||
@@ -881,6 +895,31 @@ def hypotheses_list() -> dict:
|
||||
# 每卡附最新分解 job 视图(job 化后前端唯一轮询源;内存→台账→重启判 failed)
|
||||
for it in items:
|
||||
it["decomposeJob"] = decompose_jobs.view(it["id"], _pipeline_db())
|
||||
# D7 件①1a:已产因子清单(registry hypothesis 血统反查)——卡上单因子名
|
||||
# (首条代表作指针,历史包袱)由此退役;规模小直接内联,膨胀再改按需拉取
|
||||
by_hyp: dict[str, list[dict[str, Any]]] = {}
|
||||
try:
|
||||
from sanguo_factor import version_registry as vr
|
||||
reg = vr.load_registry(_ensure_factor_runtime_registry())
|
||||
for name, e in reg["factors"].items():
|
||||
hyp = e.get("hypothesis")
|
||||
if not hyp:
|
||||
continue
|
||||
versions = e.get("versions") or []
|
||||
expr = ((versions[0].get("params") or {}).get("expression")
|
||||
if versions else None)
|
||||
by_hyp.setdefault(hyp, []).append({
|
||||
"name": name, "expression": expr,
|
||||
"origin": e.get("origin") or "manual",
|
||||
"createdAt": (versions[0].get("effective_from")
|
||||
if versions else None),
|
||||
"status": e.get("status")})
|
||||
except Exception: # noqa: BLE001 registry 读失败不拖垮卡片列表
|
||||
logger.warning("hypotheses factors 反查失败,如实空清单", exc_info=True)
|
||||
for it in items:
|
||||
fs = by_hyp.get(it["id"]) or []
|
||||
fs.sort(key=lambda f: f["createdAt"] or "", reverse=True)
|
||||
it["factors"] = fs
|
||||
return {"items": items}
|
||||
|
||||
|
||||
@@ -970,14 +1009,28 @@ async def hypotheses_decompose(hyp_id: str) -> dict:
|
||||
except LLMConfigError as e:
|
||||
raise HTTPException(503, str(e))
|
||||
|
||||
async def worker() -> dict:
|
||||
return await _decompose_worker(hyp_id, config)
|
||||
async def worker(job_id: str) -> dict:
|
||||
return await _decompose_worker(hyp_id, config, job_id)
|
||||
|
||||
job = decompose_jobs.start(hyp_id, worker, _pipeline_db())
|
||||
return {"job": job}
|
||||
|
||||
|
||||
async def _decompose_worker(hyp_id: str, config) -> dict:
|
||||
@router.get("/pipeline/hypotheses/{hyp_id}/decompose-jobs")
|
||||
def hypotheses_decompose_jobs(hyp_id: str) -> dict:
|
||||
"""该卡分解批次历史(D7 件①1b):台账新→旧,卡片折叠区按需拉取.
|
||||
|
||||
端点形状照抄 QA GET /api/v1/mining/tasks/list(app.py:553-557);
|
||||
内存只有最新一条,历史=台账单一真相(进度过程态不入此列).
|
||||
"""
|
||||
items = pipeline_store.list_hypotheses(_pipeline_db())
|
||||
if not any(i["id"] == hyp_id for i in items):
|
||||
raise HTTPException(404, "卡片不存在")
|
||||
return {"items": pipeline_store.list_decompose_jobs(
|
||||
_pipeline_db(), hyp_id, limit=20)}
|
||||
|
||||
|
||||
async def _decompose_worker(hyp_id: str, config, job_id: str | None = None) -> dict:
|
||||
"""分解后台体:锁内重读卡片→LLM+硬门循环→通过者注册挂血统→卡片转 building.
|
||||
|
||||
零特权:分解产物与手写因子同注册表同求值器,月度批评经动态注册链
|
||||
@@ -1022,7 +1075,9 @@ async def _decompose_worker(hyp_id: str, config) -> dict:
|
||||
|
||||
result = await _run_decompose(
|
||||
LLMClient(config), card, existing_names=existing_names,
|
||||
library_exprs=library_exprs, unwired_domains=unwired)
|
||||
library_exprs=library_exprs, unwired_domains=unwired,
|
||||
on_progress=(lambda **kw: decompose_jobs.update_progress(
|
||||
job_id, **kw)) if job_id else None)
|
||||
|
||||
registered: list[dict[str, Any]] = []
|
||||
if result["passed"]:
|
||||
|
||||
Reference in New Issue
Block a user