This commit is contained in:
potaser
2026-08-19 20:23:24 +08:00
62 changed files with 6726 additions and 865 deletions
+299 -118
View File
@@ -1,6 +1,7 @@
# web/pipeline_backend.py
import csv
import json
import csv
import http.client
import json
import os
import re
import shlex
@@ -48,6 +49,7 @@ from gfa_core.ppi_provenance import (
record_ppi_preflight_decision,
)
from gfa_core.string_species_registry import (
ARABIDOPSIS_TAXON_ID,
StringSnapshotError,
load_string_snapshot,
resolve_ppi_string_species,
@@ -1721,7 +1723,7 @@ def infer_string_species_from_ortholog_mapping(mapping_file: Path, fallback: str
return fallback
def infer_reference_species_from_ortholog_mapping(mapping_file: Path, fallback: str = "") -> str:
def infer_reference_species_from_ortholog_mapping(mapping_file: Path, fallback: str = "") -> str:
"""Infer the reference species label from the first data row of an ortholog mapping."""
try:
with mapping_file.open("r", encoding="utf-8", errors="ignore") as src:
@@ -1735,10 +1737,34 @@ def infer_reference_species_from_ortholog_mapping(mapping_file: Path, fallback:
break
except Exception:
pass
return fallback
def _reference_species_from_manifest_data(data: dict[str, Any]) -> str:
return fallback
def ortholog_mapping_candidates(
final_dir: Path,
prefix: str,
*,
dependency_owner: str = "",
scope_id: str = "",
) -> list[Path]:
from gfa_core.ortholog_materialize import ortholog_output_stem
output_stem = ortholog_output_stem(prefix, dependency_owner)
candidates: list[Path] = []
if scope_id:
scoped_stem = f"{output_stem}.{sanitize_token(scope_id)}"
candidates.extend([
final_dir / f"{scoped_stem}.orthologs.txt",
final_dir / f"{scoped_stem}.ortholog_pairs.txt",
])
candidates.extend([
final_dir / f"{output_stem}.orthologs.txt",
final_dir / f"{output_stem}.ortholog_pairs.txt",
])
return candidates
def _reference_species_from_manifest_data(data: dict[str, Any]) -> str:
for key in ("reference_species", "referenceSpecies"):
value = data.get(key)
if isinstance(value, list) and value:
@@ -1905,11 +1931,10 @@ NCBI_REFERENCE_ANNOTATION_TYPES = {
}
def _reference_bundle_file_plan(requested_stages: Optional[list[str]], include_recommended: bool = False) -> list[str]:
stages = _normalized_dependency_stages(requested_stages)
files = ["pep"]
if "kaks" in stages:
files.append("cds")
def _reference_bundle_file_plan(requested_stages: Optional[list[str]], include_recommended: bool = False) -> list[str]:
# PPI owns reference preparation. Download PEP and CDS together so the
# later Ka/Ks choice can reuse the exact same bundle without a second job.
files = ["pep", "cds"]
if include_recommended:
for item in ("gff3", "genome"):
if item not in files:
@@ -2005,7 +2030,7 @@ def _download_url_to_file(
raise RuntimeError("downloaded file is empty")
temp_path.replace(output_path)
return total
except (urllib.error.URLError, TimeoutError, OSError, RuntimeError) as exc:
except (http.client.IncompleteRead, urllib.error.URLError, TimeoutError, OSError, RuntimeError) as exc:
last_error = exc
if temp_path.exists():
try:
@@ -2119,6 +2144,8 @@ def _fasta_header_index(fasta_path: Path | str | None) -> dict[str, dict[str, st
if not seq_id or seq_id in records:
continue
gene_match = re.search(r"\[gene=([^\]]+)\]", header)
if not gene_match:
gene_match = re.search(r"(?:^|\s)gene:([^\s]+)", header)
ncbi_gene_match = re.search(r"\[db_xref=GeneID:(\d+)\]", header)
protein_match = re.search(r"\[protein_id=([^\]]+)\]", header)
records[seq_id] = {
@@ -2220,7 +2247,7 @@ def download_reference_annotation_bundle(
include_recommended=include_recommended,
)
assembly = {"accession": accession}
emit({"phase": "select_assembly", "message": "正在解析推荐参考物种装配", "percentage": 5, "indeterminate": True})
emit({"phase": "select_assembly", "message": "Resolving the recommended reference assembly", "percentage": 5, "indeterminate": True})
if not accession:
assembly = _select_ncbi_reference_assembly(taxon_id=taxon_id, species_name=species_name, timeout_sec=min(timeout_sec, 60))
accession = assembly.get("accession", "")
@@ -2231,24 +2258,24 @@ def download_reference_annotation_bundle(
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
work_dir = ws / "reference_bundle_download" / f"{code}_{stamp}"
zip_path = work_dir / f"{code}.ncbi_dataset.zip"
emit({"phase": "download", "message": "正在下载 NCBI 参考注释包", "percentage": 10, "indeterminate": True})
emit({"phase": "download", "message": "Downloading reference PEP and CDS from NCBI", "percentage": 10, "indeterminate": True})
download_kwargs = {"timeout_sec": timeout_sec}
if progress_callback:
download_kwargs["progress_callback"] = lambda event: emit({"message": "正在下载 NCBI 参考注释包", **event})
download_kwargs["progress_callback"] = lambda event: emit({"message": "Downloading reference PEP and CDS from NCBI", **event})
bytes_downloaded = _download_url_to_file(download_url, zip_path, **download_kwargs)
if not zipfile.is_zipfile(zip_path):
raise RuntimeError(
f"NCBI reference bundle download returned an invalid zip archive: {zip_path}"
)
species_dir = in_dir / code
emit({"phase": "extract", "message": "正在解压参考 PEP/CDS", "percentage": 80, "indeterminate": True})
emit({"phase": "extract", "message": "Extracting reference PEP and CDS", "percentage": 80, "indeterminate": True})
extracted = _extract_ncbi_reference_bundle_zip(
zip_path=zip_path,
destination_dir=species_dir,
species_code=code,
requested_file_types=file_types,
)
emit({"phase": "validate_ids", "message": "正在校验 PEP/CDS ID 对齐", "percentage": 90, "indeterminate": True})
emit({"phase": "validate_ids", "message": "Validating PEP/CDS identifier correspondence", "percentage": 90, "indeterminate": True})
id_validation = _build_reference_bundle_id_map(
species_code=code,
pep_path=extracted.get("pep"),
@@ -2288,7 +2315,7 @@ def download_reference_annotation_bundle(
manifest["manifest_path"] = str(manifest_path)
emit({
"phase": "completed",
"message": "参考物种注释包下载完成",
"message": "PPI and Ka/Ks reference species data are ready",
"percentage": 100,
"indeterminate": False,
"bytes_downloaded": bytes_downloaded,
@@ -2311,6 +2338,7 @@ def _ppi_only_preflight_response(
selected_string_species: str = "",
reference_storage_key: str = "",
reference_pep: str = "",
reference_cds: str = "",
mapping_file: Optional[Path] = None,
mapping_reference_species: str = "",
required_next_stage: str = "",
@@ -2319,22 +2347,28 @@ def _ppi_only_preflight_response(
) -> dict[str, Any]:
candidates = list(species_resolution.get("candidates") or [])
selection_mode = str(species_resolution.get("selection_mode") or "unresolved")
required_files = ["pep", "cds"] if reference_storage_key else []
bundle_files = {}
if reference_pep:
bundle_files["pep"] = reference_pep
if reference_cds:
bundle_files["cds"] = reference_cds
reference_bundle_status = {
"required_files": ["pep"],
"required_files": required_files,
"recommended_files": [],
"files": {"pep": reference_pep} if reference_pep else {},
"files": bundle_files,
"missing_required": list(missing_required or []),
"missing_recommended": [],
"mode": (
"complete"
if reference_pep
if not required_files or (reference_pep and reference_cds)
else "not_selected"
if status in {"confirmation_required", "unresolved", "conflict"}
else "unavailable"
if status == "resource_error"
else "incomplete"
),
"kaks_ready": True,
"kaks_ready": (not required_files) or bool(reference_pep and reference_cds),
}
diagnostics = {
"issue_code": issue_code,
@@ -2343,7 +2377,7 @@ def _ppi_only_preflight_response(
"required_next_stage": required_next_stage,
"recommendation": message,
"dependency_owner_stages": ["ppi"],
"required_bundle_files": ["pep"],
"required_bundle_files": required_files,
"reference_bundle_status": reference_bundle_status,
"ortholog_dependency_role": "producer" if required_next_stage == "ortholog" else "not_requested",
"reference_recommendations": candidates,
@@ -2399,24 +2433,12 @@ def _effective_ppi_owner_scope_id(
scope_data: dict[str, Any],
reference_storage_key: str,
) -> str:
scope_mode = str(scope_data.get("scope_mode") or "target_plus_reference")
comparative_species = [
str(x) for x in scope_data.get("comparative_species", []) if str(x)
]
if scope_mode == "target_only":
return str(scope_data.get("scope_id") or "") or build_scope_id(
family,
selected_targets,
[str(x) for x in scope_data.get("reference_species", []) if str(x)],
comparative_species,
scope_mode,
)
return build_scope_id(
family,
selected_targets,
[reference_storage_key],
comparative_species,
scope_mode,
[],
[],
"target_only",
)
@@ -2616,6 +2638,16 @@ def _resolve_ppi_only_snapshot_preflight(
),
None,
)
explicit_model_fallback = bool(
str(ppi_reference_selection_mode or "").strip().lower() == "model_fallback"
and chosen_taxon_id == ARABIDOPSIS_TAXON_ID
and chosen_taxon_id in snapshot.registry
)
if explicit_model_fallback:
chosen = {
"selected_string_taxon_id": chosen_taxon_id,
"selected_string_name": snapshot.nodes[chosen_taxon_id].taxon_name,
}
if not (ppi_reference_user_confirmed and chosen):
return _ppi_only_preflight_response(
status="confirmation_required",
@@ -2628,6 +2660,13 @@ def _resolve_ppi_only_snapshot_preflight(
requested_ppi_species=requested_ppi_species,
dependency_owner=dependency_owner,
)
if explicit_model_fallback:
species_resolution = {
**species_resolution,
"selection_mode": "model_fallback",
"candidate_source": "explicit_model_fallback",
"issue_code": "",
}
storage_key = str(ppi_reference_storage_key or "").strip() or f"string_taxon_{chosen_taxon_id}"
selected_name = str(chosen.get("selected_string_name") or "")
@@ -2638,16 +2677,16 @@ def _resolve_ppi_only_snapshot_preflight(
ws, final_dir, in_dir, scope_data, storage_key, family, "pep", family_only=False
)
reference_pep = str(reference_pep_path) if reference_pep_path else ""
mapping_candidates: list[Path] = []
if scope_id:
mapping_candidates.extend([
final_dir / f"{prefix}.{sanitize_token(scope_id)}.orthologs.txt",
final_dir / f"{prefix}.{sanitize_token(scope_id)}.ortholog_pairs.txt",
])
mapping_candidates.extend([
final_dir / f"{prefix}.orthologs.txt",
final_dir / f"{prefix}.ortholog_pairs.txt",
])
reference_cds_path = resolve_species_sequence_file(
ws, final_dir, in_dir, scope_data, storage_key, family, "cds", family_only=False
)
reference_cds = str(reference_cds_path) if reference_cds_path else ""
mapping_candidates = ortholog_mapping_candidates(
final_dir,
prefix,
dependency_owner="ppi",
scope_id=scope_id,
)
mapping_file = None if ignore_existing_ortholog else next(
(path for path in mapping_candidates if path.exists() and path.stat().st_size > 0),
None,
@@ -2696,18 +2735,24 @@ def _resolve_ppi_only_snapshot_preflight(
"selected_string_species": species_to_string_code(selected_name, ""),
"reference_storage_key": storage_key,
"reference_pep": reference_pep,
"reference_cds": reference_cds,
"mapping_file": mapping_file,
"mapping_reference_species": mapping_reference_species,
"dependency_owner": dependency_owner,
}
if not reference_pep:
missing_reference_files = [
file_type
for file_type, file_path in (("pep", reference_pep), ("cds", reference_cds))
if not file_path
]
if missing_reference_files:
return _ppi_only_preflight_response(
status="needs_reference_data",
issue_code="ppi_reference_data_missing",
message="The confirmed STRING reference requires a protein FASTA (PEP) before Ortholog.",
message="The confirmed STRING reference requires matching PEP and CDS before Ortholog.",
decision="needs_reference_bundle",
required_next_stage="",
missing_required=["pep"],
missing_required=missing_reference_files,
**common,
)
if mapping_file and not mapping_matches:
@@ -2775,9 +2820,7 @@ def resolve_ppi_species_resolution(
dependency_stages = _normalized_dependency_stages(requested_stages)
needs_ppi = "ppi" in dependency_stages
needs_kaks = "kaks" in dependency_stages
required_bundle_files = ["pep"]
if needs_kaks and "cds" not in required_bundle_files:
required_bundle_files.append("cds")
required_bundle_files = ["pep", "cds"]
recommended_bundle_files = ["gff3", "genome"] if needs_kaks else []
selected_targets = canonicalize_species_codes_for_job(in_dir, [str(x).strip() for x in (target_species or []) if str(x).strip()])
references = canonicalize_species_codes_for_job(in_dir, [str(x).strip() for x in (reference_species or []) if str(x).strip()])
@@ -2818,6 +2861,22 @@ def resolve_ppi_species_resolution(
)
if needs_ppi and not needs_kaks:
scope_id = _effective_ppi_owner_scope_id(
family=family,
selected_targets=selected_targets,
scope_data=scope_data,
reference_storage_key=str(ppi_reference_storage_key or ""),
)
scope_data = {
**scope_data,
"family": family,
"scope_id": scope_id,
"scope_mode": "target_only",
"target_species": list(selected_targets),
"reference_species": [],
"comparative_species": [],
"selected_species": list(selected_targets),
}
result = _resolve_ppi_only_snapshot_preflight(
job=job,
prefix=prefix,
@@ -2886,17 +2945,15 @@ def resolve_ppi_species_resolution(
and target_string_code
and selected_string_species == target_string_code
)
mapping_candidates: list[Path] = []
if not use_target_string_directly or needs_kaks:
if scope_id:
mapping_candidates.extend([
final_dir / f"{prefix}.{sanitize_token(scope_id)}.orthologs.txt",
final_dir / f"{prefix}.{sanitize_token(scope_id)}.ortholog_pairs.txt",
])
mapping_candidates.extend([
final_dir / f"{prefix}.orthologs.txt",
final_dir / f"{prefix}.ortholog_pairs.txt",
])
mapping_candidates: list[Path] = []
if not use_target_string_directly or needs_kaks:
mapping_owner = str(dependency_owner or "").strip().lower()
mapping_candidates = ortholog_mapping_candidates(
final_dir,
prefix,
dependency_owner=mapping_owner if mapping_owner in {"ppi", "kaks"} else "",
scope_id=scope_id,
)
mapping_file = None if ignore_existing_ortholog else next((p for p in mapping_candidates if p.exists() and p.stat().st_size > 0), None)
reference_data_source = ""
@@ -2917,7 +2974,7 @@ def resolve_ppi_species_resolution(
reference_data_source = str(ref_pep) if ref_pep else ""
if ref_pep:
reference_bundle_files["pep"] = str(ref_pep)
if needs_kaks:
if "cds" in required_bundle_files:
ref_cds = resolve_species_sequence_file(
ws,
final_dir,
@@ -3355,6 +3412,7 @@ RNA_SEQ_STAGES = {
"rnaseq_wgcna",
"rnaseq_candidate_family",
"rnaseq_family_score",
"rnaseq_report",
}
@@ -6025,6 +6083,7 @@ def run_stage(
ppi_reference_selection_mode: str = "",
ppi_reference_user_confirmed: bool = False,
dependency_owner: str = "",
ortholog_mapping_owner: str = "",
ppi_score: int = 200,
# Genome Prepare 参数
genome_prepare_type: str = "auto",
@@ -8383,7 +8442,32 @@ def run_stage(
expected_stages = [str(value).strip() for value in expected_stages if str(value).strip()]
report_action = str(kwargs.get("report_action") or kwargs.get("reportAction") or "generate").strip()
if report_action == "generate" and not expected_stages:
return "❌ 基因家族结果报告生成需要非空 expected_stages", manifest
report_stage_order = (
"identify", "phylogeny", "reference_classification", "motif",
"domain", "structure", "combo", "promoter", "tfbs",
"synteny", "karyotype", "interspecies_synteny", "pep",
)
recovered_stages = set()
for candidate in final_dir.glob("*.manifest.json"):
try:
payload = json.loads(candidate.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
continue
candidate_stage = str(payload.get("stage") or "").strip()
candidate_prefix = str(
payload.get("prefix")
or (payload.get("parameters") or {}).get("prefix")
or ""
).strip()
if (
candidate_stage in report_stage_order
and str(payload.get("status") or "") == "success"
and candidate_prefix == prefix
):
recovered_stages.add(candidate_stage)
expected_stages = [stage_id for stage_id in report_stage_order if stage_id in recovered_stages]
if report_action == "generate" and not expected_stages:
return "❌ 基因家族结果报告生成需要非空 expected_stages,且当前前缀没有可恢复的成功 manifest", manifest
stage_statuses = kwargs.get("stage_statuses") or kwargs.get("stageStatuses") or {}
if isinstance(stage_statuses, str):
try:
@@ -8392,6 +8476,8 @@ def run_stage(
return "❌ 基因家族结果报告的 stage_statuses 不是合法 JSON", manifest
if not isinstance(stage_statuses, dict):
return "❌ 基因家族结果报告的 stage_statuses 必须是对象", manifest
if not stage_statuses:
stage_statuses = {stage_id: "success" for stage_id in expected_stages}
report_use_model_raw = kwargs.get("report_use_model", kwargs.get("reportUseModel", True))
rnaseq_scope_id = str(kwargs.get("rnaseq_scope_id") or kwargs.get("rnaseqScopeId") or "").strip()
cmd = [
@@ -11407,6 +11493,42 @@ def run_stage(
return "✅ RNA-seq WGCNA completed.", wgcna_manifest
return f"❌ RNA-seq WGCNA failed (exit code: {code}). Log: {log_file}", wgcna_manifest if wgcna_manifest.exists() else manifest
if stage == "rnaseq_report":
report_action = str(kwargs.get("rnaseq_report_action") or kwargs.get("report_action") or "generate").strip().lower()
if report_action not in {"generate", "open"}:
report_action = "generate"
expected_raw = kwargs.get("rnaseq_expected_stages") or kwargs.get("expected_stages") or ""
if isinstance(expected_raw, (list, tuple, set)):
expected_arg = ",".join(str(item).strip() for item in expected_raw if str(item).strip())
else:
expected_arg = str(expected_raw or "").strip()
report_scope_id = str(kwargs.get("rnaseq_scope_id") or kwargs.get("rnaseqScopeId") or "").strip()
report_use_model = kwargs.get("rnaseq_report_use_model", kwargs.get("report_use_model", True))
report_cmd = [
sys.executable,
"apps/cli/run_rnaseq_report.py",
"--workspace", str(ws),
"--prefix", prefix,
"--job-id", job.job_id,
"--action", report_action,
"--log-file", str(log_file),
]
if report_scope_id:
report_cmd.extend(["--rnaseq-scope-id", report_scope_id])
if expected_arg:
report_cmd.extend(["--expected-stages", expected_arg])
if not _truthy(report_use_model):
report_cmd.append("--no-model")
print(f"[CMD] {' '.join(report_cmd)}")
code = run_cmd_logged(report_cmd, log_file, cwd=Path("."))
report_manifest = final_dir / f"{prefix}.rnaseq_report.manifest.json"
if code == 0 and report_manifest.exists():
action_label = "打开" if report_action == "open" else "生成"
return f"✅ RNA-seq 结果报告已{action_label}。日志:{log_file}", report_manifest
if code == 2:
return "⚠️ 暂无可生成的 RNA-seq 结果:当前 prefix/scope 下没有成功阶段。", None
return f"❌ RNA-seq 结果报告生成失败(exit code: {code})。日志:{log_file}", report_manifest if report_manifest.exists() else None
if stage == "rnaseq_candidate_family":
_SPECIES_RESOURCE_ALIASES = {
"os": ["Os", "Oryza", "Oryza_sativa", "rice"],
@@ -12450,20 +12572,42 @@ def run_stage(
if not references:
return "❌ Ortholog scope 中没有 reference_species,无法确定 subject/reference 物种。", manifest
reference_species = references[0]
subject_fasta = first_existing_scope_file(ortholog_scope_data, reference_species, ["family_pep", "pep"])
if not subject_fasta:
subject_fasta = resolve_species_family_pep(ws, final_dir, in_dir, reference_species, family)
if not subject_fasta:
subject_fasta = resolve_species_sequence_file(
ws,
final_dir,
if ppi_owned_producer:
subject_fasta = resolve_species_sequence_file(
ws,
final_dir,
in_dir,
ortholog_scope_data,
reference_species,
family,
"pep",
family_only=False,
)
"pep",
family_only=False,
)
else:
subject_fasta = first_existing_scope_file(
ortholog_scope_data,
reference_species,
["family_pep", "pep"],
)
if not subject_fasta:
subject_fasta = resolve_species_family_pep(
ws,
final_dir,
in_dir,
reference_species,
family,
)
if not subject_fasta:
subject_fasta = resolve_species_sequence_file(
ws,
final_dir,
in_dir,
ortholog_scope_data,
reference_species,
family,
"pep",
family_only=False,
)
if not subject_fasta:
return (
f"❌ Ortholog stage requires reference protein FASTA for {reference_species}. "
@@ -12510,11 +12654,14 @@ def run_stage(
ortholog_pairs_has_data,
write_ortholog_run_sidecar,
)
ortholog_owner = str(dependency_owner or "").strip().lower()
if ortholog_owner not in {"", "ppi", "kaks"}:
return f"❌ Unsupported Ortholog dependency owner: {dependency_owner}", manifest
scope_fields: dict[str, Any] = {}
if ortholog_scope_data:
source_scope_mode = str(ortholog_scope_data.get("scope_mode") or "")
preserve_target_only_scope = ppi_owned_producer and source_scope_mode == "target_only"
preserve_target_only_scope = ppi_owned_producer
effective_scope_id = str(ortholog_scope_data.get("scope_id") or "")
if ppi_owned_producer:
effective_scope_id = _effective_ppi_owner_scope_id(
@@ -12524,19 +12671,19 @@ def run_stage(
reference_storage_key=reference_species,
)
scope_fields = {
"scope_file": str(ortholog_scope_path),
"scope_file": str(ortholog_scope_path) if source_scope_mode == "target_only" else "",
"scope_id": effective_scope_id,
"scope_mode": ortholog_scope_data.get("scope_mode", ""),
"scope_mode": "target_only" if ppi_owned_producer else ortholog_scope_data.get("scope_mode", ""),
"target_species": [record[0] for record in run_specs],
"reference_species": (
ortholog_scope_data.get("reference_species", [])
if preserve_target_only_scope
[]
if ppi_owned_producer
else [reference_species] if reference_species else []
),
"comparative_species": ortholog_scope_data.get("comparative_species", []),
"comparative_species": [] if ppi_owned_producer else ortholog_scope_data.get("comparative_species", []),
"selected_species": (
ortholog_scope_data.get("selected_species", [record[0] for record in run_specs])
if preserve_target_only_scope
[record[0] for record in run_specs]
if ppi_owned_producer
else [record[0] for record in run_specs] + ([reference_species] if reference_species else [])
),
"family": family,
@@ -12615,9 +12762,10 @@ def run_stage(
run_identities.append(completed_identity)
try:
pair_count, ortholog_manifest = materialize_ortholog_final_outputs(
final_dir=final_dir,
prefix=prefix,
pair_count, ortholog_manifest = materialize_ortholog_final_outputs(
final_dir=final_dir,
prefix=prefix,
dependency_owner=ortholog_owner,
run_records=run_records,
job_id=job.job_id,
workspace=ws,
@@ -12791,18 +12939,15 @@ def run_stage(
and target_string_taxon_id
and selected_execution_taxon_id == target_string_taxon_id
)
mapping_candidates = []
if not use_target_string_directly:
if scope_id:
mapping_candidates.extend([
final_dir / f"{prefix}.{sanitize_token(scope_id)}.orthologs.txt",
final_dir / f"{prefix}.{sanitize_token(scope_id)}.ortholog_pairs.txt",
])
mapping_candidates.extend([
final_dir / f"{prefix}.orthologs.txt",
final_dir / f"{prefix}.ortholog_pairs.txt",
])
mapping_file = next((p for p in mapping_candidates if p.exists() and p.stat().st_size > 0), None)
mapping_candidates = []
if not use_target_string_directly:
mapping_candidates = ortholog_mapping_candidates(
final_dir,
prefix,
dependency_owner="ppi" if ppi_owner_intent else "",
scope_id=scope_id,
)
mapping_file = next((p for p in mapping_candidates if p.exists() and p.stat().st_size > 0), None)
if mapping_file:
gene_id_file = ws / "ppi" / "analysis" / f"{prefix}.{sanitize_token(scope_id) if scope_id else 'scope'}.ppi_source_ids.txt"
mapped_source_count = build_ppi_gene_ids_from_ortholog_mapping(mapping_file, gene_id_file, ppi_selected_targets)
@@ -12987,7 +13132,7 @@ def run_stage(
if mapping_file:
effective_normalization_label = (
ppi_reference_storage_key
if ppi_owner_auto_fallback and ppi_reference_storage_key
if ppi_reference_storage_key
else ppi_reference_species or ppi_species
)
elif len(ppi_selected_targets) == 1:
@@ -13054,6 +13199,41 @@ def run_stage(
if not ppi_storage_key:
ppi_storage_key = str(ppi_species)
id_map_file = Path(job.input_dir) / ppi_storage_key / f"{ppi_storage_key}.pep_cds_idmap.tsv"
if mapping_file and not id_map_file.is_file():
reference_family = str(
(ppi_scope_data or {}).get("family")
or (prefix.split(".", 1)[1] if "." in prefix else prefix)
)
reference_pep = resolve_species_sequence_file(
ws,
final_dir,
in_dir,
ppi_scope_data or {},
ppi_storage_key,
reference_family,
"pep",
family_only=False,
)
reference_cds = resolve_species_sequence_file(
ws,
final_dir,
in_dir,
ppi_scope_data or {},
ppi_storage_key,
reference_family,
"cds",
family_only=False,
)
if reference_pep and reference_cds:
try:
_build_reference_bundle_id_map(
species_code=ppi_storage_key,
pep_path=reference_pep,
cds_path=reference_cds,
output_path=id_map_file,
)
except (OSError, UnicodeError, csv.Error) as exc:
print(f"[PPI] Could not build species-matched ID map: {exc}")
if mapping_file and not id_map_file.is_file():
return fail_ppi_execution(
f"❌ PPI projected analysis requires species-matched ID map: {id_map_file}"
@@ -13335,22 +13515,23 @@ def run_stage(
f"Target.{target_species_name}.{family}",
])
candidate_prefixes = list(dict.fromkeys(str(x) for x in candidate_prefixes if str(x)))
mapping_candidates = []
if scope_id:
for candidate_prefix in candidate_prefixes:
mapping_candidates.extend([
final_dir / f"{candidate_prefix}.{sanitize_token(scope_id)}.orthologs.txt",
final_dir / f"{candidate_prefix}.{sanitize_token(scope_id)}.ortholog_pairs.txt",
])
for candidate_prefix in candidate_prefixes:
mapping_candidates.extend([
final_dir / f"{candidate_prefix}.orthologs.txt",
final_dir / f"{candidate_prefix}.ortholog_pairs.txt",
])
mapping_candidates.append(in_dir / "ortholog_pairs.txt")
scoped_pairs_source = next((p for p in mapping_candidates if p.exists() and p.stat().st_size > 0), None)
if not scoped_pairs_source:
return "❌ KaKs scope 模式需要 Ortholog 映射文件。请先运行 Ortholog 阶段。", manifest
selected_mapping_owner = str(ortholog_mapping_owner or "").strip().lower()
if selected_mapping_owner not in {"", "ppi", "kaks"}:
return f"❌ KaKs Ortholog 映射所有者无效: {ortholog_mapping_owner}", manifest
mapping_candidates = []
for candidate_prefix in candidate_prefixes:
mapping_candidates.extend(ortholog_mapping_candidates(
final_dir,
candidate_prefix,
dependency_owner=selected_mapping_owner,
scope_id=scope_id,
))
if not selected_mapping_owner:
mapping_candidates.append(in_dir / "ortholog_pairs.txt")
scoped_pairs_source = next((p for p in mapping_candidates if p.exists() and p.stat().st_size > 0), None)
if not scoped_pairs_source:
owner_hint = f"({selected_mapping_owner} 专属)" if selected_mapping_owner else ""
return f"❌ KaKs scope 模式需要{owner_hint} Ortholog 映射文件。", manifest
ref_cds = resolve_species_sequence_file(ws, final_dir, in_dir, kaks_scope_data, ref_species, family, "cds")
ref_pep = resolve_species_sequence_file(ws, final_dir, in_dir, kaks_scope_data, ref_species, family, "pep", family_only=False)
+849 -308
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+205 -32
View File
@@ -1488,18 +1488,62 @@ def _canonicalize_species_codes_for_job_strict(job_id: str, species_codes: list[
def _reference_bundle_species_available(job_id: str, species_code: str, require_cds: bool = False) -> tuple[bool, str]:
"""Return whether a downloaded job-local reference bundle can satisfy PPI/KaKs."""
"""Return whether job-local or config-resolved reference data can satisfy PPI/KaKs."""
code = str(species_code or "").strip()
if not job_id or not code:
return False, "missing job_id/species_code"
species_dir = INPUT_ROOT / job_id / code
input_dir = INPUT_ROOT / job_id
species_dir = input_dir / code
pep_path = species_dir / f"{code}.pep.fasta"
cds_path = species_dir / f"{code}.cds.fasta"
if not pep_path.exists() or pep_path.stat().st_size <= 0:
local_pep_ready = pep_path.exists() and pep_path.stat().st_size > 0
local_cds_ready = cds_path.exists() and cds_path.stat().st_size > 0
if local_pep_ready and (not require_cds or local_cds_ready):
return True, str(species_dir)
try:
scope = build_gene_family_scope(
input_dir=input_dir,
workspace=WORKSPACE_ROOT / job_id,
prefix="",
target_species=[],
reference_species=[code],
comparative_species=[],
scope_mode="target_plus_reference",
)
species_files = scope.files.get(code, {})
def has_sequence(file_type: str) -> bool:
return any(
record.get("path")
and Path(str(record["path"])).exists()
and Path(str(record["path"])).stat().st_size > 0
for record in species_files.get(file_type, [])
)
if has_sequence("pep") and (not require_cds or has_sequence("cds")):
return True, str(input_dir)
except Exception:
pass
if not local_pep_ready:
return False, f"missing PEP: {pep_path}"
if require_cds and (not cds_path.exists() or cds_path.stat().st_size <= 0):
return False, f"missing CDS: {cds_path}"
return True, str(species_dir)
return False, f"missing CDS: {cds_path}"
def _technical_reference_bundle_lookup_code(req: Any, species_code: str) -> str:
"""Resolve the local bundle identity without changing the biological species code."""
code = str(species_code or "").strip()
storage_key = str(getattr(req, "ppiReferenceStorageKey", "") or "").strip()
if not storage_key or not bool(getattr(req, "ppiReferenceUserConfirmed", False)):
return code
stage = str(getattr(req, "stage", "") or "").strip().lower()
if stage == "ppi":
return storage_key
mapping_owner = str(getattr(req, "orthologMappingOwner", "") or "").strip().lower()
if stage == "kaks" and mapping_owner == "ppi":
return storage_key
return code
def _normalize_species_alias_for_job(value: str) -> str:
@@ -1691,6 +1735,23 @@ def _looks_like_gene_family_workflow_query(message: str) -> bool:
return any(token in text for token in ["基因家族分析", "家族分析", "完整分析", "全流程", "全部阶段", "完整流程"])
def _full_gene_family_workflow_query_text(message: str, history: list[Any]) -> str:
"""Recover the user's workflow request across the MainAgent confirmation turn."""
current = str(message or "").strip()
if _looks_like_gene_family_workflow_query(current):
return current
for item in reversed(history or []):
if not isinstance(item, dict):
continue
role = str(item.get("role") or item.get("sender") or "").strip().lower()
if role not in {"user", "human"}:
continue
text = str(item.get("content") or item.get("message") or item.get("text") or "").strip()
if _looks_like_gene_family_workflow_query(text):
return text
return ""
def _looks_like_intraspecies_synteny_query(message: str) -> bool:
text = str(message or "").lower()
return (
@@ -1994,7 +2055,7 @@ def _downstream_gene_family_stages_for_context(message: str) -> list[str]:
stages = [
"phylogeny", "motif", "domain", "structure", "combo",
"promoter", "tfbs", "synteny", "interspecies_synteny", "karyotype", "pep",
"ortholog", "kaks", "ppi",
"ppi", "kaks",
]
if any(token in text for token in ["表达", "expression", "rna"]):
stages.append("expression")
@@ -2004,7 +2065,6 @@ def _downstream_gene_family_stages_for_context(message: str) -> list[str]:
if any(token in text for token in ["种间", "比较物种", "interspecies"]):
stages.append("interspecies_synteny")
if any(token in text for token in ["kaks", "ka/ks", "选择压力"]):
stages.append("ortholog")
stages.append("kaks")
if any(token in text for token in ["ppi", "互作"]):
stages.append("ppi")
@@ -2075,6 +2135,9 @@ def _resolve_contextual_gene_family_request(
confirmed = _confirmed_subfamily_species(job_id, family)
confirmed_species = [item["species"] for item in confirmed if item.get("source") == "confirmed"]
mentioned = _extract_species_codes_from_text_for_job(job_id, text)
has_explicit_scope = bool(
mentioned or structured_targets or structured_refs or structured_comps
)
prefix_species = ""
if request_prefix and "." in request_prefix:
prefix_species = request_prefix.split(".", 1)[0].strip()
@@ -2467,7 +2530,7 @@ def _stage_detail_defaults(stage_id: str) -> Dict[str, Any]:
"interspecies_synteny": ["identify"],
"karyotype": ["identify", "synteny"],
"ortholog": ["identify"],
"kaks": ["identify", "ortholog"],
"kaks": ["identify"],
"ppi": ["identify"],
"expression": ["identify"],
}
@@ -2494,18 +2557,16 @@ FULL_GENE_FAMILY_WORKFLOW_STAGES = [
"interspecies_synteny",
"karyotype",
"pep",
"ortholog",
"kaks",
"ppi",
"kaks",
]
FORMAL_GENE_FAMILY_CORE_STAGES = {"phylogeny", "motif", "domain", "structure", "combo"}
FULL_GENE_FAMILY_TARGET_ONLY_STAGES = {"promoter", "tfbs", "synteny", "karyotype", "pep"}
FULL_GENE_FAMILY_TARGET_ONLY_STAGES = {"promoter", "tfbs", "synteny", "karyotype", "pep", "ppi"}
FULL_GENE_FAMILY_REFERENCE_STAGES = FORMAL_GENE_FAMILY_CORE_STAGES | {
"interspecies_synteny",
"ortholog",
"kaks",
"ppi",
}
SCOPE_IDENTITY_PARAM_KEYS = (
"scopeFile",
@@ -2633,6 +2694,8 @@ def _normalize_full_gene_family_workflow_plan(
if sid not in ordered:
ordered.append(sid)
for sid in stage_ids:
if sid == "ortholog":
continue
if sid and sid not in ordered:
ordered.append(sid)
@@ -2661,6 +2724,8 @@ def _normalize_full_gene_family_workflow_plan(
detail["dependencies"] = ["phylogeny", "motif", "domain", "structure"]
detail_params["comboPanels"] = ["motif", "domain", "structure"]
detail_params["comboPanelSource"] = "regular_gene_family"
if sid in {"ppi", "kaks"}:
detail["dependencies"] = ["identify"]
if sid in GENE_FAMILY_SCOPE_STAGES:
detail["params"] = enrich_stage_params_with_context(sid, plan_prefix, detail_params)
else:
@@ -4681,7 +4746,7 @@ def _celery_start_script_for_current_profile() -> Path:
port = os.environ.get("PLANTAI_WEB_PORT", "").strip()
if prefix.endswith("_9000") or redis_url.endswith("/10") or port == "9000":
return PROJECT_ROOT / "start_celery_9000.sh"
return PROJECT_ROOT / "start_celery_7880.sh"
return PROJECT_ROOT / "scripts" / "legacy_startup" / "start_celery_7880.sh"
def _current_celery_profile() -> Dict[str, Any]:
@@ -4691,7 +4756,11 @@ def _current_celery_profile() -> Dict[str, Any]:
prefix = get_queue_prefix()
except Exception:
prefix = os.environ.get("BBAV5_QUEUE_PREFIX", "plantai_dev_7880").strip() or "plantai_dev_7880"
redis_default = "redis://localhost:6379/10" if prefix.endswith("_9000") or os.environ.get("PLANTAI_WEB_PORT", "").strip() == "9000" else "redis://localhost:6379/9"
current_port = os.environ.get("PLANTAI_WEB_PORT", "").strip()
if prefix.endswith("_9000") or current_port == "9000":
redis_default = "redis://localhost:6379/10"
else:
redis_default = "redis://localhost:6379/9"
redis_url = os.environ.get("BBAV5_REDIS_URL", redis_default).strip() or redis_default
script = _celery_start_script_for_current_profile()
log_dir = Path(os.environ.get("BBAV5_CELERY_LOG_DIR", "") or f"logs/celery_{prefix}")
@@ -5181,6 +5250,7 @@ def _prepare_execute_technical_reference_scope(
stage == "ortholog"
and str(getattr(req, "dependencyOwner", "") or "").strip().lower() == "ppi"
)
ppi_consumer_intent = stage == "ppi"
if ppi_owner_intent:
missing_fields = []
if not bool(getattr(req, "ppiReferenceUserConfirmed", False)):
@@ -5201,6 +5271,8 @@ def _prepare_execute_technical_reference_scope(
)
ppi_owned_ortholog = ppi_owner_intent
if ppi_owned_ortholog:
req.analysisRoute = "single_species"
req.memberSetId = ""
technical_reference_species = [
str(getattr(req, "ppiReferenceStorageKey", "") or "").strip()
]
@@ -5259,12 +5331,22 @@ def _prepare_execute_technical_reference_scope(
scope_params["scope_id"] = original_scope_id
scope_params["refSpeciesList"] = list(identity.reference_species)
scope_params["_validatedPpiOwnerScopeId"] = original_scope_id
req.memberSetId = f"{original_scope_id}:single_species:all_members"
else:
scope_params["scopeMode"] = "target_only"
scope_params["referenceSpecies"] = []
scope_params["refSpeciesList"] = []
scope_params["comparativeSpecies"] = []
scope_params["scopeFile"] = ""
elif ppi_consumer_intent:
req.analysisRoute = "single_species"
req.memberSetId = ""
technical_reference_species = []
scope_params["scopeMode"] = "target_only"
scope_params["referenceSpecies"] = []
scope_params["refSpeciesList"] = []
scope_params["comparativeSpecies"] = []
scope_params["scopeFile"] = ""
else:
technical_reference_species = []
if stage in {"ortholog", "ppi", "kaks"}:
@@ -6906,6 +6988,7 @@ class ExecuteRequest(BaseModel):
ppiReferenceSelectionMode: str = ""
ppiReferenceUserConfirmed: bool = False
dependencyOwner: str = ""
orthologMappingOwner: str = ""
identifyBlastReferenceSpecies: List[str] = []
primaryReferenceSpecies: List[str] = []
refSpeciesList: List[str] = []
@@ -7818,6 +7901,7 @@ def run_stage_sync(task_id: str, stage: str, job_id: str, params: dict):
ppi_reference_selection_mode=params.get("ppiReferenceSelectionMode") or params.get("ppi_reference_selection_mode") or "",
ppi_reference_user_confirmed=_bool_param("ppiReferenceUserConfirmed", "ppi_reference_user_confirmed", default=False),
dependency_owner=params.get("dependencyOwner") or params.get("dependency_owner") or "",
ortholog_mapping_owner=params.get("orthologMappingOwner") or params.get("ortholog_mapping_owner") or "",
ppi_score=params.get("ppiScore", 200),
# KaKs参数
kaks_neutral_rate=params.get("kaksNeutralRate", 7.08e-9),
@@ -8567,7 +8651,7 @@ def run_stage_sync(task_id: str, stage: str, job_id: str, params: dict):
async def root():
"""返回Vue3前端页面"""
return FileResponse(
Path(__file__).parent / "index.html",
Path(__file__).parent / "index_ml.html",
headers={
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
@@ -11668,7 +11752,12 @@ async def execute_stage(req: ExecuteRequest):
for raw_value in raw_values:
raw_code = str(raw_value or "").strip()
if raw_code and raw_code.lower() in technical_ref_lookup:
available, reason = _reference_bundle_species_available(job_id, raw_code, require_cds=require_cds)
bundle_lookup_code = _technical_reference_bundle_lookup_code(req, raw_code)
available, reason = _reference_bundle_species_available(
job_id,
bundle_lookup_code,
require_cds=require_cds,
)
if not available:
raise ValueError(
f"PPI/KaKs 技术参考物种 {raw_code} 已选择但参考包不完整:{reason};"
@@ -12368,6 +12457,7 @@ async def execute_stage(req: ExecuteRequest):
"ppiScore": getattr(req, 'ppiScore', 200),
# KaKs参数
"kaksNeutralRate": getattr(req, 'kaksNeutralRate', 7.08e-9),
"orthologMappingOwner": getattr(req, 'orthologMappingOwner', ''),
# Genome Prepare参数
"genomePrepareType": getattr(req, 'genomePrepareType', 'auto'),
# Interspecies species selection
@@ -15567,7 +15657,7 @@ def _apply_reference_bundle_stale_watchdog(task: Dict[str, Any]) -> None:
progress = task.get("progress") if isinstance(task.get("progress"), dict) else {}
progress = {
"phase": progress.get("phase") or status or "running",
"message": progress.get("message") or task.get("message") or "参考物种注释包仍在后台下载",
"message": progress.get("message") or task.get("message") or "PPI and Ka/Ks reference species data are still downloading",
"percentage": progress.get("percentage"),
"indeterminate": progress.get("indeterminate", True),
"pending_seconds": task["pending_seconds"],
@@ -15585,13 +15675,13 @@ def _apply_reference_bundle_stale_watchdog(task: Dict[str, Any]) -> None:
def run_reference_bundle_download_task(task_id: str, payload: Dict[str, Any]) -> None:
tasks[task_id].update({
"status": "running",
"message": "正在后台下载参考物种注释包",
"message": "Downloading PPI and Ka/Ks reference species PEP and CDS",
"start_time": datetime.now().isoformat(),
"timeout_sec": int(payload.get("timeout_sec") or 300),
"payload": dict(payload),
"progress": {
"phase": "queued",
"message": "参考物种注释包下载任务已启动",
"message": "PPI and Ka/Ks reference species data download started",
"percentage": 0,
"indeterminate": True,
},
@@ -15622,7 +15712,7 @@ def run_reference_bundle_download_task(task_id: str, payload: Dict[str, Any]) ->
progress.setdefault("indeterminate", percentage is None)
tasks[task_id].update({
"status": "running",
"message": progress.get("message") or "正在后台下载参考物种注释包",
"message": progress.get("message") or "Downloading PPI and Ka/Ks reference species PEP and CDS",
"progress": progress,
})
_persist_reference_bundle_task(tasks[task_id])
@@ -15641,11 +15731,11 @@ def run_reference_bundle_download_task(task_id: str, payload: Dict[str, Any]) ->
success = result.get("status") == "success"
tasks[task_id].update({
"status": "success" if success else "failed",
"message": "参考物种注释包下载完成" if success else "参考物种注释包下载未满足必需文件",
"message": "PPI and Ka/Ks reference species data are ready" if success else "PPI and Ka/Ks reference species data are incomplete",
"end_time": datetime.now().isoformat(),
"progress": {
"phase": "completed" if success else "failed",
"message": "参考物种注释包下载完成" if success else "参考物种注释包下载未满足必需文件",
"message": "PPI and Ka/Ks reference species data are ready" if success else "PPI and Ka/Ks reference species data are incomplete",
"percentage": 100 if success else None,
"indeterminate": False if success else True,
},
@@ -15688,7 +15778,7 @@ async def download_reference_bundle_api(req: ReferenceBundleDownloadRequest):
"stage": "reference_bundle_download",
"job_id": req.job_id,
"status": "pending",
"message": "参考物种注释包下载任务已提交",
"message": "PPI and Ka/Ks reference species data download submitted",
"created_at": datetime.now().isoformat(),
"species_code": req.speciesCode,
"species_name": req.speciesName,
@@ -17150,7 +17240,78 @@ def _gene_family_report_plan_context(job_id: str, prefix: str = "") -> Optional[
"rnaseqScopeId": "",
}
reports.append((path.stat().st_mtime, context))
return max(reports, key=lambda item: item[0])[1] if reports else None
if reports:
return max(reports, key=lambda item: item[0])[1]
# Older jobs may have complete, successful stage manifests but no saved
# confirmation-plan snapshot. Reconstruct a deterministic report plan from
# one manifest prefix rather than rejecting those verifiable results.
report_stage_order = (
"identify",
"phylogeny",
"reference_classification",
"motif",
"domain",
"structure",
"combo",
"promoter",
"tfbs",
"synteny",
"karyotype",
"interspecies_synteny",
"pep",
)
report_stage_set = set(report_stage_order)
recovered_by_prefix: Dict[str, Dict[str, tuple[float, Dict[str, Any]]]] = {}
for path in final_dir.glob("*.manifest.json") if final_dir.is_dir() else ():
try:
manifest = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
continue
stage = str(manifest.get("stage") or "").strip()
manifest_prefix = str(
manifest.get("prefix")
or (manifest.get("parameters") or {}).get("prefix")
or ""
).strip()
if (
stage not in report_stage_set
or str(manifest.get("status") or "") != "success"
or not manifest_prefix
or (requested_prefix and manifest_prefix != requested_prefix)
):
continue
stage_map = recovered_by_prefix.setdefault(manifest_prefix, {})
candidate = (path.stat().st_mtime, manifest)
if stage not in stage_map or candidate[0] > stage_map[stage][0]:
stage_map[stage] = candidate
if not recovered_by_prefix:
return None
def recovered_rank(item: tuple[str, Dict[str, tuple[float, Dict[str, Any]]]]) -> tuple[int, int, float, str]:
manifest_prefix, stage_map = item
latest_mtime = max((entry[0] for entry in stage_map.values()), default=0.0)
return (
1 if "identify" in stage_map else 0,
len(stage_map),
latest_mtime,
manifest_prefix,
)
recovered_prefix, recovered_stages = max(recovered_by_prefix.items(), key=recovered_rank)
planned = [stage for stage in report_stage_order if stage in recovered_stages]
if not planned:
return None
return {
"prefix": recovered_prefix,
"expectedStages": planned,
"stageStatuses": {stage: "success" for stage in planned},
"scopeId": "",
"analysisRoute": "",
"memberSetId": "",
"rnaseqScopeId": "",
}
async def _run_agent_chat(
@@ -17692,7 +17853,11 @@ async def _run_agent_chat(
else None
),
"handoff_request": (
str((outcome.get("proposal") or {}).get("request") or "").strip()
str(
(outcome.get("proposal") or {}).get("original_request")
or (outcome.get("proposal") or {}).get("request")
or ""
).strip()
if isinstance(outcome.get("proposal"), dict)
else request.message
) or request.message,
@@ -18890,9 +19055,13 @@ def _build_contextual_gene_family_plan_response(request: Any, session_id: str, c
detail["params"] = enrich_stage_params_with_context(stage_id, plan_prefix, detail_params)
stages_detail.append(detail)
if _looks_like_gene_family_workflow_query(getattr(request, "message", "") or "") and not subfamily_prerequisite_mode:
full_workflow_query = _full_gene_family_workflow_query_text(
getattr(request, "message", "") or "",
getattr(request, "history", []) or [],
)
if full_workflow_query and not subfamily_prerequisite_mode:
stage_ids, stages_detail = _normalize_full_gene_family_workflow_plan(
message=getattr(request, "message", "") or "",
message=full_workflow_query,
stage_ids=stage_ids,
stages_detail=stages_detail,
plan_params=plan_params,
@@ -22697,9 +22866,13 @@ async def agent_plan(request: AgentPlanRequest):
intent["gene_family"] = combo_family
full_workflow_normalized = False
if _looks_like_gene_family_workflow_query(request.message or "") and not subfamily_prediction_query:
full_workflow_query = _full_gene_family_workflow_query_text(
request.message or "",
request.history or [],
)
if full_workflow_query and not subfamily_prediction_query:
stage_ids, stages_detail = _normalize_full_gene_family_workflow_plan(
message=request.message or "",
message=full_workflow_query,
stage_ids=stage_ids,
stages_detail=stages_detail,
plan_params=plan_params,
@@ -22731,7 +22904,7 @@ async def agent_plan(request: AgentPlanRequest):
if full_workflow_normalized:
stage_ids, stages_detail = _normalize_full_gene_family_workflow_plan(
message=request.message or "",
message=full_workflow_query,
stage_ids=stage_ids,
stages_detail=stages_detail,
plan_params=plan_params,
@@ -25338,7 +25511,7 @@ async def root():
"""
返回前端首页
"""
index_file = WEB_DIR / "index.html"
index_file = WEB_DIR / "index_ml.html"
if index_file.exists():
return FileResponse(
str(index_file),