fix: preserve gene-family stage intent and completed results

This commit is contained in:
wtx
2026-08-24 09:34:13 +08:00
parent 311a49110f
commit 4a5fdff6f0
10 changed files with 910 additions and 20 deletions
+14 -5
View File
@@ -23500,16 +23500,20 @@ compare: [[flesh, peel]]`;
let stagesDetail = (rawExecutableDetails || []).filter(stage => !completedSet.has(stage.id || stage.name));
if (stages.length === 0) {
const plannedStageIds = (plan.stages || [])
const plannedStageIds = (
(plan.all_stages || []).length > 0
? plan.all_stages
: (plan.stages || [])
)
.map(stage => typeof stage === 'object' ? stage.id : stage)
.filter(Boolean);
const completedOnlyPlan = plannedStageIds.length === 0
&& completedSet.size > 0
&& ['所有阶段已完成', 'All stages completed'].includes(String(plan.description || ''));
const requestedCompletedStage = (plannedStageIds.length > 0
? plannedStageIds.filter(stageId => completedSet.has(stageId))
: Array.from(completedSet)
).pop() || '';
const requestedCompletedStage = plannedStageIds.length === 1
&& completedSet.has(plannedStageIds[0])
? plannedStageIds[0]
: '';
const allPlannedCompleted = (plannedStageIds.length > 0 && plannedStageIds.every(stageId => completedSet.has(stageId)))
|| completedOnlyPlan;
if (allPlannedCompleted && requestedCompletedStage) {
@@ -23520,6 +23524,11 @@ compare: [[flesh, peel]]`;
taskType: 'analysis_completed',
plan: plan
});
} else if (allPlannedCompleted) {
addAgentMessage('assistant', t('当前请求中的分析阶段均已有同一分析范围的有效结果,无需重复执行。'), {
taskType: 'analysis_completed',
plan: plan
});
} else {
addAgentMessage('assistant', t('未生成可执行阶段。未检测到当前 scope 的完成结果时,不再把空计划解释为已完成;请检查阶段名称或前置依赖。'), {
taskType: 'analysis_plan_empty',
+171 -9
View File
@@ -2682,12 +2682,26 @@ def _normalize_full_gene_family_workflow_plan(
stages_detail: list[Dict[str, Any]],
plan_params: Dict[str, Any],
plan_prefix: str,
authorized_stage_ids: Optional[list[str]] = None,
) -> tuple[list[str], list[Dict[str, Any]]]:
if not _looks_like_gene_family_workflow_query(message or ""):
return stage_ids, stages_detail
desired = list(FULL_GENE_FAMILY_WORKFLOW_STAGES)
if "expression" in stage_ids or any(token in (message or "").lower() for token in ["表达", "expression", "rna"]):
authorized = None if not authorized_stage_ids else {
str(stage_id).strip()
for stage_id in authorized_stage_ids
if str(stage_id).strip()
}
desired = [
stage_id
for stage_id in FULL_GENE_FAMILY_WORKFLOW_STAGES
if authorized is None or stage_id in authorized
]
include_expression = (
"expression" in stage_ids
or any(token in (message or "").lower() for token in ["表达", "expression", "rna"])
)
if include_expression and (authorized is None or "expression" in authorized):
desired.append("expression")
shared_refs = _param_list(plan_params.get("referenceSpecies") or plan_params.get("reference_species") or plan_params.get("refSpeciesList"))
@@ -2699,7 +2713,7 @@ def _normalize_full_gene_family_workflow_plan(
plan_params["scopeMode"] = "target_plus_comparative"
ordered: list[str] = []
if "identify" in stage_ids:
if "identify" in stage_ids and (authorized is None or "identify" in authorized):
ordered.append("identify")
for sid in desired:
if sid not in ordered:
@@ -2707,6 +2721,8 @@ def _normalize_full_gene_family_workflow_plan(
for sid in stage_ids:
if sid == "ortholog":
continue
if authorized is not None and sid not in authorized:
continue
if sid and sid not in ordered:
ordered.append(sid)
@@ -3681,13 +3697,38 @@ def _identity_completed_stages_for_plan(
workspace_root=WORKSPACE_ROOT,
)
status = _stage_status_for_plan_identity(job_id, stage_id, identity, strict_identity=True)
workflow_phase = str(
merged_status_params.get("workflowPhase")
or merged_status_params.get("workflow_phase")
or ""
).strip()
requires_confirmed = bool(
merged_status_params.get("requiresConfirmedSubfamily")
or merged_status_params.get("requires_confirmed_subfamily")
)
contextual_kaks_shared_reference = (
stage_id == "kaks"
and workflow_phase == "post_subfamily_confirm"
and requires_confirmed
)
technical_reference_keys = (
"referenceSpecies",
"refSpeciesList",
"ppiReferenceSpecies",
"referenceBundleSpecies",
"ppiKaksReferenceSpecies",
"reference_species",
(
"ppiReferenceSpecies",
"referenceBundleSpecies",
"ppiKaksReferenceSpecies",
"ppi_reference_species",
"reference_bundle_species",
"ppi_kaks_reference_species",
)
if contextual_kaks_shared_reference
else (
"referenceSpecies",
"refSpeciesList",
"ppiReferenceSpecies",
"referenceBundleSpecies",
"ppiKaksReferenceSpecies",
"reference_species",
)
)
explicit_technical_identity = any(
_param_list(merged_status_params.get(key)) for key in technical_reference_keys
@@ -3774,6 +3815,114 @@ def _identity_completed_stages_for_plan(
strict_identity=True,
)
break
if (
status.completed is not True
and stage_id in set(FULL_GENE_FAMILY_WORKFLOW_STAGES) - {"ppi", "kaks"}
and workflow_phase == "post_subfamily_confirm"
and requires_confirmed
):
requested_family = str(
merged_status_params.get("geneFamily")
or merged_status_params.get("gene_family")
or (str(detail_prefix).split(".")[-1] if "." in str(detail_prefix) else "")
).strip()
requested_targets = _param_list(
merged_status_params.get("targetSpecies")
or merged_status_params.get("target_species")
)
expected_route = str(
merged_status_params.get("analysisRoute")
or merged_status_params.get("analysis_route")
or ""
).strip()
plan_references = _param_list(
(plan_params or {}).get("referenceSpecies")
or (plan_params or {}).get("reference_species")
)
final_dir = WORKSPACE_ROOT / job_id / "final"
latest_confirmation_mtime = 0.0
for confirmation_path in final_dir.glob("*.subfamily_confirm.manifest.json") if final_dir.is_dir() else []:
try:
confirmation = json.loads(confirmation_path.read_text(encoding="utf-8"))
except Exception:
continue
if (
confirmation.get("status") == "success"
and str(confirmation.get("prefix") or "").strip() == str(detail_prefix)
):
latest_confirmation_mtime = max(latest_confirmation_mtime, confirmation_path.stat().st_mtime)
candidates = sorted(
final_dir.glob(f"*.{stage_id}.manifest.json") if final_dir.is_dir() else [],
key=lambda path: path.stat().st_mtime,
reverse=True,
)
for manifest_path in candidates:
try:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
except Exception:
continue
stage_context = manifest.get("stage_context") if isinstance(manifest.get("stage_context"), dict) else {}
parameters = manifest.get("parameters") if isinstance(manifest.get("parameters"), dict) else {}
def regular_manifest_value(*keys: str):
for source in (manifest, stage_context, parameters):
for key in keys:
if source.get(key) not in (None, "", []):
return source.get(key)
return ""
manifest_prefix = str(manifest.get("prefix") or manifest.get("output_prefix") or "").strip()
manifest_family = str(
regular_manifest_value("family", "gene_family")
or (manifest_prefix.split(".")[-1] if "." in manifest_prefix else "")
).strip()
manifest_targets = _param_list(regular_manifest_value("target_species", "targetSpecies"))
manifest_references = _param_list(regular_manifest_value("reference_species", "referenceSpecies"))
manifest_route = str(regular_manifest_value("analysis_route", "analysisRoute")).strip()
if (
manifest.get("stage") != stage_id
or manifest.get("status") != "success"
or manifest_prefix != str(detail_prefix)
or manifest_family != requested_family
or set(manifest_targets) != set(requested_targets)
or manifest_route != expected_route
or (
manifest_references
and set(manifest_references) != set(plan_references)
)
or (
latest_confirmation_mtime
and manifest_path.stat().st_mtime < latest_confirmation_mtime
)
):
continue
inherited_params = {
**merged_status_params,
"prefix": detail_prefix,
"targetSpecies": manifest_targets,
"referenceSpecies": manifest_references,
"scopeMode": regular_manifest_value("scope_mode", "scopeMode"),
"analysisRoute": manifest_route,
"memberSetId": regular_manifest_value("member_set_id", "memberSetId"),
}
inherited_params.pop("scopeId", None)
inherited_params.pop("scope_id", None)
inherited_identity = resolve_stage_identity(
stage=stage_id,
prefix=detail_prefix,
params=inherited_params,
job_id=job_id,
workspace_root=WORKSPACE_ROOT,
)
inherited_status = _stage_status_for_plan_identity(
job_id,
stage_id,
inherited_identity,
strict_identity=True,
)
if inherited_status.completed is True:
status = inherited_status
break
if status.completed is True:
if stage_id == "combo":
required_panels = _combo_panels_from_params(merged_status_params)
@@ -19096,6 +19245,7 @@ def _build_contextual_gene_family_plan_response(request: Any, session_id: str, c
stages_detail=stages_detail,
plan_params=plan_params,
plan_prefix=plan_prefix,
authorized_stage_ids=list(request_intent.get("stages") or requested_stage_ids),
)
completed_stages: list[str] = []
@@ -21292,6 +21442,11 @@ async def agent_plan(request: AgentPlanRequest):
"description": "执行完整 GWAS 分析链",
"confidence": 0.98,
})
authoritative_intent_stage_ids = [
str(stage_id).strip()
for stage_id in (incoming_intent.get("stages") or [])
if str(stage_id).strip()
]
incoming_params = dict(incoming_intent.get("params") or {})
def _request_list(*keys):
for key in keys:
@@ -22202,6 +22357,11 @@ async def agent_plan(request: AgentPlanRequest):
if result.get("status") == "completed":
plan = result.get("final_output", {}).get("analysis_plan", {})
intent = result.get("final_output", {}).get("intent", {})
plan_authorized_stage_ids = authoritative_intent_stage_ids or [
str(stage_id).strip()
for stage_id in (intent.get("stages") or [])
if str(stage_id).strip()
]
if authoritative_full_gwas_stages:
gwas_prefix = (
intent.get("prefix")
@@ -22907,6 +23067,7 @@ async def agent_plan(request: AgentPlanRequest):
stages_detail=stages_detail,
plan_params=plan_params,
plan_prefix=plan_prefix or effective_plan_prefix or request.prefix or "",
authorized_stage_ids=plan_authorized_stage_ids,
)
raw_stages = stage_ids
plan["stages"] = stage_ids
@@ -22939,6 +23100,7 @@ async def agent_plan(request: AgentPlanRequest):
stages_detail=stages_detail,
plan_params=plan_params,
plan_prefix=plan_prefix or effective_plan_prefix or request.prefix or "",
authorized_stage_ids=plan_authorized_stage_ids,
)
raw_stages = stage_ids
plan["stages"] = stage_ids
+1
View File
@@ -593,6 +593,7 @@ window.__I18N_EN__ = {
" 亚家族分类已完成预测,但确认表尚未保存;后续会继续显示为待确认。": " subfamily classification predicted; confirmation table not yet saved; will show as pending.",
" 到后端;系统会先生成 ": " to backend; system will generate ",
" 前端连接短暂中断,正在按后端产物状态恢复,不会立即判定阶段失败": " frontend connection interrupted; recovering from backend artifact state. Stage will not be marked as failed.",
"当前请求中的分析阶段均已有同一分析范围的有效结果,无需重复执行。": "All analysis stages in the current request already have valid results for the same analysis scope. No need to re-execute.",
" 已有同一分析范围的有效结果,无需重复执行。": " already has valid results for the same analysis scope. No need to re-execute.",
" 的依赖尚未完成": " dependency not yet completed",
" 重新提交(自动注释背景)...": " re-submitting (auto-annotation background)...",