feat: add evidence-bound RNA-seq result reports

This commit is contained in:
wtx
2026-08-09 01:37:00 +08:00
parent 7399b0bf14
commit 65f79207f0
4 changed files with 3620 additions and 2 deletions
+136 -2
View File
@@ -6911,6 +6911,10 @@ class ExecuteRequest(BaseModel):
speciesCode: str = ""
rnaseqPrefix: str = ""
rnaseqOutputPrefix: str = ""
rnaseqScopeId: str = ""
rnaseqExpectedStages: List[str] = Field(default_factory=list)
rnaseqReportAction: str = "generate"
rnaseqReportUseModel: bool = True
# RNA-seq QC 参数
rnaseqDataMode: str = "upload"
rnaseqDataPath: str = ""
@@ -7794,6 +7798,10 @@ def run_stage_sync(task_id: str, stage: str, job_id: str, params: dict):
wgcna_powers=params.get("wgcnaPowers", "1,2,3,4,5,6,7,8,9,10,12,14,16,18,20"),
candidate_max_deg_proteins=params.get("candidateMaxDegProteins", 500),
candidate_run_interproscan=params.get("candidateRunInterproscan", "auto"),
rnaseq_scope_id=params.get("rnaseqScopeId", ""),
rnaseq_expected_stages=params.get("rnaseqExpectedStages") or params.get("expectedStages") or [],
rnaseq_report_action=params.get("rnaseqReportAction", "generate"),
rnaseq_report_use_model=_bool_param("rnaseqReportUseModel", "report_use_model", default=True),
# PPI参数
ppi_species=params.get("ppiSpecies") or params.get("ppi_species") or "auto",
normalization_species_label=params.get("normalizationSpeciesLabel") or params.get("normalization_species_label") or "",
@@ -12093,6 +12101,10 @@ async def execute_stage(req: ExecuteRequest):
"wgcnaPowers": getattr(req, 'wgcnaPowers', "1,2,3,4,5,6,7,8,9,10,12,14,16,18,20"),
"candidateMaxDegProteins": getattr(req, 'candidateMaxDegProteins', 500),
"candidateRunInterproscan": getattr(req, 'candidateRunInterproscan', "auto"),
"rnaseqScopeId": getattr(req, 'rnaseqScopeId', ""),
"rnaseqExpectedStages": getattr(req, 'rnaseqExpectedStages', []),
"rnaseqReportAction": getattr(req, 'rnaseqReportAction', "generate"),
"rnaseqReportUseModel": getattr(req, 'rnaseqReportUseModel', True),
"ncbiQuery": getattr(req, 'ncbiQuery', ''),
"ncbiGeneIds": getattr(req, 'ncbiGeneIds', ''),
"ncbiMode": getattr(req, 'ncbiMode', 'gene'),
@@ -13679,6 +13691,7 @@ async def get_results(
"rnaseq_fgsea": {"images": [], "files": [], "elapsed_time": 0},
"rnaseq_wgcna": {"images": [], "files": [], "elapsed_time": 0},
"rnaseq_candidate_family": {"images": [], "files": [], "elapsed_time": 0},
"rnaseq_report": {"images": [], "files": [], "elapsed_time": 0},
"genome_prepare": {"images": [], "files": [], "elapsed_time": 0},
"prepare_organize": {"images": [], "files": [], "elapsed_time": 0},
"prepare_normalize_gff": {"images": [], "files": [], "elapsed_time": 0},
@@ -13780,6 +13793,7 @@ async def get_results(
"rnaseq_fgsea",
"rnaseq_wgcna",
"rnaseq_candidate_family",
"rnaseq_report",
"expression",
}
RNASEQ_FILE_TOKENS = (
@@ -14801,6 +14815,7 @@ async def get_results(
"genome_prepare": ["genome_prepare", "species_config"],
"msa_region_plot": ["msa_region", "region.tsv", "conservation_profile"],
"subfamily_confirm": ["subfamily_confirm", "confirmed_subfamily"],
"rnaseq_report": ["rnaseq_report"],
"rnaseq_fgsea": ["rnaseq_fgsea", "fgsea"],
"rnaseq_go_enrich": ["rnaseq_go_enrich", ".go_enrich", "_go_"],
"rnaseq_kegg_enrich": ["rnaseq_kegg_enrich", ".kegg_enrich", "_kegg_", "kegg_pathway", "kegg_pathview"],
@@ -16029,6 +16044,7 @@ def parse_natural_language(message: str) -> dict:
"rnaseq_align": ["rnaseq比对", "rna比对", "hisat2", "比对"],
"rnaseq_quant": ["rnaseq定量", "rna定量", "stringtie", "定量"],
"rnaseq_sample_qc": ["样本质量", "样本检测", "sample qc", "sample quality", "pca", "样本相关性"],
"rnaseq_report": ["rnaseq结果报告", "rna-seq结果报告", "转录组结果报告", "rnaseq报告"],
"rnaseq_wgcna": ["wgcna", "共表达", "加权共表达", "模块性状", "module trait"],
"rnaseq_de": ["rnaseq差异", "rna差异", "差异表达", "de分析"],
"rnaseq_fgsea": ["fgsea", "gsea", "基因集富集", "排序富集"],
@@ -16838,7 +16854,10 @@ async def _run_agent_chat(
)
router_context_terms = dict(terms)
from gfa_core.mas.utils.intent_parser import parse_sequence_extract_request
from gfa_core.mas.utils.intent_parser import (
deterministic_agent_chat_intent,
parse_sequence_extract_request,
)
utility_params = parse_sequence_extract_request(request.message)
if utility_params:
@@ -16884,6 +16903,58 @@ async def _run_agent_chat(
"session_id": session_id,
}
deterministic_intent = deterministic_agent_chat_intent(
request.message,
rnaseq_context=any(
str(stage).startswith("rnaseq_") for stage in completed_stages
),
)
if deterministic_intent is not None:
report_action = str(
(deterministic_intent.get("params") or {}).get("rnaseqReportAction")
or "generate"
)
report_message = (
"已识别为打开 RNA-seq 结果报告。"
if report_action == "open"
else "已识别为生成 RNA-seq 结果报告。"
)
report_intent = {
**deterministic_intent,
"stage": "rnaseq_report",
"primary_stage": "rnaseq_report",
"taskType": "analysis",
"task_kind": "analysis",
"job_id": request.job_id,
"workspace": (
str(PROJECT_ROOT / "workspace" / 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=report_intent,
)
return {
"success": True,
"status": "ok",
"action": "propose_analysis",
"intent": report_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:
@@ -20012,6 +20083,63 @@ async def agent_plan(request: AgentPlanRequest):
request.intent["params"] = dict(request.intent.get("params") or {})
_apply_reconciled_species_to_intent(request.intent, plan_role_resolution)
query_lower_for_context = (request.message or "").lower()
result_report_query = (
"结果报告" in query_lower_for_context
and any(action in query_lower_for_context for action in ["生成", "打开"])
)
if result_report_query:
from gfa_core.rnaseq_evidence import is_rnaseq_evidence_stage, manifest_identity_value
current_intent = dict(request.intent or {})
current_params = dict(current_intent.get("params") or {})
current_stages = [str(stage) for stage in (current_intent.get("stages") or [])]
explicit_rnaseq_report = any(
marker in query_lower_for_context
for marker in ["rnaseq", "rna-seq", "rna seq", "转录组"]
)
rnaseq_context = any(is_rnaseq_evidence_stage(stage) for stage in current_stages)
latest_rnaseq_manifest = None
if request.job_id:
report_final_dir = WORKSPACE_ROOT / request.job_id / "final"
report_prefix = _nonempty_prefix(request.prefix or current_intent.get("prefix") or current_params.get("prefix"))
report_pattern = f"{report_prefix}.*.manifest.json" if report_prefix else "*.manifest.json"
candidates = []
for candidate in report_final_dir.glob(report_pattern) if report_final_dir.exists() else []:
try:
payload = json.loads(candidate.read_text(encoding="utf-8"))
except Exception:
continue
if (
is_rnaseq_evidence_stage(str(payload.get("stage") or ""))
and str(payload.get("status") or "") in {"success", "partial_success"}
):
candidates.append((candidate.stat().st_mtime, candidate, payload))
if candidates:
_mtime, _path, latest_rnaseq_manifest = max(candidates, key=lambda item: item[0])
rnaseq_context = True
if explicit_rnaseq_report or rnaseq_context:
current_params["rnaseqReportAction"] = "open" if "打开" in query_lower_for_context else "generate"
if latest_rnaseq_manifest:
current_params.setdefault(
"rnaseqScopeId",
manifest_identity_value(latest_rnaseq_manifest, "rnaseq_scope_id"),
)
report_prefix = _nonempty_prefix(
request.prefix
or current_intent.get("prefix")
or current_params.get("prefix")
or (latest_rnaseq_manifest or {}).get("prefix")
)
request.intent = {
**current_intent,
"stages": ["rnaseq_report"],
"primary_stage": "rnaseq_report",
"task_type": "report",
"task_kind": "pipeline",
"prefix": report_prefix,
"params": {**current_params, "prefix": report_prefix},
"confidence": 0.99,
}
subfamily_confirm_query_for_context = any(
kw in query_lower_for_context
for kw in ["确认亚家族", "亚家族确认", "确认亚家族标签", "人工确认亚家族", "subfamily confirm", "confirmed subfamily"]
@@ -20907,7 +21035,10 @@ async def agent_plan(request: AgentPlanRequest):
"manifest_path": str(status.manifest_path) if status.manifest_path else "",
"missing_count": len(status.missing_artifacts or []) + len(status.errors or []),
}
if status.completed:
# Report requests are actions, not a request to skip an
# already completed computation. The CLI decides whether
# an "open" request can reuse the fresh snapshot.
if status.completed and stage_id != "rnaseq_report":
completed_rnaseq_stages.append(stage_id)
else:
remaining_rnaseq_stages.append(stage_id)
@@ -20923,6 +21054,7 @@ async def agent_plan(request: AgentPlanRequest):
"rnaseq_fgsea": "RNA-seq fgsea",
"rnaseq_wgcna": "RNA-seq WGCNA",
"rnaseq_candidate_family": "RNA-seq Candidate Family",
"rnaseq_report": "RNA-seq Result Report",
}
plan_prefix = effective_plan_prefix or request.prefix or incoming_intent.get("prefix", "")
final_dir_for_rnaseq = PROJECT_ROOT / "workspace" / request.job_id / "final"
@@ -20942,6 +21074,7 @@ async def agent_plan(request: AgentPlanRequest):
"rnaseq_fgsea": ["rnaseq_de"],
"rnaseq_wgcna": ["rnaseq_sample_qc"],
"rnaseq_candidate_family": ["rnaseq_de", "rnaseq_go_enrich", "rnaseq_kegg_enrich", "rnaseq_fgsea"],
"rnaseq_report": [],
}
if (
"rnaseq_fgsea" in remaining_rnaseq_stages
@@ -20965,6 +21098,7 @@ async def agent_plan(request: AgentPlanRequest):
"rnaseq_fgsea": 10,
"rnaseq_wgcna": 20,
"rnaseq_candidate_family": 10,
"rnaseq_report": 2,
}
stages_detail = []
for order, stage_id in enumerate(remaining_rnaseq_stages, start=1):
+49
View File
@@ -87,6 +87,7 @@ STAGE_KEYWORDS = {
"rnaseq_kegg_enrich": ["rnaseq kegg", "rnaseq_kegg", "rna kegg", "kegg富集", "kegg分析", "kegg", "富集分析", "enrich"],
"rnaseq_enrich": ["富集分析", "enrich", "go分析", "kegg", "clusterprofiler"],
"rnaseq_candidate_family": ["候选家族", "候选基因家族", "家族排名", "tf注释", "tf family", "tffamily", "pfam注释", "deg注释", "差异基因注释", "差异基因家族", "candidate family"],
"rnaseq_report": ["rnaseq结果报告", "rna-seq结果报告", "转录组结果报告", "rnaseq报告", "rna-seq报告"],
"ncbi_fetch_sequences": ["ncbi下载", "ncbi序列", "下载序列", "下载蛋白", "获取蛋白", "下载protein", "下载cds", "protein和cds", "蛋白和cds", "基因list", "gene list", "gene_list", "id列表", ".ids"],
}
@@ -256,6 +257,7 @@ RNASEQ_STAGES = {
"rnaseq_kegg_enrich",
"rnaseq_enrich",
"rnaseq_candidate_family",
"rnaseq_report",
}
# 物种关键词映射
@@ -732,6 +734,10 @@ def parse_intent_from_query(query: str) -> Dict:
"clarify_type": result.get("clarify_type"),
"message": result.get("message")
}
if parsed["primary_stage"] == "rnaseq_report":
parsed["params"] = {
"rnaseqReportAction": "open" if "打开" in query_lower else "generate"
}
if ncbi_params:
parsed["params"] = ncbi_params
parsed["prefix"] = ncbi_params.get("prefix", "")
@@ -781,6 +787,43 @@ def parse_intent_from_query(query: str) -> Dict:
return parsed
def deterministic_agent_chat_intent(
query: str,
*,
rnaseq_context: bool = False,
) -> Optional[Dict]:
"""Return explicit chat intents that must not depend on the Router model."""
intent = parse_intent_from_query(query)
if intent.get("stages") == ["rnaseq_report"]:
return intent
query_lower = str(query or "").lower()
query_without_space = re.sub(r"\s+", "", query_lower)
is_generic_report_action = (
rnaseq_context
and "结果报告" in query_without_space
and any(action in query_without_space for action in ("生成", "打开"))
)
if is_generic_report_action:
return {
"gene_family": "",
"species": "",
"stages": ["rnaseq_report"],
"primary_stage": "rnaseq_report",
"task_type": "report",
"task_kind": "pipeline",
"confidence": 0.99,
"need_clarify": False,
"clarify_type": None,
"message": None,
"params": {
"rnaseqReportAction": (
"open" if "打开" in query_without_space else "generate"
)
},
}
return None
def parse_msa_region_plot_request(query: str) -> Dict:
"""Extract parameters for drawing a selected MSA alignment region."""
text = str(query or "").strip()
@@ -1367,6 +1410,12 @@ def _extract_stages(query: str, query_lower: str) -> List[str]:
stages.append("gwas_fmt")
matched_keywords = [] # 记录匹配到的关键词长度,用于优先级判断
has_rnaseq_marker = any(marker in query_lower for marker in RNASEQ_MARKERS)
requests_result_report = any(
keyword in query_lower
for keyword in ["生成结果报告", "打开结果报告", "结果报告", "生成报告", "打开报告"]
)
if has_rnaseq_marker and requests_result_report:
return ["rnaseq_report"]
extraction_context = any(kw in query_lower for kw in ["提取", "extract", "gffread", "筛选", "subseq"])
explicit_gene_family_context = any(kw in query_lower for kw in ["基因家族", "家族表达", "表达量", "热图", "heatmap"])
curated_member_context = (
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff