fix: complete PPI and KaKs reference handoff
This commit is contained in:
@@ -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, [])
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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] = {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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。
|
||||
|
||||
## 网络统计指标
|
||||
|
||||
| 指标 | 描述 |
|
||||
|
||||
Reference in New Issue
Block a user