Merge remote-tracking branch 'origin/master'

# Conflicts:
#	scripts/legacy_startup/restart_with_prompts.sh
#	scripts/legacy_startup/start-webui.sh
#	scripts/legacy_startup/start_celery.sh
#	scripts/legacy_startup/start_celery_7880.sh
#	scripts/legacy_startup/start_celery_port9000.sh
This commit is contained in:
sj
2026-08-21 11:06:22 +08:00
70 changed files with 10505 additions and 1184 deletions
+234 -117
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,
@@ -6026,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",
@@ -12514,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}. "
@@ -12574,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(
@@ -12588,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,
@@ -12679,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,
@@ -12855,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)
@@ -13051,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:
@@ -13118,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}"
@@ -13399,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)
+907 -309
View File
File diff suppressed because it is too large Load Diff
+1026 -350
View File
File diff suppressed because it is too large Load Diff
+30 -5
View File
@@ -124,6 +124,17 @@
};
}
async function extractEnvDeclaration(files) {
for (const file of files) {
const path = normalizedRelativePath(file).toLowerCase();
if (!path.endsWith('.sh')) continue;
const content = await file.text();
const m = content.match(/^\s*#\s*lightweight-env\s*:\s*([A-Za-z0-9_.-]+)/m);
if (m) return m[1];
}
return null;
}
async function encodeBase64(file) {
const buffer = await file.arrayBuffer();
const bytes = new Uint8Array(buffer);
@@ -146,11 +157,12 @@
payload.files.push(await encodeBase64(file));
payload.relative_paths.push(normalizedRelativePath(file));
}
const response = await fetch('/api/job/import-folder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const response = await hooks.uploadExtensionWithProgress(
'/api/job/import-folder',
JSON.stringify(payload),
'正在镜像导入 Job',
{ 'Content-Type': 'application/json' },
);
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.detail || data.error || data.message || 'Job 导入失败');
@@ -160,6 +172,8 @@
async function resolveRuntimeEnv(files) {
const dependencies = await extractDependencies(files);
const envDeclaration = await extractEnvDeclaration(files);
if (envDeclaration) dependencies.env_declaration = envDeclaration;
console.log('[lightWeightExtensionTest] 提取的依赖:', JSON.stringify(dependencies), '| 扫描文件数:', files.length);
try {
const response = await fetch('/api/extension-stages/envs/resolve', {
@@ -268,12 +282,23 @@
function mountApi() {
window.LightWeightExtensionTest = {
async handleDrop(hooks, event) {
const progress = hooks.lightweightUploadProgress;
try {
if (progress) {
progress.active = true;
progress.percent = 0;
progress.loaded = 0;
progress.total = 0;
progress.speed = 0;
progress.label = '本地文件读取中';
}
hooks.lightweightExtensionTestStatus.value = '本地文件读取中...';
const files = await readDroppedFiles(event?.dataTransfer);
await processFolderFiles(hooks, files);
} catch (error) {
const message = `失败:${error.message || '目录处理失败'}`;
hooks.lightweightExtensionTestStatus.value = message;
if (progress) progress.active = false;
notifyError(message);
}
},
+236 -46
View File
@@ -45,6 +45,7 @@ PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
DEFAULT_RUNTIME_ENV = os.environ.get("BBAV5_DEFAULT_RUNTIME_ENV", "test4py311")
sys.path.insert(0, str(PROJECT_ROOT))
from gfa_core.project_paths import get_conda_env_bin, get_conda_env_rscript, get_configured_path, get_wgs_bcftools, get_wgs_java, get_wgs_samtools
from gfa_core.mas import confirmation_snapshot as _confirmation_snapshot
ASAW_BIN = get_conda_env_bin("ASAW")
ASAW_RSCRIPT = get_conda_env_rscript("ASAW")
@@ -705,9 +706,10 @@ def _confirmed_config_species_scope(job_id: str) -> Dict[str, list[str]]:
if not job_id:
return {}
try:
from gfa_core.mas.confirmation_snapshot import build_confirmation_snapshot
confirmation = build_confirmation_snapshot(PROJECT_ROOT, job_id)
confirmation = _confirmation_snapshot.build_confirmation_snapshot(
PROJECT_ROOT,
job_id,
)
except (OSError, ValueError):
return {}
if not confirmation.get("config_confirmed"):
@@ -880,7 +882,8 @@ def _species_role_conflict_message(conflicts: list[Dict[str, str]]) -> str:
return (
"检测到本轮请求与项目物种角色不一致:"
+ detail_text
+ "。系统已阻止执行,请按 config.xlsx 中已确认的角色重新描述分析范围。"
+ "。系统已阻止执行,请按 config.xlsx 中已确认的角色重新描述分析范围;"
"如需修改项目物种角色,请发送“打开 config.xlsx”。"
)
@@ -1090,6 +1093,8 @@ def _hydrate_goal_terms_from_current_job(
return hydrated
config_scope = _confirmed_config_species_scope(job_id)
if not config_scope and (INPUT_ROOT / job_id / "config.xlsx").is_file():
config_scope = _configured_species_scope(job_id)
for config_key, term_key in (
("targetSpecies", "target_species"),
("referenceSpecies", "reference_species"),
@@ -1483,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:
@@ -1686,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 (
@@ -1989,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")
@@ -1999,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")
@@ -2070,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()
@@ -2462,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"],
}
@@ -2489,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",
@@ -2628,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)
@@ -2656,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:
@@ -4989,12 +5059,6 @@ orchestrator = None
langgraph_orchestrator = None
if MAS_AVAILABLE:
try:
# 清除Python模块缓存,确保配置重新加载
import sys
modules_to_remove = [k for k in sys.modules.keys() if 'gfa_core.mas' in k]
for m in modules_to_remove:
del sys.modules[m]
# 加载MAS配置
from gfa_core.mas.config import config as mas_config
llm_config = mas_config.load_llm_config()
@@ -5186,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)):
@@ -5206,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()
]
@@ -5264,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"}:
@@ -6911,6 +6988,7 @@ class ExecuteRequest(BaseModel):
ppiReferenceSelectionMode: str = ""
ppiReferenceUserConfirmed: bool = False
dependencyOwner: str = ""
orthologMappingOwner: str = ""
identifyBlastReferenceSpecies: List[str] = []
primaryReferenceSpecies: List[str] = []
refSpeciesList: List[str] = []
@@ -7823,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),
@@ -8924,6 +9003,7 @@ class ExtensionStageResolveIntentRequest(BaseModel):
class ExtensionStageEnvResolveRequest(BaseModel):
python_imports: List[str] = Field(default_factory=list)
r_packages: List[str] = Field(default_factory=list)
env_declaration: Optional[str] = None
@@ -9020,6 +9100,24 @@ async def _r_packages_for_env(env_name: str) -> set[str]:
@app.post("/api/extension-stages/envs/resolve")
async def resolve_extension_stage_env(req: ExtensionStageEnvResolveRequest):
# 工具显式声明的环境优先(run.sh 里 # lightweight-env: <env>)
if req.env_declaration:
env = req.env_declaration.strip()
proc = await asyncio.create_subprocess_exec(
"conda", "run", "-n", env, "python", "--version",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
await proc.communicate()
if proc.returncode == 0:
return {
"success": True,
"runtime_env": env,
"reason": "explicit_env_declaration",
"dependency": env,
"fallback": False,
}
python_imports = _normalized_dependency_names(req.python_imports)
r_packages = _normalized_dependency_names(req.r_packages)
if not python_imports and not r_packages:
@@ -11654,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};"
@@ -12354,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
@@ -15553,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"],
@@ -15571,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,
},
@@ -15608,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])
@@ -15627,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,
},
@@ -15674,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,
@@ -17221,11 +17325,6 @@ async def _run_agent_chat(
executes analysis stages here.
"""
session_id = request.session_id or f"session_{int(time.time())}"
# Inject language instruction for non-Chinese languages
if request.language and request.language != DEFAULT_AGENT_LANG:
lang_map = {"en": "Please respond in English."}
lang_instruction = lang_map.get(request.language, f"Please respond in {request.language}.")
request.message = f"{lang_instruction}\n\n{request.message}"
completed_stages: list[str] = []
if request.job_id:
final_dir = PROJECT_ROOT / "workspace" / request.job_id / "final"
@@ -17722,6 +17821,11 @@ async def _run_agent_chat(
if isinstance(outcome.get("proposal"), dict)
else None
),
recommendation=(
outcome.get("recommendation")
if isinstance(outcome.get("recommendation"), dict)
else None
),
)
return {
@@ -17743,8 +17847,17 @@ async def _run_agent_chat(
"model_used": request.model,
"session_id": session_id,
"input_paths": list(outcome.get("input_paths") or []),
"recommendation": (
outcome.get("recommendation")
if isinstance(outcome.get("recommendation"), dict)
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,
@@ -18624,6 +18737,41 @@ class AgentPlanRequest(BaseModel):
language: str = DEFAULT_AGENT_LANG # "zh" or "en"
def _authorized_workflow_action_stages(intent: Dict[str, Any]) -> List[str]:
candidate = intent or {}
params = candidate.get("params") or {}
action_id = str(
candidate.get("workflow_action_id") or params.get("workflowActionId") or ""
).strip()
authorized = list(dict.fromkeys(_param_list(params.get("authorizedStages"))))
declared = set(_param_list(candidate.get("stages")))
if not action_id or not authorized or not set(authorized).issubset(declared):
return []
try:
from gfa_core.mas.workflow_catalog import load_canonical_workflow_contract
contract = load_canonical_workflow_contract("gene_family")
transition = next(item for item in contract.transitions if item.id == action_id)
actions = {item.id: item for item in contract.actions}
evidence_stages = {
item.id: item.stage for item in contract.evidence_requirements
}
contract_stages = [
evidence_stages[requirement_id]
for requirement_id in transition.evidence_requirement_ids
]
contract_stages.append(actions[transition.action_id].target_stage)
except (ImportError, StopIteration, KeyError):
return []
if set(authorized) != set(contract_stages):
return []
return [
stage
for stage in SUBFAMILY_PREDICTION_PLAN_STAGES
if stage in set(contract_stages)
]
SUBFAMILY_PREDICTION_PLAN_STAGES = [
"identify",
"phylogeny",
@@ -18907,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,
@@ -20711,6 +20863,7 @@ async def agent_plan(request: AgentPlanRequest):
request.intent = dict(request.intent or {})
request.intent["params"] = dict(request.intent.get("params") or {})
_apply_reconciled_species_to_intent(request.intent, plan_role_resolution)
authorized_workflow_stages = _authorized_workflow_action_stages(request.intent)
query_lower_for_context = (request.message or "").lower()
result_report_query = (
"结果报告" in query_lower_for_context
@@ -20780,7 +20933,7 @@ async def agent_plan(request: AgentPlanRequest):
and not _looks_like_gene_family_workflow_query(query_lower_for_context)
and not _requires_confirmed_subfamily_query(query_lower_for_context)
)
if subfamily_prediction_query_for_context:
if subfamily_prediction_query_for_context or authorized_workflow_stages:
requested_prefix_for_subfamily = request.prefix or ""
prefix_species, prefix_family = _prefix_species_family(requested_prefix_for_subfamily)
family_for_subfamily = _resolve_authoritative_gene_family(
@@ -20843,7 +20996,27 @@ async def agent_plan(request: AgentPlanRequest):
}
if family_for_subfamily and targets_for_subfamily:
prefix_for_subfamily = f"{targets_for_subfamily[0]}.{family_for_subfamily}" if len(targets_for_subfamily) == 1 else f"Target.{family_for_subfamily}"
deterministic_stages = ["identify", "phylogeny", "motif", "domain", "structure", "combo", "reference_classification"]
deterministic_stages = authorized_workflow_stages or [
"identify",
"phylogeny",
"motif",
"domain",
"structure",
"combo",
"reference_classification",
]
authorized_action_id = str(
(request.intent or {}).get("workflow_action_id")
or ((request.intent or {}).get("params") or {}).get(
"workflowActionId"
)
or ""
).strip()
plan_description = (
f"执行已确认的工作流业务动作 {authorized_action_id}"
if authorized_workflow_stages
else "执行 target/comparative + reference 亚家族分类"
)
classification_species_for_subfamily = list(targets_for_subfamily)
classification_species_for_subfamily, role_targets_for_subfamily, role_comps_for_subfamily = _split_subfamily_selected_species_by_config_roles(
request.job_id,
@@ -20865,6 +21038,12 @@ async def agent_plan(request: AgentPlanRequest):
"confirmPrefixBySpecies": confirm_prefix_by_species,
"subfamilyState": subfamily_state_for_plan,
}
if authorized_workflow_stages:
request_action_params = (request.intent or {}).get("params") or {}
plan_params["workflowActionId"] = authorized_action_id
plan_params["authorizedStages"] = _param_list(
request_action_params.get("authorizedStages")
)
if identify_selection_for_subfamily:
plan_params["identifySpeciesSelection"] = _identify_selection_with_default(
identify_selection_for_subfamily,
@@ -20934,7 +21113,7 @@ async def agent_plan(request: AgentPlanRequest):
"comparative_species": role_comps_for_subfamily,
"scopeMode": plan_params["scopeMode"],
"prefix": prefix_for_subfamily,
"description": "执行 target/comparative + reference 亚家族分类",
"description": plan_description,
"estimated_time": f"{estimated_minutes}分钟",
"estimated_total_time": f"{estimated_minutes}分钟",
"execution_strategy": "parallel",
@@ -20956,10 +21135,15 @@ async def agent_plan(request: AgentPlanRequest):
"comparative_species": role_comps_for_subfamily,
"scopeMode": plan_params["scopeMode"],
"stages": deterministic_stages,
"description": "执行目标物种+参考物种亚家族分类",
"description": plan_description,
"confidence": 0.98,
"prefix": prefix_for_subfamily,
"params": plan_params,
**(
{"workflow_action_id": authorized_action_id}
if authorized_action_id
else {}
),
},
"completed_stages": completed_stages,
"session_id": session_id,
@@ -22682,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,
@@ -22716,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,
@@ -22951,6 +23139,7 @@ class AgentRunRequest(BaseModel):
job_id: str = ""
intent: dict = {} # 前端可传入已有的intent
stages: list = [] # 要执行的阶段列表
language: str = DEFAULT_AGENT_LANG # "zh" or "en"
@app.post("/api/agent/run")
@@ -22984,6 +23173,7 @@ async def agent_run(request: AgentRunRequest):
)
# 添加额外的运行时字段
initial_state["language"] = request.language
initial_state["status"] = "running"
initial_state["intent_result"] = request.intent or {} # 传入已有的 intent
initial_state["completed_stages"] = []
+175 -1
View File
@@ -712,5 +712,179 @@ window.__I18N_EN__ = {
" 已暂停依赖 confirmed_subfamily.tsv": " has paused stages depending on confirmed_subfamily.tsv",
";": ";",
" 个分析阶段": " analysis stages",
" 个阶段": " stages"
" 个阶段": " stages",
// ===== 阶段 7:ElMessage/ElMessageBox i18n keys =====
// 注意:key 为中文原文(zh 模式直接返回 key),value 为 English
// tp() 类 key 需要同时注册变量 placeholder 版本和 static 文案
// ElMessageBox
"删除会话": "Delete Session",
"取消 PPI": "Cancel PPI",
"自动注释并继续": "Auto-annotate & Continue",
"我先去上传背景文件": "Upload Background Files First",
"确认设计并写入": "Confirm Design & Write",
"返回修改": "Return to Modify",
"确认第一候选": "Confirm First Candidate",
"使用 bcftools 重试": "Retry with bcftools",
"GATK 执行失败": "GATK Execution Failed",
"确认 DE 分析设计": "Confirm DE Analysis Design",
"确认 PPI 的 STRING 近源参考物种": "Confirm STRING Close Reference Species for PPI",
// autoAnnotation 标题
"GO/KEGG 富集背景缺失": "GO/KEGG Enrichment Background Missing",
"KEGG 富集背景缺失": "KEGG Enrichment Background Missing",
"GO 富集背景缺失": "GO Enrichment Background Missing",
"RNA-seq 富集背景缺失": "RNA-seq Enrichment Background Missing",
// 会话管理
"示例下载功能当前不可用": "Example download is currently unavailable",
"已Clear当前会话": "Current session cleared",
// autoFill
"请至少填写一个要预览修改的字段": "Please fill at least one field to preview modifications",
"没有匹配的草稿行": "No matching draft rows",
"请先预览要应用的修改": "Please preview modifications before applying",
"请先生成或加载草稿": "Please generate or load a draft first",
"生成草稿失败": "Failed to generate draft",
"确认失败": "Confirmation failed",
"已写入正式 config.xlsx": "Formal config.xlsx written",
"已写入正式 samples.yaml": "Formal samples.yaml written",
"请输入文件路径": "Please enter file path",
"加载失败": "Load failed",
// RAG 解读
"请求解读服务失败": "Interpretation service request failed",
"RAG解读已刷新": "RAG interpretation refreshed",
"RAG解读刷新失败": "RAG interpretation refresh failed",
"下载报告失败": "Failed to download report",
"JSON数据已加载到控制台": "JSON data loaded to console",
"解读内容已复制": "Interpretation content copied",
"复制失败": "Copy failed",
// 知识图谱
"知识图谱加载完成": "Knowledge graph loaded",
"未找到匹配的节点": "No matching node found",
"视图已重置": "View reset",
"请先通过问答加载子图,或点击「重新加载」": "Please load subgraph via Q&A first, or click Reload",
// PPI/STRING
"当前没有可用 job_id": "No job_id currently available",
"该 ID 已存在": "This ID already exists",
"至少保留一个 gene ID": "Keep at least one gene ID",
// Working Directory
"切换失败": "Switch failed",
// 图片/可视化
"请先选择一张图片": "Please select an image first",
"无法确定启动子输出前缀。": "Cannot determine promoter output prefix.",
"请至少选择一个启动子元件或类别。": "Please select at least one promoter element or category.",
"请至少选择一种图类型。": "Please select at least one plot type.",
"无法确定进化树输出标签,请先选择树图或填写输出标签。": "Cannot determine phylogeny tree output label. Please select a tree image or fill in the output label first.",
"进化树图重绘完成,已切换到新图": "Phylogeny tree replot complete; switched to new image",
"无法确定种内共线性输出标签,请先选择 Circos 图。": "Cannot determine intraspecies synteny output label. Please select a Circos image first.",
"无法确定 combo 输出标签,请先选择 combo 图。": "Cannot determine combo output label. Please select a combo image first.",
"Combo 修订至少选择两个类型。": "Combo revision requires at least two panel types.",
"Combo 图修订完成,已切换到新图": "Combo plot revision complete; switched to new image",
"无法确定MSA输出标签,请先选择对应图片或填写输出标签。": "Cannot determine MSA output label. Please select corresponding image or fill in output label.",
"请填写MSA alignment 起始列和终止列,或选择其他模式。": "Please fill in MSA alignment start/end columns or choose another mode.",
"请填写参考基因及其起止残基编号。": "Please fill in reference gene and its start/end residue numbers.",
// GWAS/诊断
"请填写 GWAS 物种简称(用于结果前缀)": "Please fill in GWAS species abbreviation (for result prefix)",
"请先选择有效 VCF": "Please select a valid VCF first",
"请先SelectAnalysis Stage": "Please select an analysis stage first",
"诊断未发现明确问题,请查看阶段错误日志。": "Diagnosis found no clear issues. Please check stage error logs.",
"诊断仅提示信息,无需确认修复。": "Diagnosis is informational only; no fix confirmation needed.",
"✅ 已应用修复方案,继续执行...": "✅ Fix applied; continuing execution...",
"❌ 应用修复失败": "❌ Fix application failed",
"❌ 网络错误,请重试": "❌ Network error; please retry",
"⏸ 已跳过修复;该问题会阻断下游,后续阶段已暂停。": "⏸ Fix skipped; issue blocks downstream; subsequent stages paused.",
"已关闭诊断提示。": "Diagnosis prompt closed.",
// 任务/消息
"当前消息未成功写入会话历史,刷新后可能丢失": "Message not persisted to session history; may be lost on refresh",
"路径已复制": "Path copied",
// 拓展分析
"当前筛选没有成员,未重建 final_members。": "No members matching current filter; final_members not rebuilt.",
"当前选择为按物种分别分析,但后端没有返回完整的分物种亚家族结果;将尝试打开可用的联合结果。": "Per-species analysis selected but backend returned incomplete results; attempting to open available joint results.",
"亚家族预测已完成,但未能自动打开确认表;请发送\"调出亚家族确认表\"手动打开。": "Subfamily prediction completed but confirmation table could not open automatically; please send \"show subfamily review table\" manually.",
"没有可确认的亚家族标签。": "No subfamily labels to confirm.",
"已请求停止当前拓展分析;状态仍会继续轮询直到进程退出。": "Stop requested for current extension; polling will continue until process exits.",
"No running task to stop": "No running task to stop",
"Task(s) stopped (backend + Celery when available)": "Task(s) stopped (backend + Celery when available)",
"File uploaded successfully": "File uploaded successfully",
"未检测到可用文件": "No usable files detected",
"请先生成拓展分析阶段。": "Please generate the extension stage first.",
"发布前验收已通过": "Pre-publish validation passed",
"请填写名称,并上传原始代码。": "Please provide name and upload source code.",
"拓展分析阶段已生成并发布": "Extension stage generated and published",
"确认已保存": "Confirmation saved",
"验证成功": "Validation successful",
"验证未通过": "Validation not passed",
"拓展分析阶段已发布": "Extension stage published",
"拓展分析阶段执行完成": "Extension stage execution complete",
"拓展分析阶段执行未成功,请查看结果日志": "Extension stage execution incomplete; please check result logs",
"该阶段已在正式列表中": "This stage is already in the formal list",
"已加入正式阶段列表,并默认纳入本次执行": "Added to formal stage list and included in this execution",
// 方法学
"请先选择 PDF 或 MinerU content_list JSON 文献文件": "Please select a PDF or MinerU content_list JSON literature file first",
"方法学提取启动失败": "Methodology extraction failed to start",
"方法学 sandbox 示例读取失败": "Failed to read methodology sandbox examples",
"请先选择或输入 methodology_dir": "Please select or enter methodology_dir first",
"方法学 sandbox 已构建": "Methodology sandbox built",
"请先构建临时库": "Please build the sandbox first",
"请输入方法学问题": "Please enter methodology question",
"方法学 sandbox 查询失败": "Methodology sandbox query failed",
"方法学方案已保存,可先准备数据,稍后再按该方案执行": "Methodology plan saved; you may prepare data first and execute later",
"方法学方案保存失败": "Failed to save methodology plan",
"当前已有批量任务在执行,请等待完成后再提交方法学流程": "A batch task is currently running; please wait for completion before submitting methodology workflow",
"请至少选择一个可执行的 bbav5 阶段": "Please select at least one executable bbav5 stage",
"亚家族参考物种未写入当前 job 配置,已生成方法学本地计划,请在批量确认页核对参考物种": "Subfamily reference species not written to current job config; local methodology plan generated. Please verify reference species on batch confirmation page.",
"方法学执行规划失败": "Methodology execution planning failed",
// Other
"请Input内容": "Please enter content",
"请先选择或创建 Working Directory": "Please select or create a Working Directory first",
"规划请求失败": "Plan request failed",
"重新绘图功能开发中...": "Replot feature is under development...",
// ===== tp() keys — 使用完整中文模板作为 key =====
"确定删除会话「{title}」吗?该操作会从列表中移除,但保留后台记录。": "Delete session \"{title}\"? This removes it from the list but keeps backend records.",
"切换到: {title}": "Switched to: {title}",
"预览 {rows} 行:{fields}。确认无误后点击 Apply。": "Preview {rows} rows: {fields}. Click Apply once confirmed.",
"已应用 {rows} 行修改。": "Applied {rows} row modifications.",
"已取消预览,表格已恢复到预览前。": "Preview cancelled, table restored.",
"链特异性为必填:还有 {count} 个样本未填写({examples})": "Library strandness required: {count} sample(s) unfilled ({examples})",
"设计提示:{error};将保留原流程的兼容处理。": "Design tip: {error}; keeping original compatible handling.",
"已加载 {count} files": "Loaded {count} files",
"加载图谱失败: {message}": "Graph load failed: {message}",
"找到节点: {name}": "Node found: {name}",
"读取表格失败: {reason}": "Table read failed: {reason}",
"PPI Reset 已完成: {reset_id}": "PPI Reset complete: {reset_id}",
"STRING 链接文件内容:{preview}": "STRING link file content: {preview}",
"读取文件失败: {message}": "File read failed: {message}",
"已创建新Working Directory: {job_id}": "New Working Directory created: {job_id}",
"创建失败: {message}": "Creation failed: {message}",
"获取Working Directory列表失败: {message}": "Failed to get Working Directory list: {message}",
"已切换到Working Directory: {job_id}": "Switched to Working Directory: {job_id}",
"切换失败: {message}": "Switch failed: {message}",
"进化树图重绘失败:{message}": "Phylogeny tree replot failed: {message}",
"Combo 图修订失败:{message}": "Combo plot revision failed: {message}",
"任务: {message}\n状态: {status}\n进度: {progress}%": "Task: {message}\nStatus: {status}\nProgress: {progress}%",
"停止拓展分析失败:{message}": "Failed to stop extension: {message}",
"已选择 {count} 个原始代码文件": "{count} code file(s) selected",
"已选择 {count} 个测试数据文件": "{count} test data file(s) selected",
"已拖入 {count} 个文件,已自动填充拓展包": "{count} file(s) dropped; extension package auto-filled",
"❌ {message}": "❌ {message}",
"Upload complete. Latest Excel is now used as config.xlsx.{warningText}": "Upload complete. Latest Excel is now used as config.xlsx.{warningText}",
"{title},需要自动注释": "{title}, Auto-annotation Required",
// String close species (tp with multi-line)
"目标物种不在 STRING 固定快照中。请确认是否采用排序第一的近源物种:": "Target species not in STRING fixed snapshot. Confirm using the top-ranked close species:",
"确认后才会下载参考 PEP、生成 Ortholog,并执行 PPI。": "Reference PEP will be downloaded, Ortholog generated, and PPI executed only after confirmation.",
"是否切换为\"自动注释 + 继续富集\"?": "Switch to \"Auto-annotation + Continue Enrichment\"?"
};