fix: complete PPI and KaKs reference handoff

This commit is contained in:
potaser
2026-08-10 23:13:05 +08:00
parent 39a2c969de
commit 7b7e334213
37 changed files with 4770 additions and 561 deletions
+1 -13
View File
@@ -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",
@@ -1958,17 +1957,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, [])
+6
View File
@@ -458,6 +458,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",
@@ -474,6 +477,7 @@ class RouterRuntime:
session_id: str,
job_id: str,
query: str,
original_query: str,
subject: str,
workflow_id: WorkflowId | None = None,
) -> dict[str, str]:
@@ -487,6 +491,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:
@@ -791,6 +796,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,
)
+30 -6
View File
@@ -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] = {
+5
View File
@@ -368,6 +368,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",
+1 -1
View File
@@ -159,7 +159,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"],
+1
View File
@@ -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),
+21 -6
View File
@@ -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)
@@ -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。
## 网络统计指标
| 指标 | 描述 |