feat: add gene family result reports

This commit is contained in:
wtx
2026-08-09 04:32:28 +08:00
parent 65f79207f0
commit cb122fd088
20 changed files with 3676 additions and 20 deletions
+59 -3
View File
@@ -6054,8 +6054,9 @@ def run_stage(
target_species = kwargs.get("target_species")
interspecies_ref_species = kwargs.get("interspecies_ref_species")
scope_file = str(kwargs.get("scope_file") or kwargs.get("scopeFile") or "").strip()
analysis_route = str(kwargs.get("analysis_route") or kwargs.get("analysisRoute") or "").strip()
scope_file = str(kwargs.get("scope_file") or kwargs.get("scopeFile") or "").strip()
scope_id = str(kwargs.get("scope_id") or kwargs.get("scopeId") or "").strip()
analysis_route = str(kwargs.get("analysis_route") or kwargs.get("analysisRoute") or "").strip()
analysis_mode = str(kwargs.get("analysis_mode") or kwargs.get("analysisMode") or "").strip()
member_set_id = str(kwargs.get("member_set_id") or kwargs.get("memberSetId") or "").strip()
combo_output_label = str(combo_output_label or kwargs.get("comboOutputLabel") or kwargs.get("combo_output_label") or "").strip()
@@ -8374,7 +8375,62 @@ def run_stage(
else:
return f"❌ PEP failed (exit code: {code}). Log: {log_file}", manifest
if stage == "summary":
if stage == "gene_family_report":
expected_stages = kwargs.get("expected_stages") or kwargs.get("expectedStages") or []
if isinstance(expected_stages, str):
expected_stages = [value.strip() for value in expected_stages.split(",") if value.strip()]
else:
expected_stages = [str(value).strip() for value in expected_stages if str(value).strip()]
report_action = str(kwargs.get("report_action") or kwargs.get("reportAction") or "generate").strip()
if report_action == "generate" and not expected_stages:
return "❌ 基因家族结果报告生成需要非空 expected_stages", manifest
stage_statuses = kwargs.get("stage_statuses") or kwargs.get("stageStatuses") or {}
if isinstance(stage_statuses, str):
try:
stage_statuses = json.loads(stage_statuses)
except json.JSONDecodeError:
return "❌ 基因家族结果报告的 stage_statuses 不是合法 JSON", manifest
if not isinstance(stage_statuses, dict):
return "❌ 基因家族结果报告的 stage_statuses 必须是对象", manifest
report_use_model_raw = kwargs.get("report_use_model", kwargs.get("reportUseModel", True))
rnaseq_scope_id = str(kwargs.get("rnaseq_scope_id") or kwargs.get("rnaseqScopeId") or "").strip()
cmd = [
sys.executable,
"apps/cli/run_gene_family_report.py",
"--workspace",
str(ws),
"--prefix",
prefix,
"--job-id",
job.job_id,
"--expected-stages",
",".join(expected_stages),
"--stage-statuses-json",
json.dumps(stage_statuses, ensure_ascii=False, separators=(",", ":")),
"--action",
report_action,
]
for flag, value in (
("--scope-id", scope_id),
("--analysis-route", analysis_route),
("--member-set-id", member_set_id),
("--rnaseq-scope-id", rnaseq_scope_id),
):
if value:
cmd += [flag, value]
if not _truthy(report_use_model_raw):
cmd.append("--no-model")
code = run_cmd_logged(cmd, log_file, cwd=Path("."))
report_manifests = sorted(
final_dir.glob(f"{prefix}*.gene_family_report.manifest.json"),
key=lambda path: path.stat().st_mtime,
)
report_manifest = report_manifests[-1] if report_manifests else manifest
if code != 0:
return f"❌ 基因家族结果报告生成失败,日志:{log_file}", report_manifest
return f"✅ 基因家族结果报告已生成。日志:{log_file}", report_manifest
if stage == "summary":
summary_manifest_seed = (
ws / "final" / f"{prefix}.identify.manifest.json"
if (ws / "final" / f"{prefix}.identify.manifest.json").exists()
+73 -3
View File
@@ -7950,6 +7950,7 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓
pop_snp_density: 'SNP密度分析',
rnaseq_de: 'RNA-seq DE',
combo: '组合图',
gene_family_report: '基因家族结果报告',
reference_classification: '参考亚家族分类',
subfamily_confirm: '亚家族标签确认',
ppi_kaks_reference_data: 'PPI/KaKs 参考数据',
@@ -10111,6 +10112,7 @@ compare: [[flesh, peel]]`;
{ name: 'pep', label: '1️⃣2️⃣ PEP', status: 'pending', images: [], files: [], elapsed_time: 0 },
{ name: 'expression', label: '1️⃣3️⃣ Expression (FPKM)', status: 'pending', images: [], files: [], elapsed_time: 0 },
{ name: 'combo', label: '6️⃣ Combo', status: 'pending', images: [], files: [], elapsed_time: 0 },
{ name: 'gene_family_report', label: '📄 基因家族结果报告', status: 'pending', images: [], files: [], elapsed_time: 0 },
// RNA-seq stages
{ name: 'rnaseq_qc', label: '1️⃣5️⃣ RNA-seq QC', status: 'pending', images: [], files: [], elapsed_time: 0 },
{ name: 'rnaseq_align', label: '1️⃣6️⃣ RNA-seq Align', status: 'pending', images: [], files: [], elapsed_time: 0 },
@@ -15080,6 +15082,54 @@ compare: [[flesh, peel]]`;
return `⏸️ 批量执行已暂停:成功 ${completedCount}/${totalCount},暂停 ${pausedCount}/${totalCount}。真实原因:${reasonText}。`;
};
const openGeneratedGeneFamilyReport = async () => {
await refreshResults(jobInfo.job_id, 'gene_family_report');
const reportStage = stages.find(stage => stage.name === 'gene_family_report');
const reportFile = (reportStage?.files || []).find(file =>
file.role === 'gene_family_report' || String(file.name || '').endsWith('.gene_family_report.md')
);
if (!reportFile) return false;
rightPanelTab.value = 'results';
selectFile('gene_family_report', reportFile);
return true;
};
const submitGeneFamilyReportForBatch = async (stages, pausedCount) => {
if (pausedCount > 0) return false;
const plannedIds = (stages || []).map(stage => getStageId(stage)).filter(
stageId => stageId && !['summary', 'gene_family_report', 'subfamily_confirm'].includes(stageId)
);
const geneFamilyStages = new Set([
'identify', 'phylogeny', 'motif', 'domain', 'structure', 'promoter',
'synteny', 'interspecies_synteny', 'karyotype', 'combo', 'ppi', 'expression', 'pep'
]);
if (!plannedIds.some(stageId => geneFamilyStages.has(stageId))) return false;
const stageStatuses = Object.fromEntries(plannedIds.map(stageId => {
const status = stageExecutionStatus.value[stageId];
return [stageId, status === 'completed' ? 'success' : status === 'failed' ? 'failed' : 'missing'];
}));
const batchParams = batchExecuteParams.value || {};
const planParams = batchParams.plan?.params || {};
const stageParams = (stages || []).find(stage => stage && typeof stage === 'object' && stage.params)?.params || {};
try {
await executeSingleStageWithParams('gene_family_report', {
expectedStages: plannedIds,
stageStatuses,
reportAction: 'generate',
reportUseModel: true,
scopeId: batchParams.scopeId || planParams.scopeId || stageParams.scopeId || '',
analysisRoute: batchParams.analysisRoute || planParams.analysisRoute || stageParams.analysisRoute || '',
memberSetId: batchParams.memberSetId || planParams.memberSetId || stageParams.memberSetId || '',
rnaseqScopeId: batchParams.rnaseqScopeId || planParams.rnaseqScopeId || stageParams.rnaseqScopeId || ''
});
addAgentMessage('assistant', '📄 基因家族结果报告已生成。');
return true;
} catch (err) {
addAgentMessage('assistant', `⚠️ 基因家族结果报告生成失败:${err.message || err}`);
return false;
}
};
const confirmAndStartBatchExecute = async (event) => {
if (!event?.isTrusted) return;
batchExecutionUserConfirmed.value = true;
@@ -15195,6 +15245,7 @@ compare: [[flesh, peel]]`;
}).length;
const totalCount = stages.length;
let continuationOpened = false;
if (failedCount > 0) {
addAgentMessage('assistant', `⚠️ 批量执行完成:成功 ${completedCount}/${totalCount},失败 ${failedCount}/${totalCount}`);
} else if (pausedCount > 0) {
@@ -15202,13 +15253,16 @@ compare: [[flesh, peel]]`;
} else {
addAgentMessage('assistant', `🎉 批量执行完成!成功: ${completedCount}/${totalCount}`);
if (isMethodologyDataPreparationContinuationCandidate(stages, completedCount, totalCount, failedCount, pausedCount)) {
await openMethodologyDataPreparationContinuation();
continuationOpened = await openMethodologyDataPreparationContinuation();
} else if (isMethodologyPostConfirmContinuationCandidate(stages, completedCount, totalCount, failedCount, pausedCount)) {
await openMethodologyPostConfirmContinuation();
continuationOpened = await openMethodologyPostConfirmContinuation();
} else if (isContextualPostConfirmContinuationCandidate(stages, completedCount, totalCount, failedCount, pausedCount)) {
await openContextualPostConfirmContinuation();
continuationOpened = await openContextualPostConfirmContinuation();
}
}
if (pausedCount === 0 && !continuationOpened) {
await submitGeneFamilyReportForBatch(stages, pausedCount);
}
}
};
@@ -17496,6 +17550,16 @@ compare: [[flesh, peel]]`;
requestBody.geoId = confirmParams.geoId || '';
requestBody.idTransform = confirmParams.idTransform || 'remove_T01';
}
if (stageName === 'gene_family_report') {
requestBody.expectedStages = activeStageParams.expectedStages || [];
requestBody.stageStatuses = activeStageParams.stageStatuses || {};
requestBody.reportAction = activeStageParams.reportAction || 'generate';
requestBody.reportUseModel = activeStageParams.reportUseModel !== false;
requestBody.scopeId = activeStageParams.scopeId || '';
requestBody.analysisRoute = activeStageParams.analysisRoute || '';
requestBody.memberSetId = activeStageParams.memberSetId || '';
requestBody.rnaseqScopeId = activeStageParams.rnaseqScopeId || '';
}
if (stageName === 'promoter_plot_revise') {
const elements = Array.isArray(confirmParams.promoterElements) ? confirmParams.promoterElements : [];
const categories = Array.isArray(confirmParams.promoterCategories) ? confirmParams.promoterCategories : [];
@@ -17586,6 +17650,9 @@ compare: [[flesh, peel]]`;
} else if (data.async === false) {
// 同步执行完成,直接返回结果
console.log(`[DEBUG-EXEC] ${stageName}: sync completed`);
if (stageName === 'gene_family_report') {
await openGeneratedGeneFamilyReport();
}
return data;
} else {
throw createStructuredExecuteError(data);
@@ -18851,6 +18918,9 @@ compare: [[flesh, peel]]`;
result: data
});
await handleStageCompleted(stageName, data, {});
if (stageName === 'gene_family_report') {
await openGeneratedGeneFamilyReport();
}
// 更新阶段结果 - 如果有分stage的文件,则更新所有stage的文件
if (stageIdx >= 0 && data.result) {
+305 -5
View File
@@ -6876,9 +6876,14 @@ class ExecuteRequest(BaseModel):
domCoverage: float = 30.0
idTransform: str = "none"
scopeFile: str = ""
scopeId: str = ""
scopeMode: str = ""
analysisRoute: str = ""
memberSetId: str = ""
expectedStages: List[str] = []
stageStatuses: Dict[str, str] = {}
reportAction: str = "generate"
reportUseModel: bool = True
contextRoute: str = ""
subfamilyLabelSource: str = ""
requiresConfirmedSubfamily: bool = False
@@ -7816,8 +7821,13 @@ def run_stage_sync(task_id: str, stage: str, job_id: str, params: dict):
# Genome Prepare参数
genome_prepare_type=params.get("genomePrepareType", "auto"),
scope_file=params.get("scopeFile", ""),
scope_id=params.get("scopeId", ""),
analysis_route=params.get("analysisRoute", ""),
member_set_id=params.get("memberSetId", ""),
expected_stages=params.get("expectedStages", []),
stage_statuses=params.get("stageStatuses", {}),
report_action=params.get("reportAction", "generate"),
report_use_model=params.get("reportUseModel", True),
analysis_mode=params.get("analysisMode", ""),
species_queue=params.get("speciesQueue", []),
subfamily_label_source=params.get("subfamilyLabelSource", ""),
@@ -11605,7 +11615,7 @@ async def execute_stage(req: ExecuteRequest):
})
if req.stage == "gwas_fmt":
stage_context["genotype_scope_id"] = trusted_genotype_scope
if req.stage in {"genome_prepare", "genome_prepare_v2"} or req.stage in RNA_SEQ_STAGES:
if req.stage in {"genome_prepare", "genome_prepare_v2", "gene_family_report"} or req.stage in RNA_SEQ_STAGES:
stage_context.pop("scope_id", None)
stage_context.pop("scope_mode", None)
stage_context.pop("analysis_route", None)
@@ -11738,6 +11748,7 @@ async def execute_stage(req: ExecuteRequest):
skip_completed_check = (
is_utility_stage(req.stage)
or is_repair_stage(req.stage)
or req.stage == "gene_family_report"
or force_rerun_requested
or (req.stage == "gwas_fmt" and not trusted_genotype_scope)
or (req.stage == "gwas_mlm" and not gwas_mlm_full_identity)
@@ -11966,9 +11977,15 @@ async def execute_stage(req: ExecuteRequest):
"domCoverage": req.domCoverage,
"idTransform": req.idTransform,
"scopeFile": scope_params.get("scopeFile", ""),
"scopeId": getattr(req, "scopeId", ""),
"rnaseqScopeId": getattr(req, "rnaseqScopeId", ""),
"scopeMode": scope_params.get("scopeMode", ""),
"analysisRoute": stage_context.get("analysis_route") or getattr(req, "analysisRoute", ""),
"memberSetId": stage_context.get("member_set_id") or getattr(req, "memberSetId", ""),
"expectedStages": list(getattr(req, "expectedStages", []) or []),
"stageStatuses": dict(getattr(req, "stageStatuses", {}) or {}),
"reportAction": getattr(req, "reportAction", "generate"),
"reportUseModel": bool(getattr(req, "reportUseModel", True)),
"contextRoute": getattr(req, "contextRoute", ""),
"subfamilyLabelSource": getattr(req, "subfamilyLabelSource", ""),
"requiresConfirmedSubfamily": bool(getattr(req, "requiresConfirmedSubfamily", False)),
@@ -12728,6 +12745,69 @@ def _confirmed_execute_request(
return ExecuteRequest(**params)
def _confirmed_plan_identity(ordered: List[ConfirmedPlanStage]) -> Dict[str, str]:
identity: Dict[str, str] = {}
for stage in reversed(ordered):
params = stage.params or {}
for target, keys in {
"scopeId": ("scopeId", "scope_id"),
"analysisRoute": ("analysisRoute", "analysis_route"),
"memberSetId": ("memberSetId", "member_set_id"),
"rnaseqScopeId": ("rnaseqScopeId", "rnaseq_scope_id"),
}.items():
if identity.get(target):
continue
identity[target] = next((str(params.get(key) or "").strip() for key in keys if params.get(key)), "")
return identity
def _confirmed_plan_report_request(
request: ConfirmedPlanExecutionRequest,
ordered: List[ConfirmedPlanStage],
status: Dict[str, Any],
) -> Optional[ExecuteRequest]:
"""Build the one formal-report submission for a terminal confirmed plan."""
terminal = str(status.get("status") or "")
if terminal not in {"success", "failed"}:
return None
planned = [stage.id for stage in ordered if stage.id not in {"summary", "gene_family_report", "subfamily_confirm"}]
if not planned:
return None
completed = set(status.get("completed_stages") or [])
failed_stage = str((status.get("failure") or {}).get("stage") or "")
stage_statuses = {
stage: "success" if stage in completed else "failed" if stage == failed_stage else "not_run"
for stage in planned
}
if terminal == "success":
stage_statuses = {stage: "success" for stage in planned}
return ExecuteRequest(
stage="gene_family_report",
session_id=request.session_id,
job_id=request.job_id,
prefix=request.prefix,
expectedStages=planned,
stageStatuses=stage_statuses,
reportAction="generate",
reportUseModel=True,
**_confirmed_plan_identity(ordered),
)
async def _submit_confirmed_plan_report(
request: ConfirmedPlanExecutionRequest,
ordered: List[ConfirmedPlanStage],
status: Dict[str, Any],
) -> None:
report_request = _confirmed_plan_report_request(request, ordered, status)
if report_request is None:
return
try:
status["report_submission"] = await execute_stage(report_request)
except Exception as exc:
status["report_submission"] = {"success": False, "error_type": type(exc).__name__}
async def _wait_for_submitted_stage(
task_id: str,
stage_id: str,
@@ -12777,6 +12857,8 @@ async def _run_confirmed_plan_execution(
"run_id": run_id,
"plan_id": request.plan_id,
"job_id": request.job_id,
"prefix": request.prefix,
**_confirmed_plan_identity(ordered),
"status": "running",
"stages": [stage.id for stage in ordered],
"completed_stages": [],
@@ -12820,9 +12902,11 @@ async def _run_confirmed_plan_execution(
},
}
)
await _submit_confirmed_plan_report(request, ordered, status)
_write_confirmed_plan_status(status_path, status)
return
status.update({"status": "success", "current_stage": ""})
await _submit_confirmed_plan_report(request, ordered, status)
_write_confirmed_plan_status(status_path, status)
@@ -12850,6 +12934,8 @@ async def agent_execute_confirmed(request: ConfirmedPlanExecutionRequest):
"run_id": run_id,
"plan_id": request.plan_id,
"job_id": request.job_id,
"prefix": request.prefix,
**_confirmed_plan_identity(_confirmed_plan_stages(request)),
"status": "queued",
"stages": [stage.id for stage in _confirmed_plan_stages(request)],
}
@@ -16759,6 +16845,93 @@ def _answer_methodology_sandbox_qa(message: str) -> dict[str, Any]:
return payload
def _gene_family_report_plan_context(job_id: str, prefix: str = "") -> Optional[Dict[str, Any]]:
"""Recover only a user-confirmed or previously reported stage plan."""
safe_job_id = str(job_id or "").strip()
requested_prefix = str(prefix or "").strip()
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}", safe_job_id):
return None
final_dir = WORKSPACE_ROOT / safe_job_id / "final"
excluded = {"summary", "gene_family_report", "subfamily_confirm"}
confirmed: list[tuple[float, Dict[str, Any]]] = []
confirmed_dir = final_dir / ".confirmed_plans"
for path in confirmed_dir.glob("*.json") if confirmed_dir.is_dir() else ():
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
continue
terminal = str(payload.get("status") or "")
plan_prefix = str(payload.get("prefix") or "").strip()
if (
terminal not in {"success", "failed"}
or str(payload.get("job_id") or "") != safe_job_id
or not plan_prefix
or (requested_prefix and plan_prefix != requested_prefix)
):
continue
planned = [
str(stage).strip()
for stage in (payload.get("stages") or [])
if str(stage).strip() and str(stage).strip() not in excluded
]
if not planned:
continue
completed = {str(stage) for stage in (payload.get("completed_stages") or [])}
failed_stage = str((payload.get("failure") or {}).get("stage") or "")
statuses = {
stage: "success" if terminal == "success" or stage in completed else "failed" if stage == failed_stage else "not_run"
for stage in planned
}
context = {
"prefix": plan_prefix,
"expectedStages": planned,
"stageStatuses": statuses,
"scopeId": str(payload.get("scopeId") or payload.get("scope_id") or ""),
"analysisRoute": str(payload.get("analysisRoute") or payload.get("analysis_route") or ""),
"memberSetId": str(payload.get("memberSetId") or payload.get("member_set_id") or ""),
"rnaseqScopeId": str(payload.get("rnaseqScopeId") or payload.get("rnaseq_scope_id") or ""),
}
confirmed.append((path.stat().st_mtime, context))
if confirmed:
return max(confirmed, key=lambda item: item[0])[1]
pattern = f"{requested_prefix}.gene_family_report.manifest.json" if requested_prefix else "*.gene_family_report.manifest.json"
reports: list[tuple[float, Dict[str, Any]]] = []
for path in final_dir.glob(pattern) if final_dir.is_dir() else ():
try:
manifest = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
continue
report_prefix = str(manifest.get("prefix") or "").strip()
parameters = manifest.get("parameters") or {}
planned = [
str(stage).strip()
for stage in (parameters.get("expected_stages") or [])
if str(stage).strip() and str(stage).strip() not in excluded
]
if (
manifest.get("stage") != "gene_family_report"
or manifest.get("status") != "success"
or not report_prefix
or (requested_prefix and report_prefix != requested_prefix)
or not planned
):
continue
recorded_statuses = parameters.get("stage_statuses") or {}
context = {
"prefix": report_prefix,
"expectedStages": planned,
"stageStatuses": {stage: str(recorded_statuses.get(stage) or "missing") for stage in planned},
"scopeId": "",
"analysisRoute": "",
"memberSetId": "",
"rnaseqScopeId": "",
}
reports.append((path.stat().st_mtime, context))
return max(reports, key=lambda item: item[0])[1] if reports else None
async def _run_agent_chat(
request: AgentChatRequest,
progress_callback=None,
@@ -16857,6 +17030,7 @@ async def _run_agent_chat(
from gfa_core.mas.utils.intent_parser import (
deterministic_agent_chat_intent,
parse_sequence_extract_request,
resolve_gene_family_report_intent,
)
utility_params = parse_sequence_extract_request(request.message)
@@ -16903,11 +17077,10 @@ async def _run_agent_chat(
"session_id": session_id,
}
deterministic_intent = deterministic_agent_chat_intent(
compact_message = re.sub(r"\s+", "", str(request.message or "").lower())
deterministic_intent = None if "基因家族" in compact_message else deterministic_agent_chat_intent(
request.message,
rnaseq_context=any(
str(stage).startswith("rnaseq_") for stage in completed_stages
),
rnaseq_context=any(str(stage).startswith("rnaseq_") for stage in completed_stages),
)
if deterministic_intent is not None:
report_action = str(
@@ -16955,6 +17128,76 @@ async def _run_agent_chat(
"handoff_request": request.message,
}
gene_report_intent = resolve_gene_family_report_intent(request.message)
if gene_report_intent is not None:
plan_context = _gene_family_report_plan_context(
request.job_id,
str(router_context_terms.get("prefix") or ""),
)
report_action = str((gene_report_intent.get("params") or {}).get("reportAction") or "generate")
if plan_context is None:
report_message = "已识别为基因家族结果报告请求,但当前 job 缺少可验证的分析计划,无法生成或打开正式报告。"
if conversation_turn is not None:
active_store.finish_turn(
conversation_turn,
assistant_message=report_message,
interaction_kind="independent",
action="reply",
intent=None,
)
return {
"success": False,
"status": "report_plan_missing",
"action": None,
"intent": None,
"message": report_message,
"sources": [],
"tool_trace": [],
"mode": "analysis",
"completed_stages": completed_stages,
"model_used": "rule",
"session_id": session_id,
}
report_message = (
"已识别为打开基因家族结果报告。"
if report_action == "open"
else "已识别为生成基因家族结果报告。"
)
report_params = {**(gene_report_intent.get("params") or {}), **plan_context}
resolved_intent = {
**gene_report_intent,
"stage": "gene_family_report",
"primary_stage": "gene_family_report",
"taskType": "analysis",
"task_kind": "report",
"prefix": plan_context["prefix"],
"params": report_params,
"job_id": request.job_id,
"workspace": str(WORKSPACE_ROOT / request.job_id) if request.job_id else "",
"message": report_message,
}
if conversation_turn is not None:
active_store.finish_turn(
conversation_turn,
assistant_message=report_message,
interaction_kind="independent",
action="propose_analysis",
intent=resolved_intent,
)
return {
"success": True,
"status": "ok",
"action": "propose_analysis",
"intent": resolved_intent,
"sources": [],
"tool_trace": [],
"mode": "analysis",
"completed_stages": completed_stages,
"model_used": "rule",
"session_id": session_id,
"handoff_request": request.message,
}
global llm_manager
active_llm_manager = llm_manager
if active_llm_manager is None:
@@ -20060,6 +20303,63 @@ async def agent_plan(request: AgentPlanRequest):
extension_intent = _match_extension_stage_intent(request.message, request.job_id or "")
if extension_intent:
return _extension_stage_plan_response(extension_intent, session_id, request.job_id or "")
report_intent = dict(request.intent or {})
if report_intent.get("stages") == ["gene_family_report"]:
report_params = dict(report_intent.get("params") or {})
report_prefix = str(
report_intent.get("prefix")
or report_params.get("prefix")
or request.prefix
or ""
).strip()
if not report_params.get("expectedStages"):
recovered = _gene_family_report_plan_context(request.job_id, report_prefix)
if recovered:
report_params.update(recovered)
report_prefix = recovered["prefix"]
if not report_params.get("expectedStages"):
return {
"success": False,
"mode": "gene_family_report_deterministic",
"status": "report_plan_missing",
"message": "当前 job 缺少可验证的基因家族分析计划,无法生成或打开正式报告。",
"session_id": session_id,
}
report_params["prefix"] = report_prefix
report_intent.update(
{
"stage": "gene_family_report",
"primary_stage": "gene_family_report",
"stages": ["gene_family_report"],
"prefix": report_prefix,
"params": report_params,
}
)
detail = _stage_detail_defaults("gene_family_report")
detail.update({"order": 1, "dependencies": [], "params": dict(report_params)})
return {
"success": True,
"mode": "gene_family_report_deterministic",
"plan": {
"analysis_type": "gene_family",
"prefix": report_prefix,
"description": "生成或打开基因家族结果报告",
"estimated_time": "2分钟",
"estimated_total_time": "2分钟",
"execution_strategy": "sequential",
"max_parallel": 1,
"total_stages": 1,
"stages": ["gene_family_report"],
"stages_detail": [detail],
"all_stages": ["gene_family_report"],
"all_stages_detail": [detail],
"parallel_groups": [],
"params": report_params,
},
"intent": report_intent,
"completed_stages": [],
"session_id": session_id,
}
plan_role_resolution = (
_reconcile_confirmed_gene_family_species(
request.job_id,