feat: add evidence-bound RNA-seq result reports
This commit is contained in:
+136
-2
@@ -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):
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user