Merge branch 'master' of http://192.168.0.175:3000/admin/PlantAI-dev
This commit is contained in:
@@ -700,11 +700,14 @@ def build_report_data(
|
||||
),
|
||||
]
|
||||
sections.extend(stage_sections)
|
||||
# The integrated conclusion has a fixed evidence contract. Do not take the
|
||||
# first arbitrary observations because plan ordering can displace one of
|
||||
# the three core facts requested for delivery.
|
||||
integrated_observations = [
|
||||
section.observations[0]
|
||||
for section in stage_sections
|
||||
if section.observations
|
||||
][:3]
|
||||
stage_sections_by_name[stage].observations[0]
|
||||
for stage in ("identify", "phylogeny", "reference_classification")
|
||||
if stage in stage_sections_by_name and stage_sections_by_name[stage].observations
|
||||
]
|
||||
sections.extend(
|
||||
[
|
||||
ReportSection(
|
||||
|
||||
@@ -299,6 +299,7 @@ class BoundRecognizeAnalysisIntentArgs(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
query: str
|
||||
prefix: str = ""
|
||||
language: str = "zh"
|
||||
_prefix_is_literal = field_validator("prefix")(_validate_prefix)
|
||||
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ STAGE_SCRIPT_MAP = {
|
||||
"annotation_kegg": "run_annotation.py",
|
||||
"rnaseq_wgcna": "run_rnaseq_wgcna.py",
|
||||
"rnaseq_candidate_family": "run_rnaseq_candidate_family.py",
|
||||
"rnaseq_report": "run_rnaseq_report.py",
|
||||
"rnaseq_sample_qc": "run_rnaseq_sample_qc.py",
|
||||
"genome_prepare": "run_genome_prepare.py",
|
||||
"genome_prepare_v2": "run_genome_prepare.py",
|
||||
@@ -341,6 +342,11 @@ DIAGNOSIS_STAGE_COVERAGE = {
|
||||
"postflight": ["candidate_family_ranking", "evidence_matrix", "recommendation", "rag_queries", "rag_references"],
|
||||
"failure": ["annotation_missing", "id_mapping"],
|
||||
},
|
||||
"rnaseq_report": {
|
||||
"preflight": ["successful_rnaseq_manifest", "prefix_scope_identity"],
|
||||
"postflight": ["report_markdown", "evidence_snapshot", "coverage_status"],
|
||||
"failure": ["no_successful_rnaseq_evidence", "scope_mismatch", "artifact_missing"],
|
||||
},
|
||||
"annotation": {
|
||||
"preflight": ["protein_background", "tool_availability"],
|
||||
"postflight": ["gene2go", "gene2ko", "annotation_coverage"],
|
||||
|
||||
@@ -74,6 +74,7 @@ class IntentAgentV2(BaseAgent):
|
||||
"rnaseq_de": "RNA-seq差异表达分析 - 从原始FASTQ数据开始,进行质控→比对→定量→差异表达分析(DESeq2)",
|
||||
"rnaseq_enrich": "RNA-seq富集分析 - GO/KEGG通路富集分析(RNA-seq流程最后一步)",
|
||||
"rnaseq_candidate_family": "RNA-seq候选家族筛选 - 整合DEG、InterPro/Pfam和GO/KEGG富集,输出候选基因家族排名",
|
||||
"rnaseq_report": "RNA-seq结果报告 - 从当前成功阶段生成可追溯的正式Markdown报告",
|
||||
"enrichment": "富集分析 - 基于基因家族鉴定结果进行GO/KEGG通路富集分析",
|
||||
"ncbi_fetch_sequences": "NCBI序列下载 - 根据GeneID、基因名或基因list下载protein/CDS序列"
|
||||
}
|
||||
@@ -521,6 +522,22 @@ class IntentAgentV2(BaseAgent):
|
||||
has_rnaseq_marker = any(kw in query_lower for kw in ["rnaseq", "rna-seq", "rna seq", "转录组"])
|
||||
explicit_gene_family_context = any(kw in query_lower for kw in ["基因家族", "家族表达", "表达量", "热图", "heatmap"])
|
||||
|
||||
if has_rnaseq_marker and any(
|
||||
keyword in query_lower
|
||||
for keyword in ["生成结果报告", "打开结果报告", "结果报告", "生成报告", "打开报告"]
|
||||
):
|
||||
action = "open" if "打开" in query_lower else "generate"
|
||||
return {
|
||||
"gene_family": "",
|
||||
"species": "",
|
||||
"stages": ["rnaseq_report"],
|
||||
"primary_stage": "rnaseq_report",
|
||||
"description": "打开或生成当前RNA-seq正式结果报告",
|
||||
"requirements": ["至少一个成功的RNA-seq阶段manifest"],
|
||||
"params": {"rnaseqReportAction": action},
|
||||
"confidence": 0.99,
|
||||
}
|
||||
|
||||
stage_force_rules = [
|
||||
("rnaseq_candidate_family", ["候选家族", "候选基因家族", "家族排名", "tf注释", "tf family", "tffamily", "pfam注释", "deg注释", "差异基因注释"], "执行RNA-seq候选家族筛选"),
|
||||
("rnaseq_fgsea", ["fgsea", "gsea", "排序富集", "基因集富集"], "执行RNA-seq fgsea排序富集分析"),
|
||||
|
||||
@@ -44,7 +44,7 @@ from gfa_core.wgs_upstream_manifest import (
|
||||
)
|
||||
|
||||
|
||||
GF_TARGET_ONLY_STAGE_PARAMS = {"promoter", "synteny", "karyotype", "pep", "tfbs"}
|
||||
GF_TARGET_ONLY_STAGE_PARAMS = {"promoter", "synteny", "karyotype", "pep", "tfbs", "ppi"}
|
||||
GF_REFERENCE_STAGE_PARAMS = {
|
||||
"phylogeny",
|
||||
"motif",
|
||||
@@ -54,7 +54,6 @@ GF_REFERENCE_STAGE_PARAMS = {
|
||||
"interspecies_synteny",
|
||||
"ortholog",
|
||||
"kaks",
|
||||
"ppi",
|
||||
}
|
||||
GF_SCOPE_IDENTITY_KEYS = (
|
||||
"scopeFile",
|
||||
@@ -377,6 +376,7 @@ class PlanningAgentV2(BaseAgent):
|
||||
"rnaseq_fgsea": [],
|
||||
"rnaseq_wgcna": [],
|
||||
"rnaseq_candidate_family": [],
|
||||
"rnaseq_report": [],
|
||||
}
|
||||
|
||||
# 各阶段依赖 (来自PreFlight)
|
||||
@@ -1958,17 +1958,6 @@ class PlanningAgentV2(BaseAgent):
|
||||
recommended_params = learning_recommendations.get("params", {})
|
||||
print(f"[PlanningAgentV2] 应用Learning推荐参数: {recommended_params}")
|
||||
|
||||
if "ppi" in stage_ids:
|
||||
intent_params = dict(intent_params or {})
|
||||
ppi_species = str(intent_params.get("ppiSpecies") or intent_params.get("ppi_species") or "").strip().lower()
|
||||
if ppi_species and ppi_species != "auto" and not intent_params.get("referenceSpecies"):
|
||||
ppi_ref_species = {"ath": ["At"], "osa": ["Os"], "zma": ["Zm"], "gma": ["Gm"], "sly": ["Sl"]}.get(ppi_species, [])
|
||||
if ppi_ref_species:
|
||||
intent_params["referenceSpecies"] = ppi_ref_species
|
||||
intent_params["reference_species"] = ppi_ref_species
|
||||
intent_params["refSpeciesList"] = ppi_ref_species
|
||||
intent_params.setdefault("scopeMode", "target_plus_reference")
|
||||
|
||||
for i, stage_id in enumerate(stage_ids):
|
||||
info = self.STAGE_INFO.get(stage_id, {"name": stage_id, "estimated_time": 10, "cpu": 8})
|
||||
deps = self.DEPENDENCIES.get(stage_id, [])
|
||||
|
||||
@@ -620,6 +620,9 @@ class RouterRuntime:
|
||||
}
|
||||
if not all(proposal.values()):
|
||||
return None
|
||||
original_request = str(raw.get("original_request") or "").strip()
|
||||
if original_request:
|
||||
proposal["original_request"] = original_request
|
||||
workflow_id = str(raw.get("workflow_id") or "").strip()
|
||||
if workflow_id in {
|
||||
"data_preparation",
|
||||
@@ -705,6 +708,7 @@ class RouterRuntime:
|
||||
session_id: str,
|
||||
job_id: str,
|
||||
query: str,
|
||||
original_query: str,
|
||||
subject: str,
|
||||
workflow_id: WorkflowId | None = None,
|
||||
) -> dict[str, str]:
|
||||
@@ -718,6 +722,7 @@ class RouterRuntime:
|
||||
proposal = {
|
||||
"proposal_id": f"proposal-{digest}",
|
||||
"request": request,
|
||||
"original_request": str(original_query or "").strip() or request,
|
||||
"subject": normalized_subject,
|
||||
}
|
||||
if workflow_id is not None:
|
||||
@@ -1400,8 +1405,8 @@ class RouterRuntime:
|
||||
tool_trace=decision_trace,
|
||||
interaction_kind=decision.interaction_kind,
|
||||
)
|
||||
if decision.goal == "new_analysis":
|
||||
effective_query = self.tools.resolve_analysis_handoff_request(
|
||||
if decision.goal == "new_analysis" and confirmed_proposal is None:
|
||||
proposal_query = self.tools.resolve_analysis_handoff_request(
|
||||
effective_query,
|
||||
workflow_id=decision.workflow_id,
|
||||
guidance=decision.analysis_request_kind == "guidance",
|
||||
@@ -1431,6 +1436,7 @@ class RouterRuntime:
|
||||
session_id=session_id,
|
||||
job_id=job_id,
|
||||
query=proposal_query,
|
||||
original_query=query,
|
||||
subject=decision.subject,
|
||||
workflow_id=decision.workflow_id,
|
||||
)
|
||||
|
||||
@@ -21,6 +21,35 @@ def _is_beneath(path: Path, parent: Path) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _resolve_job_layout(root: Path, job_id: str) -> tuple[Path, Path, Path]:
|
||||
logical_input_job = root / "input" / job_id
|
||||
logical_workspace = root / "workspace" / job_id
|
||||
input_root = (root / "input").resolve(strict=False)
|
||||
workspace_root = (root / "workspace").resolve(strict=False)
|
||||
input_job = logical_input_job.resolve(strict=False)
|
||||
job_workspace = logical_workspace.resolve(strict=False)
|
||||
if _is_beneath(input_job, input_root) and _is_beneath(
|
||||
job_workspace,
|
||||
workspace_root,
|
||||
):
|
||||
return root, input_job, job_workspace
|
||||
|
||||
if not (logical_input_job.is_symlink() and logical_workspace.is_symlink()):
|
||||
if not _is_beneath(input_job, input_root):
|
||||
raise ValueError("input job path escapes project root")
|
||||
raise ValueError("workspace job path escapes project root")
|
||||
|
||||
input_job = logical_input_job.resolve(strict=True)
|
||||
job_workspace = logical_workspace.resolve(strict=True)
|
||||
linked_root = input_job.parents[1]
|
||||
if (
|
||||
input_job != linked_root / "input" / job_id
|
||||
or job_workspace != linked_root / "workspace" / job_id
|
||||
):
|
||||
raise ValueError("linked job paths do not share one project root")
|
||||
return linked_root, input_job, job_workspace
|
||||
|
||||
|
||||
def _read_manifest(path: Path) -> dict[str, Any]:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
if type(payload) is not dict:
|
||||
@@ -263,12 +292,7 @@ def build_confirmation_snapshot(
|
||||
if not isinstance(job_id, str) or not _JOB_ID.fullmatch(job_id):
|
||||
raise ValueError("invalid job_id")
|
||||
root = Path(project_root).expanduser().resolve(strict=True)
|
||||
input_job = (root / "input" / job_id).resolve(strict=False)
|
||||
job_workspace = (root / "workspace" / job_id).resolve(strict=False)
|
||||
if not _is_beneath(input_job, (root / "input").resolve(strict=False)):
|
||||
raise ValueError("input job path escapes project root")
|
||||
if not _is_beneath(job_workspace, (root / "workspace").resolve(strict=False)):
|
||||
raise ValueError("workspace job path escapes project root")
|
||||
root, input_job, job_workspace = _resolve_job_layout(root, job_id)
|
||||
final_dir = job_workspace / "final"
|
||||
|
||||
result: dict[str, Any] = {
|
||||
|
||||
@@ -20,6 +20,7 @@ ANALYSIS_KEYWORDS = {
|
||||
"rnaseq_kegg_enrich": ["kegg富集", "kegg分析", "kegg", "pathway"],
|
||||
"rnaseq_fgsea": ["fgsea", "gsea", "排序富集", "基因集富集"],
|
||||
"rnaseq_candidate_family": ["候选家族", "候选基因家族", "家族排名", "tf注释", "tf family", "tffamily", "pfam注释", "deg注释", "差异基因注释"],
|
||||
"rnaseq_report": ["rnaseq结果报告", "rna-seq结果报告", "转录组结果报告", "rnaseq报告", "rna-seq报告"],
|
||||
"rnaseq_enrich": ["富集分析", "功能富集"],
|
||||
"identify": ["鉴定", "identify", "识别", "找出", "鉴定基因家族"],
|
||||
"identify_refine": ["修订成员", "修订鉴定成员", "修改成员", "删除基因", "删除", "去除基因", "去掉基因", "移除基因", "补充基因", "遗漏基因", "重建蛋白", "人工修订id", "curated members", "refine identify"],
|
||||
@@ -61,6 +62,7 @@ RNASEQ_STAGES = {
|
||||
"rnaseq_kegg_enrich",
|
||||
"rnaseq_fgsea",
|
||||
"rnaseq_candidate_family",
|
||||
"rnaseq_report",
|
||||
"rnaseq_enrich",
|
||||
}
|
||||
|
||||
@@ -179,6 +181,21 @@ def fast_intent_recognition(query: str) -> Optional[Dict[str, Any]]:
|
||||
except Exception:
|
||||
pass
|
||||
has_rnaseq_marker = any(marker in query_lower for marker in RNASEQ_MARKERS)
|
||||
if has_rnaseq_marker and any(
|
||||
keyword in query_lower
|
||||
for keyword in ["生成结果报告", "打开结果报告", "结果报告", "生成报告", "打开报告"]
|
||||
):
|
||||
action = "open" if "打开" in query_lower else "generate"
|
||||
return {
|
||||
"stage": "rnaseq_report",
|
||||
"stages": ["rnaseq_report"],
|
||||
"gene_family": "",
|
||||
"species": "",
|
||||
"prefix": "",
|
||||
"params": {"rnaseqReportAction": action},
|
||||
"confidence": 0.99,
|
||||
"method": "rnaseq_report_deterministic",
|
||||
}
|
||||
header_clean_context = any(kw in query_lower for kw in [
|
||||
"id处理", "处理id", "修订id", "简化id", "清理id", "id简化",
|
||||
"去掉注释", "去除注释", "注释信息", "长注释",
|
||||
|
||||
@@ -380,6 +380,11 @@ class PersistentConversationStore:
|
||||
for key in ("proposal_id", "request", "subject")
|
||||
}
|
||||
if all(normalized_proposal.values()):
|
||||
original_request = str(
|
||||
proposal.get("original_request") or ""
|
||||
).strip()
|
||||
if original_request:
|
||||
normalized_proposal["original_request"] = original_request
|
||||
workflow_id = str(proposal.get("workflow_id") or "").strip()
|
||||
if workflow_id in {
|
||||
"data_preparation",
|
||||
|
||||
@@ -793,6 +793,26 @@ CAPABILITIES: Dict[str, StageCapability] = {
|
||||
repair_candidates=["gff_id_overlap_report", "motif_database_fallback_to_jaspar", "artifact_reresolve"],
|
||||
rerun_policy="manual",
|
||||
),
|
||||
"rnaseq_report": StageCapability(
|
||||
stage="rnaseq_report",
|
||||
cli="apps/cli/run_rnaseq_report.py",
|
||||
env="test4py311",
|
||||
category="rnaseq",
|
||||
consumes=[
|
||||
ArtifactNeed("successful_rnaseq_manifests", "manifest", alternatives=["final_manifest_scan"]),
|
||||
],
|
||||
provides=[
|
||||
ArtifactProduct("rnaseq_report", "markdown", "{prefix}.rnaseq_report.md"),
|
||||
ArtifactProduct("rnaseq_report_evidence", "json", "{prefix}.rnaseq_report.evidence.json"),
|
||||
ArtifactProduct("rnaseq_report_model", "json", "{prefix}.rnaseq_report.model.json"),
|
||||
ArtifactProduct("rnaseq_report_manifest", "manifest", "{prefix}.rnaseq_report.manifest.json"),
|
||||
],
|
||||
scope_modes=["rnaseq_scope_id"],
|
||||
preflight_checks=["at_least_one_successful_rnaseq_manifest", "prefix_scope_identity_consistent"],
|
||||
postflight_checks=["deterministic_markdown_exists", "evidence_snapshot_exists", "coverage_recorded"],
|
||||
repair_candidates=["manifest_recovery", "regenerate_from_current_snapshot"],
|
||||
rerun_policy="manual_or_after_selected_batch_terminal",
|
||||
),
|
||||
"summary": StageCapability(
|
||||
stage="summary",
|
||||
cli="apps/cli/run_summary.py",
|
||||
@@ -804,11 +824,14 @@ CAPABILITIES: Dict[str, StageCapability] = {
|
||||
],
|
||||
provides=[
|
||||
ArtifactProduct("summary_report", "markdown", "{prefix}.summary_report.md"),
|
||||
ArtifactProduct("summary_evidence_index", "markdown", "{prefix}.summary_evidence_index.md"),
|
||||
ArtifactProduct("summary_evidence", "json", "{prefix}.summary.evidence.json"),
|
||||
ArtifactProduct("deepseek_metadata", "json", "{prefix}.summary.deepseek.json"),
|
||||
ArtifactProduct("summary_manifest", "manifest", "{prefix}.summary.manifest.json"),
|
||||
],
|
||||
scope_modes=["target_only", "target_plus_reference", "all_selected", "rnaseq"],
|
||||
preflight_checks=["at_least_one_success_manifest"],
|
||||
postflight_checks=["summary_contains_stage_evidence"],
|
||||
postflight_checks=["summary_contains_stage_evidence", "deepseek_report_generation_proven"],
|
||||
repair_candidates=["manifest_recovery", "artifact_reresolve"],
|
||||
rerun_policy="auto_after_manifest_recovery",
|
||||
),
|
||||
|
||||
@@ -90,6 +90,7 @@ PIPELINE_STAGES = {
|
||||
"rnaseq_go_enrich",
|
||||
"rnaseq_kegg_enrich",
|
||||
"rnaseq_candidate_family",
|
||||
"rnaseq_report",
|
||||
}
|
||||
|
||||
STAGE_DEPENDENCIES = {
|
||||
@@ -160,7 +161,7 @@ STAGE_DEPENDENCIES = {
|
||||
"interspecies_synteny": ["identify"],
|
||||
"karyotype": ["identify", "synteny"],
|
||||
"ppi": ["identify"],
|
||||
"kaks": ["identify", "ortholog"],
|
||||
"kaks": ["identify"],
|
||||
"combo": ["identify", "phylogeny", "motif", "structure", "domain"],
|
||||
"rnaseq_align": ["rnaseq_qc"],
|
||||
"rnaseq_quant": ["rnaseq_align"],
|
||||
@@ -173,6 +174,9 @@ STAGE_DEPENDENCIES = {
|
||||
"rnaseq_go_enrich": ["rnaseq_de"],
|
||||
"rnaseq_kegg_enrich": ["rnaseq_de"],
|
||||
"rnaseq_candidate_family": ["rnaseq_de", "rnaseq_go_enrich", "rnaseq_kegg_enrich", "rnaseq_fgsea"],
|
||||
# A report is a snapshot of whatever RNA-seq evidence is already successful.
|
||||
# Static dependencies would incorrectly prevent intentional mid-pipeline reports.
|
||||
"rnaseq_report": [],
|
||||
}
|
||||
|
||||
PARALLEL_GROUPS = [
|
||||
@@ -271,6 +275,7 @@ STAGE_RESOURCE_PROFILES = {
|
||||
"rnaseq_kegg_enrich": {"queue": "rnaseq", "resource_class": "rnaseq", "default_cpu": 4, "max_cpu": 4},
|
||||
"rnaseq_fgsea": {"queue": "rnaseq", "resource_class": "rnaseq", "default_cpu": 4, "max_cpu": 4},
|
||||
"rnaseq_candidate_family": {"queue": "rnaseq", "resource_class": "rnaseq", "default_cpu": 4, "max_cpu": 4},
|
||||
"rnaseq_report": {"queue": "light", "resource_class": "light", "default_cpu": 4, "max_cpu": 4},
|
||||
}
|
||||
|
||||
DEFAULT_STAGE_RESOURCE_PROFILE = {
|
||||
@@ -391,6 +396,7 @@ STAGE_ORDER = [
|
||||
"rnaseq_kegg_enrich",
|
||||
"rnaseq_fgsea",
|
||||
"rnaseq_candidate_family",
|
||||
"rnaseq_report",
|
||||
"rnaseq_enrich",
|
||||
"gene_family_report",
|
||||
]
|
||||
@@ -469,6 +475,7 @@ STAGE_INFO = {
|
||||
"rnaseq_enrich": {"name": "RNA-seq富集分析", "estimated_time": 15, "cpu": 4},
|
||||
"rnaseq_fgsea": {"name": "RNA-seq fgsea分析", "estimated_time": 15, "cpu": 4},
|
||||
"rnaseq_candidate_family": {"name": "RNA-seq候选家族筛选", "estimated_time": 20, "cpu": 4},
|
||||
"rnaseq_report": {"name": "RNA-seq结果报告", "estimated_time": 2, "cpu": 4},
|
||||
}
|
||||
|
||||
EXECUTABLE_STAGES = {
|
||||
@@ -544,6 +551,7 @@ EXECUTABLE_STAGES = {
|
||||
"rnaseq_enrich",
|
||||
"rnaseq_fgsea",
|
||||
"rnaseq_candidate_family",
|
||||
"rnaseq_report",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ RNASEQ_STATUS_STAGES = {
|
||||
"rnaseq_fgsea",
|
||||
"rnaseq_wgcna",
|
||||
"rnaseq_candidate_family",
|
||||
"rnaseq_report",
|
||||
"annotation",
|
||||
"annotation_go",
|
||||
"annotation_kegg",
|
||||
|
||||
@@ -546,6 +546,7 @@ class StageToolExecutor:
|
||||
ppi_reference_selection_mode=(extra_params or {}).get("ppiReferenceSelectionMode") or (extra_params or {}).get("ppi_reference_selection_mode") or "",
|
||||
ppi_reference_user_confirmed=bool_param("ppiReferenceUserConfirmed", "ppi_reference_user_confirmed", default=False),
|
||||
dependency_owner=(extra_params or {}).get("dependencyOwner") or (extra_params or {}).get("dependency_owner") or "",
|
||||
ortholog_mapping_owner=(extra_params or {}).get("orthologMappingOwner") or (extra_params or {}).get("ortholog_mapping_owner") or "",
|
||||
ppi_score=(extra_params or {}).get("ppiScore", 200),
|
||||
genome_prepare_type=(extra_params or {}).get("genomePrepareType", "auto"),
|
||||
kaks_neutral_rate=(extra_params or {}).get("kaksNeutralRate", 7.08e-9),
|
||||
@@ -1516,6 +1517,19 @@ class StageToolRegistry:
|
||||
"timeout": 7200,
|
||||
"conda_env": "rnaseq"
|
||||
},
|
||||
# 24. Formal RNA-seq Result Report
|
||||
{
|
||||
"id": "rnaseq_report",
|
||||
"name": "RNA-seq Result Report",
|
||||
"script": "run_rnaseq_report.py",
|
||||
"category": "rnaseq",
|
||||
"description": "Generate a scoped formal Markdown report from successful RNA-seq manifests",
|
||||
"required_inputs": [],
|
||||
"outputs": ["rnaseq_report", "rnaseq_report_evidence", "rnaseq_report_model"],
|
||||
"parameters": {"cpu": 4},
|
||||
"timeout": 1800,
|
||||
"conda_env": "test4py311"
|
||||
},
|
||||
# Combo Plot - 组合图
|
||||
{
|
||||
"id": "combo",
|
||||
|
||||
@@ -9,6 +9,16 @@ from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
ORTHOLOG_DEPENDENCY_OWNERS = {"", "ppi", "kaks"}
|
||||
|
||||
|
||||
def ortholog_output_stem(prefix: str, dependency_owner: str = "") -> str:
|
||||
owner = str(dependency_owner or "").strip().lower()
|
||||
if owner not in ORTHOLOG_DEPENDENCY_OWNERS:
|
||||
raise ValueError(f"unsupported Ortholog dependency owner: {dependency_owner}")
|
||||
return f"{prefix}.{owner}" if owner else prefix
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrthologRunRecord:
|
||||
query_species: str
|
||||
@@ -151,6 +161,7 @@ def materialize_ortholog_final_outputs(
|
||||
*,
|
||||
final_dir: Path,
|
||||
prefix: str,
|
||||
dependency_owner: str = "",
|
||||
run_records: list[OrthologRunRecord],
|
||||
job_id: str,
|
||||
workspace: Path,
|
||||
@@ -170,10 +181,12 @@ def materialize_ortholog_final_outputs(
|
||||
|
||||
final_dir.mkdir(parents=True, exist_ok=True)
|
||||
scope_fields = dict(scope_fields or {})
|
||||
owner = str(dependency_owner or "").strip().lower()
|
||||
output_stem = ortholog_output_stem(prefix, owner)
|
||||
|
||||
pairs_dst = final_dir / f"{prefix}.ortholog_pairs.txt"
|
||||
orthologs_dst = final_dir / f"{prefix}.orthologs.txt"
|
||||
raw_pairs_dst = final_dir / f"{prefix}.ortholog_pairs.raw.txt"
|
||||
pairs_dst = final_dir / f"{output_stem}.ortholog_pairs.txt"
|
||||
orthologs_dst = final_dir / f"{output_stem}.orthologs.txt"
|
||||
raw_pairs_dst = final_dir / f"{output_stem}.ortholog_pairs.raw.txt"
|
||||
|
||||
# run_ortholog.py writes subject<TAB>query<TAB>identity.
|
||||
# Downstream PPI expects query/source -> target/reference.
|
||||
@@ -203,7 +216,7 @@ def materialize_ortholog_final_outputs(
|
||||
pairs_handle.write(out_line)
|
||||
orthologs_handle.write(out_line)
|
||||
|
||||
blast_dst = final_dir / f"{prefix}.ortholog_blastp.txt"
|
||||
blast_dst = final_dir / f"{output_stem}.ortholog_blastp.txt"
|
||||
with blast_dst.open("w", encoding="utf-8") as blast_handle:
|
||||
for record in run_records:
|
||||
blast_src = record.blast_src
|
||||
@@ -266,7 +279,7 @@ def materialize_ortholog_final_outputs(
|
||||
"sha256": _sha256_file(blast_dst),
|
||||
}
|
||||
|
||||
ortholog_manifest = final_dir / f"{prefix}.ortholog.manifest.json"
|
||||
ortholog_manifest = final_dir / f"{output_stem}.ortholog.manifest.json"
|
||||
scope_file = str(scope_fields.pop("scope_file", "") or "").strip()
|
||||
scope_data = dict(scope_fields)
|
||||
|
||||
@@ -283,6 +296,7 @@ def materialize_ortholog_final_outputs(
|
||||
"stage": "ortholog",
|
||||
"status": "success",
|
||||
"prefix": prefix,
|
||||
"dependency_owner": owner,
|
||||
"job_id": job_id,
|
||||
"workspace": str(workspace),
|
||||
"workspace_root": str(workspace),
|
||||
@@ -303,6 +317,7 @@ def materialize_ortholog_final_outputs(
|
||||
],
|
||||
"parameters": {
|
||||
"prefix": prefix,
|
||||
"dependency_owner": owner,
|
||||
"blast_evalue": blast_evalue,
|
||||
"cpu": cpu,
|
||||
"reference_species": reference_species,
|
||||
@@ -340,7 +355,7 @@ def materialize_ortholog_final_outputs(
|
||||
if scope_id:
|
||||
from gfa_core.gene_family_scope import sanitize_token
|
||||
|
||||
scoped_manifest = final_dir / f"{prefix}.{sanitize_token(scope_id)}.ortholog.manifest.json"
|
||||
scoped_manifest = final_dir / f"{output_stem}.{sanitize_token(scope_id)}.ortholog.manifest.json"
|
||||
manifest_data["scope_manifest"] = str(scoped_manifest.resolve())
|
||||
with scoped_manifest.open("w", encoding="utf-8") as handle:
|
||||
json.dump(manifest_data, handle, ensure_ascii=False, indent=2)
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
"""Lightweight, provider-free RNA-seq manifest evidence snapshot.
|
||||
|
||||
The live interpretation view and the formal report share this module so scope
|
||||
selection and manifest provenance have one implementation. It intentionally
|
||||
does not import the MAS agent package.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
|
||||
REPORT_STAGE = "rnaseq_report"
|
||||
SUCCESS_STATUSES = {"success", "partial_success"}
|
||||
GLOBAL_LEGACY_STAGES = {
|
||||
"rnaseq_qc",
|
||||
"rnaseq_align",
|
||||
"rnaseq_quant",
|
||||
"annotation",
|
||||
"annotation_go",
|
||||
"annotation_kegg",
|
||||
}
|
||||
RNASEQ_STAGE_NAMES = {
|
||||
"rnaseq_qc": "RNA-seq质控",
|
||||
"rnaseq_align": "RNA-seq比对",
|
||||
"rnaseq_quant": "RNA-seq定量",
|
||||
"rnaseq_sample_qc": "RNA-seq样本质量评估",
|
||||
"rnaseq_de": "RNA-seq差异表达",
|
||||
"annotation": "背景功能注释",
|
||||
"annotation_go": "GO背景自动注释",
|
||||
"annotation_kegg": "KEGG/KO背景自动注释",
|
||||
"rnaseq_go_enrich": "RNA-seq GO富集分析",
|
||||
"rnaseq_kegg_enrich": "RNA-seq KEGG富集分析",
|
||||
"rnaseq_enrich": "RNA-seq富集分析(兼容入口)",
|
||||
"rnaseq_fgsea": "RNA-seq fgsea排序富集",
|
||||
"rnaseq_wgcna": "RNA-seq WGCNA共表达分析",
|
||||
"rnaseq_candidate_family": "RNA-seq候选家族筛选",
|
||||
"rnaseq_family_score": "RNA-seq候选家族评分",
|
||||
}
|
||||
RNASEQ_STAGE_ORDER = list(RNASEQ_STAGE_NAMES)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RNASeqManifestSelection:
|
||||
rnaseq_scope_id: str
|
||||
discovered_scope_ids: tuple[str, ...]
|
||||
manifests: dict[str, dict[str, Any]]
|
||||
|
||||
|
||||
def read_json(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
return data if isinstance(data, dict) else {}
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
|
||||
def manifest_identity_value(manifest: dict[str, Any], key: str) -> str:
|
||||
context = manifest.get("stage_context") or manifest.get("stageContext") or {}
|
||||
params = manifest.get("parameters") or {}
|
||||
return str(manifest.get(key) or params.get(key) or context.get(key) or "").strip()
|
||||
|
||||
|
||||
def is_rnaseq_evidence_stage(stage: str) -> bool:
|
||||
stage = str(stage or "").strip()
|
||||
return stage != REPORT_STAGE and (
|
||||
stage.startswith("rnaseq_") or stage in {"annotation", "annotation_go", "annotation_kegg"}
|
||||
)
|
||||
|
||||
|
||||
def stage_sort_key(stage: str) -> tuple[int, str]:
|
||||
try:
|
||||
return RNASEQ_STAGE_ORDER.index(stage), stage
|
||||
except ValueError:
|
||||
return len(RNASEQ_STAGE_ORDER), stage
|
||||
|
||||
|
||||
def _manifest_score(path: Path, manifest: dict[str, Any]) -> tuple[int, int, int, float]:
|
||||
status = str(manifest.get("status") or "")
|
||||
status_rank = 2 if status == "success" else 1 if status == "partial_success" else 0
|
||||
metrics = manifest.get("metrics") if isinstance(manifest.get("metrics"), dict) else {}
|
||||
artifacts = manifest.get("artifacts") if isinstance(manifest.get("artifacts"), dict) else {}
|
||||
try:
|
||||
mtime = path.stat().st_mtime
|
||||
except OSError:
|
||||
mtime = 0.0
|
||||
return status_rank, len(metrics), len(artifacts), mtime
|
||||
|
||||
|
||||
def select_rnaseq_manifests(
|
||||
final_dir: Path,
|
||||
prefix: str,
|
||||
*,
|
||||
rnaseq_scope_id: str = "",
|
||||
) -> RNASeqManifestSelection:
|
||||
"""Select one non-mixing manifest per RNA-seq stage for a prefix/scope."""
|
||||
final_dir = Path(final_dir)
|
||||
pattern = f"{prefix}.*.manifest.json" if prefix else "*.manifest.json"
|
||||
candidates: list[tuple[Path, dict[str, Any]]] = []
|
||||
latest_by_scope: dict[str, float] = {}
|
||||
for path in sorted(final_dir.glob(pattern)) if final_dir.exists() else []:
|
||||
manifest = read_json(path)
|
||||
stage = str(manifest.get("stage") or "")
|
||||
if not is_rnaseq_evidence_stage(stage):
|
||||
continue
|
||||
manifest_prefix = str(manifest.get("prefix") or "").strip()
|
||||
if prefix and manifest_prefix and manifest_prefix != prefix:
|
||||
continue
|
||||
candidates.append((path, manifest))
|
||||
scope = manifest_identity_value(manifest, "rnaseq_scope_id")
|
||||
if scope:
|
||||
try:
|
||||
latest_by_scope[scope] = max(latest_by_scope.get(scope, 0.0), path.stat().st_mtime)
|
||||
except OSError:
|
||||
latest_by_scope.setdefault(scope, 0.0)
|
||||
|
||||
discovered = sorted(latest_by_scope, key=lambda item: (-latest_by_scope[item], item))
|
||||
resolved_scope = str(rnaseq_scope_id or "").strip() or (discovered[0] if discovered else "")
|
||||
selected_paths: dict[str, Path] = {}
|
||||
selected: dict[str, dict[str, Any]] = {}
|
||||
for path, manifest in candidates:
|
||||
stage = str(manifest.get("stage") or "")
|
||||
scope = manifest_identity_value(manifest, "rnaseq_scope_id")
|
||||
if resolved_scope and scope and scope != resolved_scope:
|
||||
continue
|
||||
if resolved_scope and not scope and stage not in GLOBAL_LEGACY_STAGES:
|
||||
continue
|
||||
existing = selected.get(stage)
|
||||
if existing is not None and _manifest_score(selected_paths[stage], existing) >= _manifest_score(path, manifest):
|
||||
continue
|
||||
manifest = dict(manifest)
|
||||
manifest["_manifest_path"] = str(path)
|
||||
selected[stage] = manifest
|
||||
selected_paths[stage] = path
|
||||
return RNASeqManifestSelection(resolved_scope, tuple(discovered), selected)
|
||||
|
||||
|
||||
def _compact_value(value: Any, limit: int = 240) -> str:
|
||||
if isinstance(value, (dict, list)):
|
||||
text = json.dumps(value, ensure_ascii=False, sort_keys=True)
|
||||
else:
|
||||
text = str(value)
|
||||
return text if len(text) <= limit else text[: limit - 1] + "…"
|
||||
|
||||
|
||||
def resolve_artifact_path(
|
||||
final_dir: Path,
|
||||
item: dict[str, Any],
|
||||
*,
|
||||
require_exists: bool = False,
|
||||
) -> Path | None:
|
||||
"""Resolve artifacts after a workspace has been moved or mounted elsewhere."""
|
||||
final_dir = Path(final_dir)
|
||||
workspace = final_dir.parent
|
||||
candidates: list[Path] = []
|
||||
for raw in (item.get("full_path"), item.get("path")):
|
||||
if not raw:
|
||||
continue
|
||||
path = Path(str(raw))
|
||||
candidates.append(path if path.is_absolute() else workspace / path)
|
||||
if path.is_absolute():
|
||||
candidates.append(final_dir / path.name)
|
||||
|
||||
unique: list[Path] = []
|
||||
seen: set[str] = set()
|
||||
for path in candidates:
|
||||
identity = str(path)
|
||||
if identity not in seen:
|
||||
seen.add(identity)
|
||||
unique.append(path)
|
||||
for path in unique:
|
||||
if path.exists():
|
||||
return path
|
||||
if require_exists:
|
||||
return None
|
||||
return unique[0] if unique else None
|
||||
|
||||
|
||||
def build_rnaseq_evidence_snapshot(
|
||||
final_dir: Path,
|
||||
prefix: str,
|
||||
*,
|
||||
rnaseq_scope_id: str = "",
|
||||
include_stages: Iterable[str] | None = None,
|
||||
selection: RNASeqManifestSelection | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a compact evidence object without invoking an LLM or agent cache."""
|
||||
selection = selection or select_rnaseq_manifests(
|
||||
final_dir, prefix, rnaseq_scope_id=rnaseq_scope_id
|
||||
)
|
||||
allowed = {str(stage) for stage in include_stages or [] if str(stage).strip()}
|
||||
manifests = {
|
||||
stage: manifest
|
||||
for stage, manifest in selection.manifests.items()
|
||||
if not allowed or stage in allowed
|
||||
}
|
||||
stages = sorted(manifests, key=stage_sort_key)
|
||||
stage_flow: list[dict[str, Any]] = []
|
||||
findings: list[dict[str, Any]] = []
|
||||
reviews: list[dict[str, Any]] = []
|
||||
cards: list[dict[str, Any]] = []
|
||||
risks: list[dict[str, Any]] = []
|
||||
card_index = 0
|
||||
for stage in stages:
|
||||
manifest = manifests[stage]
|
||||
status = str(manifest.get("status") or "unknown")
|
||||
metrics = manifest.get("metrics") if isinstance(manifest.get("metrics"), dict) else {}
|
||||
parameters = manifest.get("parameters") if isinstance(manifest.get("parameters"), dict) else {}
|
||||
artifacts = manifest.get("artifacts") if isinstance(manifest.get("artifacts"), dict) else {}
|
||||
metric_items = list(sorted(metrics.items()))[:12]
|
||||
metric_summary = ", ".join(f"{key}={_compact_value(value, 80)}" for key, value in metric_items[:5])
|
||||
key_result = metric_summary or f"登记产物 {len(artifacts)} 个"
|
||||
stage_flow.append(
|
||||
{
|
||||
"id": stage,
|
||||
"name": RNASEQ_STAGE_NAMES.get(stage, stage),
|
||||
"status": status,
|
||||
"key_result": key_result,
|
||||
}
|
||||
)
|
||||
findings.append(
|
||||
{
|
||||
"stage": stage,
|
||||
"title": RNASEQ_STAGE_NAMES.get(stage, stage),
|
||||
"description": f"manifest 状态为 {status};{key_result}。",
|
||||
}
|
||||
)
|
||||
limitations: list[str] = []
|
||||
if status == "partial_success":
|
||||
limitations.append("该阶段为 partial_success,结果可用但不完整。")
|
||||
elif status != "success":
|
||||
limitations.append("该阶段未成功,不纳入正式结论。")
|
||||
missing_artifacts: list[str] = []
|
||||
for name, item in sorted(artifacts.items()):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
path = resolve_artifact_path(final_dir, item)
|
||||
exists = bool(path and path.exists())
|
||||
card_index += 1
|
||||
cards.append(
|
||||
{
|
||||
"id": f"RNA-E{card_index:04d}",
|
||||
"stage": stage,
|
||||
"claim": f"产物 {name}",
|
||||
"evidence": f"path={path or ''}, exists={exists}",
|
||||
"source_type": "raw_file" if exists else "manifest",
|
||||
}
|
||||
)
|
||||
if not exists:
|
||||
missing_artifacts.append(str(name))
|
||||
for key, value in metric_items:
|
||||
card_index += 1
|
||||
cards.append(
|
||||
{
|
||||
"id": f"RNA-E{card_index:04d}",
|
||||
"stage": stage,
|
||||
"claim": f"指标 {key}",
|
||||
"evidence": _compact_value(value),
|
||||
"source_type": "manifest",
|
||||
}
|
||||
)
|
||||
safe_parameter_items = [
|
||||
(key, value)
|
||||
for key, value in sorted(parameters.items())
|
||||
if not any(secret in str(key).lower() for secret in ("api_key", "token", "secret", "password"))
|
||||
][:10]
|
||||
for key, value in safe_parameter_items:
|
||||
card_index += 1
|
||||
cards.append(
|
||||
{
|
||||
"id": f"RNA-E{card_index:04d}",
|
||||
"stage": stage,
|
||||
"claim": f"参数 {key}",
|
||||
"evidence": _compact_value(value),
|
||||
"source_type": "manifest",
|
||||
}
|
||||
)
|
||||
if missing_artifacts:
|
||||
shown = missing_artifacts[:12]
|
||||
remainder = len(missing_artifacts) - len(shown)
|
||||
suffix = f"(另有 {remainder} 个,详见 evidence JSON)" if remainder else ""
|
||||
message = f"manifest 登记但文件不存在的产物:{', '.join(shown)}{suffix}"
|
||||
limitations.append(message)
|
||||
risks.append({"stage": stage, "severity": "high", "message": message})
|
||||
if status not in SUCCESS_STATUSES:
|
||||
risks.append({"stage": stage, "severity": "high", "message": f"阶段状态为 {status},不得作为成功证据。"})
|
||||
reviews.append({"stage": stage, "stage_name": RNASEQ_STAGE_NAMES.get(stage, stage), "limitations": limitations})
|
||||
|
||||
successful = [stage for stage in stages if str(manifests[stage].get("status") or "") in SUCCESS_STATUSES]
|
||||
return {
|
||||
"schema": "rnaseq_manifest_evidence_v1",
|
||||
"metadata": {
|
||||
"prefix": prefix,
|
||||
"rnaseq_scope_id": selection.rnaseq_scope_id,
|
||||
"discovered_scope_ids": list(selection.discovered_scope_ids),
|
||||
"included_stages": stages,
|
||||
"successful_stages": successful,
|
||||
"interpretation_boundary": "仅陈述当前 prefix 与 RNA-seq scope 下 manifest/产物支持的事实;富集、相关性和候选排序不得写成调控因果。",
|
||||
},
|
||||
"stage_flow": {"visualization": " -> ".join(stages), "stages": stage_flow},
|
||||
"key_findings": findings,
|
||||
"stage_reviews": reviews,
|
||||
"evidence_cards": cards,
|
||||
"risk_flags": risks,
|
||||
"major_claims": [],
|
||||
"biological_story": (
|
||||
f"当前报告汇总了 {len(successful)} 个成功或部分成功的 RNA-seq 阶段。"
|
||||
"跨阶段生物学解释必须以各阶段数值、图表和风险标记为边界。"
|
||||
),
|
||||
"next_steps": ["补齐缺失或失败阶段后重新生成报告,以刷新证据快照。"],
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import mimetypes
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
from urllib.parse import unquote
|
||||
@@ -79,8 +79,21 @@ class RNASeqReportResult:
|
||||
reused: bool = False
|
||||
|
||||
|
||||
BEIJING_TIMEZONE = timezone(timedelta(hours=8))
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now().astimezone().isoformat(timespec="seconds")
|
||||
return datetime.now(BEIJING_TIMEZONE).isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def _display_generated_at(value: str) -> str:
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value)
|
||||
if parsed.tzinfo is None:
|
||||
parsed = parsed.replace(tzinfo=BEIJING_TIMEZONE)
|
||||
return parsed.astimezone(BEIJING_TIMEZONE).strftime("%Y-%m-%d %H:%M:%S")
|
||||
except (TypeError, ValueError):
|
||||
return str(value or "").replace("T", " ")
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
@@ -1224,7 +1237,7 @@ def _render_deterministic_markdown(
|
||||
lines = [
|
||||
f"# {prefix} RNA-seq 结果解读报告",
|
||||
"",
|
||||
f"生成时间:{generated_at} ",
|
||||
f"生成时间(北京时间):{_display_generated_at(generated_at)} ",
|
||||
f"任务编号:`{workspace.name}`",
|
||||
"",
|
||||
"**任务结果路径:** ",
|
||||
|
||||
@@ -27,6 +27,11 @@
|
||||
- 若 reference CDS 在 config/scope 中缺失,可从已知 reference 数据目录解析,例如 At 的 TAIR10 CDS。
|
||||
- KaKs 是 reference dependency owner:若缺少 ortholog mapping,可以要求调度器先自动插入 Ortholog;但若 reference CDS/PEP 不完整,必须先提示上传/下载完整参考注释包,不能只跑 Ortholog 后再失败。
|
||||
- 若 PPI 与 KaKs 同批执行,reference resolver 必须按阶段分别检查;KaKs 使用 `requested_stages=["kaks"]` 检查 reference PEP/CDS 与 Ortholog,而其缺失状态不得覆盖 PPI 的独立就绪状态。
|
||||
- 普通计划不显示隐式 Ortholog,KaKs 也不声明对 Ortholog 或 PPI 的静态硬依赖。执行层仅用软顺序保证 KaKs 参数确认发生在 PPI 真实终态之后。
|
||||
- KaKs 确认必须显式选择参考来源:复用本次 PPI 技术参考,或使用项目配置参考。选择只写入 KaKs 参数,不得修改 PPI owner 或其它阶段的参考 scope。
|
||||
- 复用 PPI 参考时,只有 identity 完全匹配的 `final/{prefix}.ppi.orthologs.txt` 可用,并以 `orthologMappingOwner=ppi` 提交。
|
||||
- 选择项目参考时,缺少精确映射则运行 `dependency_owner=kaks` 的内部 Ortholog,输出 `final/{prefix}.kaks.orthologs.txt`,随后以 `orthologMappingOwner=kaks` 提交。
|
||||
- 两种 owner 都不得回落到另一 owner 或 standalone 映射。缺少所选参考的 PEP/CDS/映射只暂停 KaKs,不影响已完成 PPI 或其它阶段。
|
||||
|
||||
### Legacy 模式
|
||||
|
||||
|
||||
@@ -59,6 +59,19 @@
|
||||
| `{prefix}.ortholog.manifest.json` | json | 标准 manifest |
|
||||
| `{prefix}.{scope_id}.ortholog.manifest.json` | json | scope 模式下的 scoped manifest 副本 |
|
||||
|
||||
Owner namespace:
|
||||
|
||||
| producer 身份 | 映射/manifest stem | 用途 |
|
||||
|---|---|---|
|
||||
| standalone (`dependency_owner=""`) | `{prefix}.ortholog*` | 用户显式请求的 Ortholog,保持原契约 |
|
||||
| PPI (`dependency_owner="ppi"`) | `{prefix}.ppi.ortholog*` | PPI STRING 技术参考专属内部映射 |
|
||||
| KaKs (`dependency_owner="kaks"`) | `{prefix}.kaks.ortholog*` | KaKs 显式项目参考专属内部映射 |
|
||||
|
||||
- owner namespace 同时作用于 orthologs、pairs、raw pairs、BLAST、identity sidecar、标准 manifest 与 scoped manifest。
|
||||
- manifest 的 `stage` 仍为 `ortholog`;`parameters.dependency_owner` 记录 producer 身份。
|
||||
- standalone、PPI、KaKs 三套产物可为同一 prefix 并存;重跑一个 owner 不得覆盖或改变另一个 owner。
|
||||
- 下游按 owner token 解析候选路径,不接受前端传入任意文件路径,也不得跨 owner 回落。
|
||||
|
||||
## Manifest 最小字段
|
||||
|
||||
```json
|
||||
@@ -104,6 +117,7 @@
|
||||
- 单 target + `target_only` scope 时,`analysis_route` 通常为 `single_species`;含 reference 的 comparative scope 通常为 `multi_species`。
|
||||
- postflight strict identity 诊断依赖上述字段;materialize 落盘时必须补齐,不能只写 `scope_id`。
|
||||
- PPI-owned producer 的 `reference_taxon_id` 来自已确认 STRING TaxID,`reference_storage_key` 决定本地 PEP 路径及 mapping 的 Target_Species。
|
||||
- PPI-owned producer 的生物学 scope 必须与 PPI consumer 相同,固定为目标家族 `target_only`;技术参考物种只写入 owner/provenance 参数。
|
||||
- sidecar 与当前请求必须在 `reference_taxon_id`、`reference_storage_key`、reference PEP SHA256、target/query PEP identity、scope/member identity、mapping path/SHA256 和 `mapping_identity` 上完全一致,才允许跳过 BLAST 并复用中间 pairs。
|
||||
- sidecar 缺失、无法解析、字段缺失或任一 identity 冲突都视为不可复用;应按原 Ortholog 命令重新生成,不得根据“文件存在”推断身份。
|
||||
- PPI preflight 不得只读取 final mapping 的 Target_Species 后判定 ready;还必须同时验证 final mapping sidecar、对应 Ortholog manifest,以及当前 reference PEP、target/query PEP、scope/member 和 mapping identity。任一输入变化或身份材料缺失均返回 `needs_ortholog` / stale identity conflict。
|
||||
|
||||
@@ -181,6 +181,17 @@ python skills/ppi/run.py \
|
||||
## 下游阶段
|
||||
- summary: 可汇总PPI结果
|
||||
|
||||
## 内部 Ortholog 身份与调度
|
||||
|
||||
- 普通基因家族计划只展示 PPI;PPI 所需的 Ortholog 是执行层内部 producer,不作为可见计划阶段或独立确认弹窗。
|
||||
- PPI 的生物学分析身份固定为目标家族成员 `target_only`;项目参考物种不得进入 PPI 的 `scope_id`、`analysis_route` 或 `member_set_id`。
|
||||
- STRING TaxID、技术参考 storage key 和同源映射参考只属于 PPI 技术溯源,不得被当作生物学 `referenceSpecies` 重建 PPI scope。
|
||||
- 非目标物种直连 STRING 路线使用 `dependency_owner=ppi`,映射输出为 `final/{prefix}.ppi.orthologs.txt`(及同 owner 的 pairs/raw/BLAST/manifest/sidecar)。
|
||||
- PPI 只能读取 PPI owner 映射;不得因 standalone 或 KaKs owner 文件存在而静默复用。
|
||||
- PPI 的 STRING 技术参考只属于 PPI。它不得覆盖项目参考物种或 KaKs 的显式参考选择。
|
||||
- PPI 成功、失败或用户明确跳过都会释放后续 KaKs 确认;正在进行的 PPI 参考包下载不是终态。
|
||||
- PPI-owned Ortholog 完成后必须用同一 target-only 身份重新执行预检;只有返回 `ready` 才能提交 PPI。
|
||||
|
||||
## 网络统计指标
|
||||
|
||||
| 指标 | 描述 |
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# RNA-seq Result Report Contract v1
|
||||
|
||||
## Stage
|
||||
|
||||
- Stage id: `rnaseq_report`
|
||||
- Script: `apps/cli/run_rnaseq_report.py`
|
||||
- Scope: one exact `prefix + rnaseq_scope_id` evidence snapshot
|
||||
- Purpose: materialize a formal, reproducible RNA-seq Markdown result report without depending on frontend session state.
|
||||
|
||||
## Inputs
|
||||
|
||||
- At least one RNA-seq/annotation manifest with status `success` or `partial_success` under `final/`.
|
||||
- All selected evidence must match the requested prefix and RNA-seq scope. Legacy unscoped QC/alignment/quantification/annotation manifests may be treated as experiment-global evidence.
|
||||
- `expected_stages` is optional and records the stages selected for the current batch. It controls coverage only; it must not become a static execution dependency.
|
||||
- An existing configured `LLMManager` provider is optional. No provider or model name is required by this stage.
|
||||
|
||||
If no successful RNA-seq evidence exists, the stage must return `暂无可生成的 RNA-seq 结果` and must not create an empty report.
|
||||
|
||||
## Required Outputs
|
||||
|
||||
- `final/{prefix}.rnaseq_report.md`
|
||||
- `final/{prefix}.rnaseq_report.evidence.json`
|
||||
- `final/{prefix}.rnaseq_report.model.json`
|
||||
- `final/{prefix}.rnaseq_report.manifest.json`
|
||||
|
||||
The manifest stage status is `success` whenever these outputs are generated. Content completeness is recorded separately as `metrics.coverage_status = complete|partial`.
|
||||
|
||||
## Generation Semantics
|
||||
|
||||
- `generate`: always rebuild from the current manifest snapshot.
|
||||
- `open`: reuse a fresh report; regenerate first when the source/request fingerprint changed.
|
||||
- The deterministic Markdown body is always complete enough to read without an API.
|
||||
- Optional model output is a labeled language-enhancement section only. Model failure must retain the deterministic report and be recorded in `.model.json`.
|
||||
- `rnaseq_report` manifests are excluded from their own evidence scan.
|
||||
|
||||
## Boundary With Deep Interpretation
|
||||
|
||||
Both features may call the pure `gfa_core.rnaseq_evidence` snapshot builder. The formal report must not call `/api/agent/interpret`, read its session cache, or replace the frontend deep-interpretation view.
|
||||
|
||||
## Validation
|
||||
|
||||
- Every manifest artifact path exists and its digest is recorded.
|
||||
- Evidence contains only the selected prefix/scope.
|
||||
- `coverage_status=complete` only when every expected stage has status `success`.
|
||||
- Missing, failed, or `partial_success` expected stages yield `coverage_status=partial` while the report manifest remains `success`.
|
||||
@@ -33,10 +33,6 @@
|
||||
|
||||
各阶段依次写明分析对象、源结果中的直接观察、谨慎解释、限制,并仅在有助复核时列出少量相对路径。
|
||||
|
||||
### 2.2 语言边界
|
||||
|
||||
预测关系统一表述为线索;没有逐成员表或独立证据时,不指定候选基因,不声称功能改变或调控关系已经证实。
|
||||
|
||||
<!-- GFA_SECTION common.completeness -->
|
||||
## 三、结果总览与完整性检查
|
||||
|
||||
|
||||
Reference in New Issue
Block a user