2026-08-03 18:42:47 +08:00
|
|
|
|
"""
|
|
|
|
|
|
Celery任务定义
|
|
|
|
|
|
"""
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import os
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
# 添加项目路径
|
|
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parent
|
|
|
|
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
|
|
|
|
|
|
|
|
from celery_app import app
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-05 09:40:45 +08:00
|
|
|
|
@app.task(
|
|
|
|
|
|
bind=True,
|
|
|
|
|
|
name="tasks.execute_lightweight_extension_operation",
|
|
|
|
|
|
time_limit=None,
|
|
|
|
|
|
soft_time_limit=None,
|
|
|
|
|
|
)
|
|
|
|
|
|
def execute_lightweight_extension_operation(self, payload: dict) -> dict:
|
|
|
|
|
|
"""Run one lightweight-extension operation without a Celery deadline."""
|
|
|
|
|
|
from gfa_core.extension_stage_lightweight.api import execute_queued_operation
|
|
|
|
|
|
|
|
|
|
|
|
return execute_queued_operation(payload)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-03 18:42:47 +08:00
|
|
|
|
def normalize_stage_params(stage: str, params: dict | None) -> tuple[dict, dict]:
|
|
|
|
|
|
"""Clamp per-stage CPU at the worker boundary and return resource metadata."""
|
|
|
|
|
|
params = dict(params or {})
|
|
|
|
|
|
try:
|
|
|
|
|
|
from gfa_core.mas.stage_catalog import clamp_stage_cpu, get_stage_resource_profile
|
|
|
|
|
|
|
|
|
|
|
|
requested_cpu = params.get("requested_cpu", params.get("cpu"))
|
|
|
|
|
|
effective_cpu = clamp_stage_cpu(stage, params.get("cpu"))
|
|
|
|
|
|
profile = get_stage_resource_profile(stage)
|
|
|
|
|
|
if stage in {"rnaseq_qc", "rnaseq_align"}:
|
|
|
|
|
|
from gfa_core.mas.resource_governor import recommend_rnaseq_parallelism, rnaseq_project_cpu_limit
|
|
|
|
|
|
|
|
|
|
|
|
explicit_cpu = requested_cpu not in {None, "", 0, "0"}
|
|
|
|
|
|
project_limit = rnaseq_project_cpu_limit(requested_cpu if explicit_cpu else None)
|
2026-08-05 09:40:45 +08:00
|
|
|
|
explicit_threads = params.get("threads_per_sample") or params.get("threadsPerSample")
|
2026-08-03 18:42:47 +08:00
|
|
|
|
plan = recommend_rnaseq_parallelism(
|
|
|
|
|
|
total_cpu=project_limit,
|
|
|
|
|
|
sample_parallel_jobs=params.get("sample_parallel_jobs") or params.get("sampleParallelJobs"),
|
|
|
|
|
|
stage=stage,
|
|
|
|
|
|
)
|
2026-08-05 09:40:45 +08:00
|
|
|
|
effective_cpu = clamp_stage_cpu(stage, project_limit)
|
2026-08-03 18:42:47 +08:00
|
|
|
|
params["sample_parallel_jobs"] = plan["sample_parallel_jobs"]
|
2026-08-05 09:40:45 +08:00
|
|
|
|
if explicit_threads:
|
|
|
|
|
|
params["threads_per_sample"] = max(1, int(explicit_threads))
|
|
|
|
|
|
else:
|
|
|
|
|
|
params.pop("threads_per_sample", None)
|
|
|
|
|
|
params.pop("threadsPerSample", None)
|
2026-08-03 18:42:47 +08:00
|
|
|
|
params["rnaseq_project_max_cpu"] = plan["project_cpu_limit"]
|
|
|
|
|
|
params["resource_decision"] = {
|
|
|
|
|
|
"stage": stage,
|
|
|
|
|
|
"requested_cpu": requested_cpu,
|
|
|
|
|
|
"effective_cpu": effective_cpu,
|
|
|
|
|
|
"project_cpu_limit": plan["project_cpu_limit"],
|
|
|
|
|
|
"sample_parallel_jobs": plan["sample_parallel_jobs"],
|
2026-08-05 09:40:45 +08:00
|
|
|
|
"threads_per_sample": max(1, int(explicit_threads)) if explicit_threads else None,
|
|
|
|
|
|
"reason": (
|
|
|
|
|
|
f"{stage} preserves the selected total CPU budget; per-sample threads "
|
|
|
|
|
|
"are derived after the real sample count is known"
|
|
|
|
|
|
),
|
2026-08-03 18:42:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
requested_cpu = params.get("requested_cpu", params.get("cpu"))
|
|
|
|
|
|
try:
|
|
|
|
|
|
effective_cpu = max(1, min(int(requested_cpu), 4))
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
effective_cpu = 4
|
|
|
|
|
|
profile = {
|
|
|
|
|
|
"queue": "medium",
|
|
|
|
|
|
"resource_class": "medium",
|
|
|
|
|
|
"default_cpu": effective_cpu,
|
|
|
|
|
|
"max_cpu": effective_cpu,
|
|
|
|
|
|
"profile_error": str(exc),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
params["requested_cpu"] = requested_cpu
|
|
|
|
|
|
params["cpu"] = effective_cpu
|
|
|
|
|
|
_sync_member_set_id_with_scope(stage, params)
|
|
|
|
|
|
return params, profile
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sync_member_set_id_with_scope(stage: str, params: dict) -> None:
|
|
|
|
|
|
"""Keep a scope-derived member_set_id consistent with the execute-time scope_id.
|
|
|
|
|
|
|
|
|
|
|
|
The execute layer can hand down a comp-polluted scope file together with a
|
|
|
|
|
|
plan-time (no-comp) member_set_id, producing a self-contradictory manifest
|
|
|
|
|
|
that postflight rejects as stage_identity_contract_mismatch. Re-derive the
|
|
|
|
|
|
scope token from the actual scope file so the CLI, the manifest and the
|
|
|
|
|
|
postflight check all agree. User-defined subsets (subfamily-xxx) are NOT
|
|
|
|
|
|
scope-derived and are left untouched.
|
|
|
|
|
|
"""
|
|
|
|
|
|
member_set_id = str(params.get("memberSetId") or params.get("member_set_id") or "").strip()
|
|
|
|
|
|
scope_file = str(params.get("scopeFile") or params.get("scope_file") or "").strip()
|
|
|
|
|
|
if not member_set_id or not scope_file:
|
|
|
|
|
|
return
|
|
|
|
|
|
if "subfamily-" in member_set_id:
|
|
|
|
|
|
return
|
|
|
|
|
|
parts = member_set_id.split(":")
|
|
|
|
|
|
if len(parts) != 3 or parts[2] not in {"all_members", "domain_refined"}:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
from gfa_core.gene_family_scope import read_scope, sanitize_token
|
|
|
|
|
|
|
|
|
|
|
|
scope_path = Path(scope_file)
|
|
|
|
|
|
if not scope_path.is_absolute():
|
|
|
|
|
|
scope_path = PROJECT_ROOT / scope_path
|
|
|
|
|
|
if not scope_path.exists():
|
|
|
|
|
|
return
|
|
|
|
|
|
scope_id = str(read_scope(scope_path).get("scope_id") or "").strip()
|
|
|
|
|
|
if not scope_id:
|
|
|
|
|
|
return
|
|
|
|
|
|
expected_token = sanitize_token(scope_id)
|
|
|
|
|
|
analysis_route = str(params.get("analysisRoute") or params.get("analysis_route") or parts[1] or "").strip()
|
|
|
|
|
|
if analysis_route == "unscoped":
|
|
|
|
|
|
analysis_route = "single_species" if stage == "identify" else ""
|
|
|
|
|
|
expected_route = analysis_route or parts[1]
|
|
|
|
|
|
expected_selector = "domain_refined" if expected_route == "subfamily_evidence" else parts[2]
|
|
|
|
|
|
if parts[0] == expected_token and parts[1] == expected_route and parts[2] == expected_selector:
|
|
|
|
|
|
return
|
|
|
|
|
|
new_member_set_id = f"{expected_token}:{expected_route}:{expected_selector}"
|
|
|
|
|
|
print(f"[MEMBER_SET SYNC] scope_id={scope_id} member_set_id {member_set_id} -> {new_member_set_id}")
|
|
|
|
|
|
if params.get("memberSetId"):
|
|
|
|
|
|
params["memberSetId"] = new_member_set_id
|
|
|
|
|
|
if params.get("member_set_id"):
|
|
|
|
|
|
params["member_set_id"] = new_member_set_id
|
|
|
|
|
|
if not params.get("memberSetId") and not params.get("member_set_id"):
|
|
|
|
|
|
params["memberSetId"] = new_member_set_id
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"[MEMBER_SET SYNC] skipped: {exc}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _nonempty_prefix(value) -> str:
|
|
|
|
|
|
text = str(value or "").strip()
|
|
|
|
|
|
return text if text and text.lower() not in {"none", "null", "undefined", "nan", "-"} else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _merge_manifest_identity_params(params: dict | None, manifest: dict | None) -> dict:
|
|
|
|
|
|
"""Prefer the identity actually written by the stage for postflight checks."""
|
|
|
|
|
|
from gfa_core.mas.tools.stage_identity import merge_manifest_identity_params
|
|
|
|
|
|
|
|
|
|
|
|
return merge_manifest_identity_params(params, manifest)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _gene_family_from_config(config_path: Path):
|
|
|
|
|
|
if not config_path.exists():
|
|
|
|
|
|
return None
|
|
|
|
|
|
try:
|
|
|
|
|
|
import pandas as pd
|
|
|
|
|
|
sheets = pd.read_excel(config_path, sheet_name=None)
|
|
|
|
|
|
for sheet_name, df in sheets.items():
|
|
|
|
|
|
if str(sheet_name).lower() in {"analysis", "meta", "metadata"}:
|
|
|
|
|
|
for _, row in df.fillna("").iterrows():
|
|
|
|
|
|
cells = [str(v).strip() for v in row.tolist()]
|
|
|
|
|
|
for idx, cell in enumerate(cells[:-1]):
|
|
|
|
|
|
if cell.lower() in {"family", "gene_family", "analysis_family", "project_family"}:
|
|
|
|
|
|
value = _nonempty_prefix(cells[idx + 1])
|
|
|
|
|
|
if value:
|
|
|
|
|
|
return value
|
|
|
|
|
|
if "Gene_Family" in df.columns:
|
|
|
|
|
|
for value in df["Gene_Family"].tolist():
|
|
|
|
|
|
value = _nonempty_prefix(value)
|
|
|
|
|
|
if value:
|
|
|
|
|
|
return value
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"[Celery] Failed to infer gene family from {config_path}: {exc}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _rnaseq_prefix_from_final(final_dir: Path) -> str:
|
|
|
|
|
|
if not final_dir.exists():
|
|
|
|
|
|
return ""
|
|
|
|
|
|
preferred = [
|
|
|
|
|
|
"*.rnaseq_wgcna.manifest.json",
|
|
|
|
|
|
"*.rnaseq_fgsea.manifest.json",
|
|
|
|
|
|
"*.rnaseq_candidate_family.manifest.json",
|
|
|
|
|
|
"*.rnaseq_sample_qc.manifest.json",
|
|
|
|
|
|
"*.rnaseq_de.manifest.json",
|
|
|
|
|
|
"*.rnaseq_quant.manifest.json",
|
|
|
|
|
|
"*.rnaseq_align.manifest.json",
|
|
|
|
|
|
"*.rnaseq_qc.manifest.json",
|
|
|
|
|
|
"*.annotation.manifest.json",
|
|
|
|
|
|
"*.genome_prepare.manifest.json",
|
|
|
|
|
|
]
|
|
|
|
|
|
for pattern in preferred:
|
|
|
|
|
|
matches = sorted(final_dir.glob(pattern))
|
|
|
|
|
|
if len(matches) == 1:
|
|
|
|
|
|
suffix = pattern[1:]
|
|
|
|
|
|
return matches[0].name[: -len(suffix)]
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resolve_effective_prefix(job_id: str, prefix: str = "", stage: str = "") -> str:
|
|
|
|
|
|
provided = _nonempty_prefix(prefix)
|
|
|
|
|
|
gene_family_stages = {
|
|
|
|
|
|
"identify", "phylogeny", "motif", "domain", "structure", "promoter", "tfbs",
|
|
|
|
|
|
"synteny", "interspecies_synteny", "karyotype", "combo", "ppi",
|
|
|
|
|
|
"pep", "summary", "reference_classification", "tree_plot",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def normalize_gene_family_prefix(value: str) -> str:
|
|
|
|
|
|
value = _nonempty_prefix(value)
|
|
|
|
|
|
if stage in gene_family_stages and value.startswith("Multi."):
|
|
|
|
|
|
family = value.split(".", 1)[1].strip()
|
|
|
|
|
|
if family:
|
|
|
|
|
|
return f"Target.{family}"
|
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
final_dir = PROJECT_ROOT / "workspace" / job_id / "final"
|
|
|
|
|
|
if stage in {"rnaseq_enrich", "rnaseq_go_enrich", "rnaseq_kegg_enrich", "rnaseq_sample_qc", "rnaseq_fgsea", "rnaseq_wgcna", "rnaseq_candidate_family"}:
|
|
|
|
|
|
rnaseq_prefix = _rnaseq_prefix_from_final(final_dir)
|
|
|
|
|
|
if rnaseq_prefix and rnaseq_prefix != provided:
|
|
|
|
|
|
print(f"[Celery] Overriding RNA-seq prefix {provided or '<empty>'} -> {rnaseq_prefix}")
|
|
|
|
|
|
return rnaseq_prefix
|
|
|
|
|
|
if provided:
|
|
|
|
|
|
return normalize_gene_family_prefix(provided)
|
|
|
|
|
|
input_dir = PROJECT_ROOT / "input" / job_id
|
|
|
|
|
|
species_config = input_dir / "genome_prepare" / "species_config.json"
|
|
|
|
|
|
if species_config.exists():
|
|
|
|
|
|
try:
|
|
|
|
|
|
import json
|
|
|
|
|
|
data = json.loads(species_config.read_text(encoding="utf-8"))
|
|
|
|
|
|
target = data.get("target") if isinstance(data.get("target"), dict) else None
|
|
|
|
|
|
if target:
|
|
|
|
|
|
value = _nonempty_prefix(target.get("prefix"))
|
|
|
|
|
|
if value:
|
|
|
|
|
|
return normalize_gene_family_prefix(value)
|
|
|
|
|
|
value = _nonempty_prefix(data.get("prefix"))
|
|
|
|
|
|
if value:
|
|
|
|
|
|
return normalize_gene_family_prefix(value)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"[Celery] Failed to read species_config prefix: {exc}")
|
|
|
|
|
|
for base in [final_dir, input_dir / "final"]:
|
|
|
|
|
|
if base.exists():
|
|
|
|
|
|
for mf in sorted(base.glob("Target.*.genome_prepare.manifest.json")):
|
|
|
|
|
|
name = mf.name
|
|
|
|
|
|
return name.split(".genome_prepare.manifest", 1)[0]
|
|
|
|
|
|
for mf in sorted(base.glob("Multi.*.genome_prepare.manifest.json")):
|
|
|
|
|
|
name = mf.name
|
|
|
|
|
|
return normalize_gene_family_prefix(name.split(".genome_prepare.manifest", 1)[0])
|
|
|
|
|
|
family = _gene_family_from_config(input_dir / "config.xlsx")
|
|
|
|
|
|
if family:
|
|
|
|
|
|
return f"Target.{family}"
|
|
|
|
|
|
for pattern in ("Target.*.final_members.pep.fa", "Multi.*.final_members.pep.fa"):
|
|
|
|
|
|
for file in sorted(final_dir.glob(pattern)):
|
|
|
|
|
|
return normalize_gene_family_prefix(file.name[:-len(".final_members.pep.fa")])
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _split_prefix(value: str) -> tuple[str, str]:
|
|
|
|
|
|
text = str(value or "").strip()
|
|
|
|
|
|
if "." in text:
|
|
|
|
|
|
left, right = text.split(".", 1)
|
|
|
|
|
|
return left.strip(), right.strip()
|
|
|
|
|
|
return "", text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _list_param(value) -> list[str]:
|
|
|
|
|
|
if isinstance(value, list):
|
|
|
|
|
|
return [str(item).strip() for item in value if str(item).strip()]
|
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
|
|
return [item.strip() for item in re.split(r"[,;,;\s]+", str(value or "")) if item.strip()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolved_artifacts_from_decisions(decisions: dict) -> dict:
|
|
|
|
|
|
resolved = {}
|
|
|
|
|
|
for key, decision in (decisions or {}).items():
|
|
|
|
|
|
if not isinstance(decision, dict):
|
|
|
|
|
|
continue
|
|
|
|
|
|
selected = decision.get("selected") if isinstance(decision.get("selected"), dict) else None
|
|
|
|
|
|
if selected and selected.get("path"):
|
|
|
|
|
|
resolved[key] = {
|
|
|
|
|
|
"path": selected.get("path"),
|
|
|
|
|
|
"source": selected.get("source", ""),
|
|
|
|
|
|
"kind": selected.get("kind", ""),
|
|
|
|
|
|
"role": selected.get("role", ""),
|
|
|
|
|
|
"species": selected.get("species", ""),
|
|
|
|
|
|
"family": selected.get("family", ""),
|
|
|
|
|
|
}
|
|
|
|
|
|
return resolved
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def attach_semantic_execution_context(job_id: str, stage: str, prefix: str, params: dict | None) -> dict:
|
|
|
|
|
|
"""Attach resolver decisions to params for execution and diagnosis.
|
|
|
|
|
|
|
|
|
|
|
|
/api/execute already performs a semantic preflight, but Celery workers can
|
|
|
|
|
|
also be called directly or run after code reloads. Rebuilding here keeps
|
|
|
|
|
|
execution, diagnosis, and logs on the same project-context view.
|
|
|
|
|
|
"""
|
|
|
|
|
|
params = dict(params or {})
|
|
|
|
|
|
if isinstance(params.get("semanticPreflight"), dict) and isinstance(params.get("resolvedArtifacts"), dict):
|
|
|
|
|
|
return params
|
|
|
|
|
|
try:
|
|
|
|
|
|
from gfa_core.mas.artifact_resolver import ArtifactResolver
|
|
|
|
|
|
from gfa_core.mas.context_index import build_project_context
|
|
|
|
|
|
|
|
|
|
|
|
context = build_project_context(job_id=job_id, prefix=prefix, project_root=PROJECT_ROOT)
|
|
|
|
|
|
resolver = ArtifactResolver(context)
|
|
|
|
|
|
prefix_species, prefix_family = _split_prefix(prefix)
|
|
|
|
|
|
family = str(params.get("geneFamily") or params.get("family") or prefix_family or context.family or "").strip()
|
|
|
|
|
|
target_species = _list_param(params.get("targetSpecies") or params.get("target_species") or [])
|
|
|
|
|
|
if target_species:
|
|
|
|
|
|
species_list = target_species
|
|
|
|
|
|
elif prefix_species and prefix_species.lower() not in {"multi", "target"}:
|
|
|
|
|
|
species_list = [prefix_species]
|
|
|
|
|
|
elif len(context.target_species) == 1:
|
|
|
|
|
|
species_list = [context.target_species[0]]
|
|
|
|
|
|
else:
|
|
|
|
|
|
species_list = [""]
|
|
|
|
|
|
|
|
|
|
|
|
decisions = {}
|
|
|
|
|
|
for species in species_list:
|
|
|
|
|
|
for role, decision in resolver.resolve_stage(stage, species=species, family=family).items():
|
|
|
|
|
|
decisions[f"{species or 'auto'}:{role}"] = decision.to_dict()
|
|
|
|
|
|
|
|
|
|
|
|
semantic_preflight = {
|
|
|
|
|
|
"valid": True,
|
|
|
|
|
|
"stage": stage,
|
|
|
|
|
|
"prefix": prefix,
|
|
|
|
|
|
"decisions": decisions,
|
|
|
|
|
|
"context": {
|
|
|
|
|
|
"family": context.family,
|
|
|
|
|
|
"target_species": context.target_species,
|
|
|
|
|
|
"reference_species": context.reference_species,
|
|
|
|
|
|
"comparative_species": context.comparative_species,
|
|
|
|
|
|
"config_path": context.config_path,
|
|
|
|
|
|
"input_dir": context.input_dir,
|
|
|
|
|
|
"final_dir": context.final_dir,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
params["semanticPreflight"] = semantic_preflight
|
|
|
|
|
|
params["resolvedArtifacts"] = _resolved_artifacts_from_decisions(decisions)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
params["semanticContextError"] = str(exc)
|
|
|
|
|
|
return params
|
|
|
|
|
|
|
|
|
|
|
|
# 初始化Orchestrator(每个worker进程初始化一次)
|
|
|
|
|
|
_orchestrator = None
|
|
|
|
|
|
|
|
|
|
|
|
def get_orchestrator():
|
|
|
|
|
|
global _orchestrator
|
|
|
|
|
|
if _orchestrator is None:
|
|
|
|
|
|
from gfa_core.mas.config import config as mas_config
|
|
|
|
|
|
from gfa_core.mas.llm import LLMManager
|
|
|
|
|
|
from gfa_core.mas.memory import MemoryManager
|
|
|
|
|
|
from gfa_core.mas.core.langgraph_orchestrator import LangGraphOrchestrator
|
|
|
|
|
|
|
|
|
|
|
|
llm_config = mas_config.load_llm_config()
|
|
|
|
|
|
llm_config["openai_api_key"] = mas_config.settings.openai_api_key
|
|
|
|
|
|
llm_config["openai_base_url"] = mas_config.settings.openai_base_url
|
|
|
|
|
|
llm_manager = LLMManager(config=llm_config)
|
|
|
|
|
|
_orchestrator = LangGraphOrchestrator(llm_manager=llm_manager)
|
|
|
|
|
|
return _orchestrator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def validate_single_stage_artifacts(job_id: str, prefix: str, stage: str, result: dict | None = None) -> dict | None:
|
|
|
|
|
|
"""Return a failure response if a single-stage task did not produce valid artifacts."""
|
|
|
|
|
|
from gfa_core.mas.tools.stage_identity import get_stage_status_for_identity, resolve_stage_identity
|
|
|
|
|
|
from gfa_core.mas.tools.stage_status import get_stage_status
|
|
|
|
|
|
|
|
|
|
|
|
actual_stage = stage
|
|
|
|
|
|
actual_prefix = prefix
|
|
|
|
|
|
manifest = {}
|
|
|
|
|
|
if isinstance(result, dict):
|
|
|
|
|
|
final_output = result.get("final_output", {})
|
|
|
|
|
|
if isinstance(final_output, dict) and isinstance(final_output.get("manifest"), dict):
|
|
|
|
|
|
manifest = final_output.get("manifest") or {}
|
|
|
|
|
|
elif isinstance(result.get("manifest"), dict):
|
|
|
|
|
|
manifest = result.get("manifest") or {}
|
|
|
|
|
|
if isinstance(manifest, dict) and manifest:
|
|
|
|
|
|
actual_stage = _nonempty_prefix(manifest.get("stage")) or actual_stage
|
|
|
|
|
|
actual_prefix = _nonempty_prefix(manifest.get("prefix")) or actual_prefix
|
|
|
|
|
|
|
|
|
|
|
|
stage_context = manifest.get("stage_context") or manifest.get("stageContext") or {} if isinstance(manifest, dict) else {}
|
|
|
|
|
|
identity_params = {
|
|
|
|
|
|
"prefix": actual_prefix,
|
|
|
|
|
|
"scopeFile": manifest.get("scope_file") or stage_context.get("scope_file") or "" if isinstance(manifest, dict) else "",
|
|
|
|
|
|
"scopeId": manifest.get("scope_id") or stage_context.get("scope_id") or "" if isinstance(manifest, dict) else "",
|
|
|
|
|
|
"scopeMode": manifest.get("scope_mode") or stage_context.get("scope_mode") or "" if isinstance(manifest, dict) else "",
|
|
|
|
|
|
"analysisRoute": manifest.get("analysis_route") or stage_context.get("analysis_route") or "" if isinstance(manifest, dict) else "",
|
|
|
|
|
|
"memberSetId": manifest.get("member_set_id") or stage_context.get("member_set_id") or "" if isinstance(manifest, dict) else "",
|
|
|
|
|
|
"geneFamily": manifest.get("gene_family") or manifest.get("family") or stage_context.get("family") or "" if isinstance(manifest, dict) else "",
|
|
|
|
|
|
"targetSpecies": manifest.get("target_species") or stage_context.get("target_species") or [] if isinstance(manifest, dict) else [],
|
|
|
|
|
|
"referenceSpecies": manifest.get("reference_species") or stage_context.get("reference_species") or [] if isinstance(manifest, dict) else [],
|
|
|
|
|
|
"comparativeSpecies": manifest.get("comparative_species") or stage_context.get("comparative_species") or [] if isinstance(manifest, dict) else [],
|
|
|
|
|
|
"wgsSampleSetId": manifest.get("wgs_sample_set_id") if isinstance(manifest, dict) else "",
|
|
|
|
|
|
"wgsReferenceId": manifest.get("wgs_reference_id") if isinstance(manifest, dict) else "",
|
|
|
|
|
|
"wgsAnalysisId": manifest.get("wgs_analysis_id") if isinstance(manifest, dict) else "",
|
|
|
|
|
|
"genotypeScopeId": manifest.get("genotype_scope_id") if isinstance(manifest, dict) else "",
|
|
|
|
|
|
"phenotypeScopeId": manifest.get("phenotype_scope_id") if isinstance(manifest, dict) else "",
|
|
|
|
|
|
"covariateScopeId": manifest.get("covariate_scope_id") if isinstance(manifest, dict) else "",
|
|
|
|
|
|
"modelScopeId": manifest.get("model_scope_id") if isinstance(manifest, dict) else "",
|
|
|
|
|
|
"sourceModelScopeId": manifest.get("source_model_scope_id") if isinstance(manifest, dict) else "",
|
|
|
|
|
|
"analysisScopeId": manifest.get("analysis_scope_id") if isinstance(manifest, dict) else "",
|
|
|
|
|
|
"runId": manifest.get("run_id") if isinstance(manifest, dict) else "",
|
|
|
|
|
|
}
|
|
|
|
|
|
try:
|
|
|
|
|
|
identity = resolve_stage_identity(
|
|
|
|
|
|
stage=actual_stage,
|
|
|
|
|
|
prefix=actual_prefix,
|
|
|
|
|
|
params=identity_params,
|
|
|
|
|
|
job_id=job_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
stage_status = get_stage_status_for_identity(
|
|
|
|
|
|
job_id=job_id,
|
|
|
|
|
|
identity=identity,
|
|
|
|
|
|
strict_identity=bool(
|
|
|
|
|
|
identity.scope_id
|
|
|
|
|
|
or identity.analysis_route
|
|
|
|
|
|
or identity.member_set_id
|
|
|
|
|
|
or identity.wgs_sample_set_id
|
|
|
|
|
|
or identity.wgs_reference_id
|
|
|
|
|
|
or identity.wgs_analysis_id
|
|
|
|
|
|
or identity.genotype_scope_id
|
|
|
|
|
|
or identity.phenotype_scope_id
|
|
|
|
|
|
or identity.covariate_scope_id
|
|
|
|
|
|
or identity.model_scope_id
|
|
|
|
|
|
or identity.source_model_scope_id
|
|
|
|
|
|
or identity.analysis_scope_id
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
scope_id = str(identity_params.get("scopeId") or "")
|
|
|
|
|
|
analysis_route = str(identity_params.get("analysisRoute") or "")
|
|
|
|
|
|
member_set_id = str(identity_params.get("memberSetId") or "")
|
|
|
|
|
|
print(f"[Celery] Identity-aware artifact validation fallback for {stage}: {exc}")
|
|
|
|
|
|
stage_status = get_stage_status(
|
|
|
|
|
|
job_id=job_id,
|
|
|
|
|
|
prefix=actual_prefix,
|
|
|
|
|
|
stage=actual_stage,
|
|
|
|
|
|
scope_id=scope_id,
|
|
|
|
|
|
analysis_route=analysis_route,
|
|
|
|
|
|
member_set_id=member_set_id,
|
|
|
|
|
|
strict_identity=bool(scope_id or analysis_route or member_set_id),
|
|
|
|
|
|
)
|
|
|
|
|
|
if actual_stage in {"rnaseq_go_enrich", "rnaseq_kegg_enrich"} and stage_status.completed:
|
|
|
|
|
|
final_dir = PROJECT_ROOT / "workspace" / job_id / "final"
|
|
|
|
|
|
suffix = ".GO_enrich.txt" if actual_stage == "rnaseq_go_enrich" else ".KEGG_enrich.txt"
|
|
|
|
|
|
if any(final_dir.glob(f"*{suffix}")):
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
if stage_status.completed:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
business_messages: list[str] = []
|
|
|
|
|
|
if isinstance(result, dict):
|
|
|
|
|
|
for error in result.get("errors", []) or []:
|
|
|
|
|
|
if error:
|
|
|
|
|
|
business_messages.append(str(error))
|
|
|
|
|
|
final_output = result.get("final_output", {})
|
|
|
|
|
|
if isinstance(final_output, dict):
|
|
|
|
|
|
for key in ("response", "message", "error"):
|
|
|
|
|
|
value = final_output.get(key)
|
|
|
|
|
|
if value:
|
|
|
|
|
|
business_messages.append(str(value))
|
|
|
|
|
|
|
|
|
|
|
|
business_error = next(
|
|
|
|
|
|
(
|
|
|
|
|
|
msg for msg in business_messages
|
|
|
|
|
|
if "❌" in msg or "failed" in msg.lower() or "cannot choose a background" in msg.lower()
|
|
|
|
|
|
),
|
|
|
|
|
|
"",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
detail_parts = []
|
|
|
|
|
|
if stage_status.errors:
|
|
|
|
|
|
detail_parts.extend(stage_status.errors)
|
|
|
|
|
|
if stage_status.missing_artifacts:
|
|
|
|
|
|
detail_parts.append("缺失产物: " + "; ".join(stage_status.missing_artifacts))
|
|
|
|
|
|
detail_msg = ";".join(detail_parts) or f"{stage} 未生成有效输出"
|
|
|
|
|
|
error_msg = business_error or f"{stage} 执行失败:{detail_msg}"
|
|
|
|
|
|
print(f"[Celery] 单阶段产物校验失败: {stage_status.to_dict()}")
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
'status': 'failed',
|
|
|
|
|
|
'success': False,
|
|
|
|
|
|
'current_stage': stage,
|
|
|
|
|
|
'progress': 0,
|
|
|
|
|
|
'message': error_msg,
|
|
|
|
|
|
'errors': [error_msg],
|
|
|
|
|
|
'stage_status': stage_status.to_dict(),
|
|
|
|
|
|
'result': (result or {}).get("final_output", {}) if isinstance(result, dict) else {},
|
|
|
|
|
|
'logs': [
|
|
|
|
|
|
{'time': datetime.now().isoformat(), 'level': 'error', 'message': error_msg}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _diagnose_single_stage_failure(
|
|
|
|
|
|
job_id: str,
|
|
|
|
|
|
stage: str,
|
|
|
|
|
|
prefix: str = "",
|
|
|
|
|
|
*,
|
|
|
|
|
|
prepare_patch_workspace: bool = False,
|
|
|
|
|
|
) -> dict:
|
|
|
|
|
|
"""DiagnosisAgent is disabled; keep failures as plain execution failures."""
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "diagnosis_disabled",
|
|
|
|
|
|
"stage": stage,
|
|
|
|
|
|
"job_id": job_id,
|
|
|
|
|
|
"prefix": prefix,
|
|
|
|
|
|
"issues": [],
|
|
|
|
|
|
}
|
|
|
|
|
|
try:
|
|
|
|
|
|
from gfa_core.mas.agents.diagnosis import DiagnosisAgent
|
|
|
|
|
|
|
|
|
|
|
|
diagnosis = DiagnosisAgent(None).diagnose_job_stage(
|
|
|
|
|
|
job_id=job_id,
|
|
|
|
|
|
stage=stage,
|
|
|
|
|
|
prefix=prefix or "",
|
|
|
|
|
|
project_root=PROJECT_ROOT,
|
|
|
|
|
|
prepare_patch_workspace=prepare_patch_workspace,
|
|
|
|
|
|
)
|
|
|
|
|
|
return diagnosis if isinstance(diagnosis, dict) else {}
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"[Celery] DiagnosisAgent failed for {job_id}/{stage}: {exc}")
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "diagnosis_failed",
|
|
|
|
|
|
"stage": stage,
|
|
|
|
|
|
"job_id": job_id,
|
|
|
|
|
|
"prefix": prefix,
|
|
|
|
|
|
"error": str(exc),
|
|
|
|
|
|
"policy": {
|
|
|
|
|
|
"auto_modify_input": False,
|
|
|
|
|
|
"auto_modify_global_code": False,
|
|
|
|
|
|
"auto_rerun_stage": False,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _diagnose_stage_gate(
|
|
|
|
|
|
job_id: str,
|
|
|
|
|
|
stage: str,
|
|
|
|
|
|
prefix: str = "",
|
|
|
|
|
|
*,
|
|
|
|
|
|
gate: str,
|
|
|
|
|
|
params: dict | None = None,
|
|
|
|
|
|
) -> dict:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "diagnosis_disabled",
|
|
|
|
|
|
"stage": stage,
|
|
|
|
|
|
"job_id": job_id,
|
|
|
|
|
|
"prefix": prefix,
|
|
|
|
|
|
"gate": gate,
|
|
|
|
|
|
"issues": [],
|
|
|
|
|
|
}
|
|
|
|
|
|
try:
|
|
|
|
|
|
from gfa_core.mas.agents.diagnosis import DiagnosisAgent
|
|
|
|
|
|
|
|
|
|
|
|
diagnosis_prefix = "genome_prepare" if stage in {"genome_prepare", "genome_prepare_v2"} else (prefix or "")
|
|
|
|
|
|
return DiagnosisAgent(None).diagnose_stage_gate(
|
|
|
|
|
|
job_id=job_id,
|
|
|
|
|
|
stage=stage,
|
|
|
|
|
|
prefix=diagnosis_prefix,
|
|
|
|
|
|
gate=gate,
|
|
|
|
|
|
project_root=PROJECT_ROOT,
|
|
|
|
|
|
params=params or {},
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"[Celery] Diagnosis gate failed for {job_id}/{stage}/{gate}: {exc}")
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "diagnosis_failed",
|
|
|
|
|
|
"stage": stage,
|
|
|
|
|
|
"job_id": job_id,
|
|
|
|
|
|
"prefix": prefix,
|
|
|
|
|
|
"gate": gate,
|
|
|
|
|
|
"error": str(exc),
|
|
|
|
|
|
"issues": [],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _apply_runtime_repairs(
|
|
|
|
|
|
job_id: str,
|
|
|
|
|
|
stage: str,
|
|
|
|
|
|
prefix: str = "",
|
|
|
|
|
|
diagnosis: dict | None = None,
|
|
|
|
|
|
*,
|
|
|
|
|
|
execute_heavy: bool = False,
|
|
|
|
|
|
) -> dict:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"success": False,
|
|
|
|
|
|
"status": "diagnosis_disabled",
|
|
|
|
|
|
"stage": stage,
|
|
|
|
|
|
"job_id": job_id,
|
|
|
|
|
|
"results": [],
|
|
|
|
|
|
}
|
|
|
|
|
|
try:
|
|
|
|
|
|
from gfa_core.mas.agents.diagnosis import DiagnosisAgent
|
|
|
|
|
|
|
|
|
|
|
|
return DiagnosisAgent(None).apply_runtime_repairs(
|
|
|
|
|
|
job_id=job_id,
|
|
|
|
|
|
stage=stage,
|
|
|
|
|
|
prefix=prefix or "",
|
|
|
|
|
|
diagnosis=diagnosis,
|
|
|
|
|
|
project_root=PROJECT_ROOT,
|
|
|
|
|
|
execute_heavy=execute_heavy,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"[Celery] Runtime repair failed for {job_id}/{stage}: {exc}")
|
|
|
|
|
|
return {
|
|
|
|
|
|
"success": False,
|
|
|
|
|
|
"status": "repair_failed",
|
|
|
|
|
|
"stage": stage,
|
|
|
|
|
|
"job_id": job_id,
|
|
|
|
|
|
"error": str(exc),
|
|
|
|
|
|
"results": [],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _attach_diagnosis_to_failure(response: dict, job_id: str, stage: str, prefix: str = "") -> dict:
|
|
|
|
|
|
"""Keep failed task responses free of DiagnosisAgent side effects."""
|
|
|
|
|
|
if not isinstance(response, dict) or response.get("success") is not False:
|
|
|
|
|
|
return response
|
|
|
|
|
|
response.pop("pending_confirmation", None)
|
|
|
|
|
|
response.pop("diagnosis", None)
|
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
diagnosis = response.get("diagnosis")
|
|
|
|
|
|
if not isinstance(diagnosis, dict) or not diagnosis:
|
|
|
|
|
|
diagnosis = _diagnose_single_stage_failure(job_id=job_id, stage=stage, prefix=prefix)
|
|
|
|
|
|
response["diagnosis"] = diagnosis
|
|
|
|
|
|
|
|
|
|
|
|
session_id = str(response.get("session_id") or job_id or "").strip()
|
|
|
|
|
|
if diagnosis.get("requires_user_confirmation"):
|
|
|
|
|
|
response["pending_confirmation"] = {
|
|
|
|
|
|
"status": "waiting",
|
|
|
|
|
|
"type": "diagnosis_review",
|
|
|
|
|
|
"session_id": session_id,
|
|
|
|
|
|
"stage": stage,
|
|
|
|
|
|
"job_id": job_id,
|
|
|
|
|
|
"prefix": prefix,
|
|
|
|
|
|
"diagnosis": diagnosis,
|
|
|
|
|
|
"message": "检测到阶段失败,已生成诊断。请用户确认后再修订输入、执行副本修复或升级全局代码。",
|
|
|
|
|
|
"allowed_actions": [
|
|
|
|
|
|
"review_only",
|
|
|
|
|
|
"prepare_job_patch",
|
|
|
|
|
|
"apply_confirmed_global_fix_after_backup",
|
|
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.task(bind=True, max_retries=3)
|
|
|
|
|
|
def execute_agent_task(self, task_id: str, message: str, session_id: str, job_id: str = "", history: list = None, prefix: str = ""):
|
|
|
|
|
|
"""
|
|
|
|
|
|
执行Agent分析任务
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
print(f"[Celery] Starting task {task_id}, job_id={job_id}")
|
|
|
|
|
|
|
|
|
|
|
|
# 更新任务状态为运行中(使用self.request.id获取Celery任务ID)
|
|
|
|
|
|
celery_task_id = self.request.id if self.request else task_id
|
|
|
|
|
|
self.update_state(
|
|
|
|
|
|
task_id=celery_task_id,
|
|
|
|
|
|
state='RUNNING',
|
|
|
|
|
|
meta={
|
|
|
|
|
|
'current_stage': 'intent_parsing',
|
|
|
|
|
|
'progress': 0,
|
|
|
|
|
|
'message': message,
|
|
|
|
|
|
'logs': [
|
|
|
|
|
|
{'time': datetime.now().isoformat(), 'level': 'info', 'message': '开始分析任务...'},
|
|
|
|
|
|
{'time': datetime.now().isoformat(), 'level': 'info', 'message': f'解析用户意图: {message}'}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 获取Orchestrator
|
|
|
|
|
|
orchestrator = get_orchestrator()
|
|
|
|
|
|
|
|
|
|
|
|
# 查询已完成的阶段
|
|
|
|
|
|
completed_stages = []
|
|
|
|
|
|
if job_id:
|
|
|
|
|
|
from gfa_core.mas.tools.stage_status import list_completed_stages
|
|
|
|
|
|
|
|
|
|
|
|
completed_stages = list_completed_stages(job_id=job_id, prefix=prefix or "")
|
|
|
|
|
|
print(f"[tasks.py] Completed stages: {completed_stages}")
|
|
|
|
|
|
|
|
|
|
|
|
# 构建初始状态
|
|
|
|
|
|
from gfa_core.mas.core.state import GraphState
|
|
|
|
|
|
initial_state = {
|
|
|
|
|
|
"query": message,
|
|
|
|
|
|
"session_id": session_id,
|
|
|
|
|
|
"job_id": job_id, # 传入 job_id 用于 auto_continue
|
|
|
|
|
|
"files": [],
|
|
|
|
|
|
"current_agent": "router",
|
|
|
|
|
|
"status": "running",
|
|
|
|
|
|
"errors": [],
|
|
|
|
|
|
"final_output": {},
|
|
|
|
|
|
"history": history or [],
|
|
|
|
|
|
"messages": [], # 添加 messages 字段
|
|
|
|
|
|
"completed_stages": completed_stages, # 添加已完成阶段
|
|
|
|
|
|
"retry_count": 0, # 添加重试计数
|
|
|
|
|
|
"max_retries": 3, # 添加最大重试次数
|
|
|
|
|
|
"user_input": {"prefix": prefix} # 传递 prefix 参数
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
print(f"[tasks.py] initial_state keys: {list(initial_state.keys())}")
|
|
|
|
|
|
print(f"[tasks.py] initial_state['completed_stages']: {initial_state.get('completed_stages', 'NOT FOUND')}")
|
|
|
|
|
|
|
|
|
|
|
|
# 运行Orchestrator
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
result = asyncio.run(orchestrator.run(initial_state))
|
|
|
|
|
|
|
|
|
|
|
|
# 处理结果
|
|
|
|
|
|
final_output = result.get("final_output", {})
|
|
|
|
|
|
|
|
|
|
|
|
if result.get("status") == "completed":
|
|
|
|
|
|
# ============ 生成累积解读报告(新增) ============
|
|
|
|
|
|
try:
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
from gfa_core.mas.agents.result_parser import create_result_parser_agent
|
|
|
|
|
|
from gfa_core.mas.llm import LLMManager
|
|
|
|
|
|
|
|
|
|
|
|
# 从 result 中提取 prefix(如果传入的 prefix 为空)
|
|
|
|
|
|
if not prefix:
|
|
|
|
|
|
prefix = result.get("final_output", {}).get("user_input", {}).get("prefix", "")
|
|
|
|
|
|
if not prefix:
|
|
|
|
|
|
print("[Celery] 未解析到输出标签,跳过累积解读报告生成")
|
|
|
|
|
|
raise RuntimeError("missing_prefix_for_interpretation")
|
|
|
|
|
|
|
|
|
|
|
|
# 获取已完成的阶段列表(统一 manifest/artifacts 校验)
|
|
|
|
|
|
from gfa_core.mas.tools.stage_status import list_completed_stages
|
|
|
|
|
|
|
|
|
|
|
|
completed_stages = list_completed_stages(job_id=job_id, prefix=prefix)
|
|
|
|
|
|
|
|
|
|
|
|
# 按顺序排序
|
|
|
|
|
|
stage_order = ["identify", "phylogeny", "structure", "motif", "domain", "promoter", "synteny", "interspecies_synteny", "karyotype", "expression", "kaks", "ppi"]
|
|
|
|
|
|
completed_stages = [s for s in stage_order if s in completed_stages] + [s for s in completed_stages if s not in stage_order]
|
|
|
|
|
|
|
|
|
|
|
|
print(f"[Celery] 生成累积解读,已完成阶段: {completed_stages}")
|
|
|
|
|
|
|
|
|
|
|
|
# 创建 ResultParserAgent(不需要 LLMManager)
|
|
|
|
|
|
agent = create_result_parser_agent(None)
|
|
|
|
|
|
|
|
|
|
|
|
# 生成累积解读报告
|
|
|
|
|
|
report = asyncio.run(agent.generate_cumulative_report(
|
|
|
|
|
|
job_id=job_id,
|
|
|
|
|
|
prefix=prefix,
|
|
|
|
|
|
completed_stages=completed_stages
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
# 保存解读报告
|
|
|
|
|
|
save_success = agent.save_interpretation(job_id, prefix, report)
|
|
|
|
|
|
|
|
|
|
|
|
if save_success:
|
|
|
|
|
|
print(f"[Celery] 解读报告已保存到 interpretation.json")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"[Celery] 保存解读报告失败")
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
# 解读失败不阻塞主流程
|
|
|
|
|
|
print(f"[Celery] 生成解读报告失败(非阻塞): {e}")
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
'status': 'completed',
|
|
|
|
|
|
'current_stage': result.get("current_agent", "unknown"),
|
|
|
|
|
|
'progress': 100,
|
|
|
|
|
|
'message': message,
|
|
|
|
|
|
'result': {
|
|
|
|
|
|
'response': final_output.get("response", ""),
|
|
|
|
|
|
'analysis_plan': final_output.get("analysis_plan", {}),
|
|
|
|
|
|
'execution_result': final_output.get("execution_result", {})
|
|
|
|
|
|
},
|
|
|
|
|
|
'logs': [
|
|
|
|
|
|
{'time': datetime.now().isoformat(), 'level': 'success', 'message': '分析任务完成'}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
return {
|
|
|
|
|
|
'status': 'failed',
|
|
|
|
|
|
'current_stage': result.get("current_agent", "unknown"),
|
|
|
|
|
|
'progress': 0,
|
|
|
|
|
|
'message': message,
|
|
|
|
|
|
'result': {},
|
|
|
|
|
|
'logs': [
|
|
|
|
|
|
{'time': datetime.now().isoformat(), 'level': 'error', 'message': f"分析失败: {result.get('errors', ['Unknown error'])}"}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
error_msg = f"任务异常: {str(e)}\n{traceback.format_exc()}"
|
|
|
|
|
|
print(f"[Celery] Task {task_id} failed: {error_msg}")
|
|
|
|
|
|
celery_task_id = self.request.id if self.request else task_id
|
|
|
|
|
|
self.update_state(
|
|
|
|
|
|
task_id=celery_task_id,
|
|
|
|
|
|
state='FAILURE',
|
|
|
|
|
|
meta={
|
|
|
|
|
|
'exc_type': type(e).__name__,
|
|
|
|
|
|
'exc_message': str(e),
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.task(bind=True, max_retries=3)
|
|
|
|
|
|
def execute_single_stage_task(self, task_id: str, stage: str, job_id: str, prefix: str,
|
|
|
|
|
|
params: dict = None, session_id: str = None):
|
|
|
|
|
|
"""
|
|
|
|
|
|
执行单个分析阶段任务(用于/api/execute统一架构)
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
- task_id: 任务ID
|
|
|
|
|
|
- stage: 分析阶段(如identify, phylogeny等)
|
|
|
|
|
|
- job_id: 任务目录ID
|
|
|
|
|
|
- prefix: 分析前缀(例如 At.WRKY;基因家族流程通常由 config/scope 自动生成)
|
|
|
|
|
|
- params: 执行参数
|
|
|
|
|
|
- session_id: 会话ID
|
|
|
|
|
|
"""
|
|
|
|
|
|
resource_acquired = False
|
|
|
|
|
|
resource_outcome = "failed"
|
|
|
|
|
|
resource_lease = None
|
|
|
|
|
|
try:
|
|
|
|
|
|
print(f"[Celery] Starting single stage task {task_id}, stage={stage}, job_id={job_id}")
|
|
|
|
|
|
params, resource_profile = normalize_stage_params(stage, params)
|
|
|
|
|
|
print(
|
|
|
|
|
|
"[Celery] Resource profile "
|
|
|
|
|
|
f"stage={stage}, queue={resource_profile.get('queue')}, "
|
|
|
|
|
|
f"class={resource_profile.get('resource_class')}, "
|
|
|
|
|
|
f"requested_cpu={params.get('requested_cpu')}, effective_cpu={params.get('cpu')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
celery_task_id = self.request.id if self.request else task_id
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
from gfa_core.mas.resource_governor import ResourceLease
|
|
|
|
|
|
from gfa_core.mas.stage_catalog import get_stage_queue
|
|
|
|
|
|
|
|
|
|
|
|
resource_queue = get_stage_queue(stage)
|
|
|
|
|
|
resource_lease = ResourceLease(
|
|
|
|
|
|
task_id=task_id,
|
|
|
|
|
|
stage=stage,
|
|
|
|
|
|
job_id=job_id,
|
|
|
|
|
|
prefix=prefix,
|
|
|
|
|
|
cpu=int(params.get("cpu") or 1),
|
|
|
|
|
|
queue=resource_queue,
|
|
|
|
|
|
resource_class=str(resource_profile.get("resource_class") or ""),
|
|
|
|
|
|
concurrency_group=str(resource_profile.get("concurrency_group") or ""),
|
|
|
|
|
|
celery_task_id=celery_task_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _update_waiting_resource(wait_result: dict):
|
|
|
|
|
|
usage = wait_result.get("usage", {}) if isinstance(wait_result, dict) else {}
|
|
|
|
|
|
reason = wait_result.get("reason", "等待资源可用") if isinstance(wait_result, dict) else "等待资源可用"
|
|
|
|
|
|
self.update_state(
|
|
|
|
|
|
task_id=celery_task_id,
|
|
|
|
|
|
state="WAITING_RESOURCE",
|
|
|
|
|
|
meta={
|
|
|
|
|
|
"status": "waiting_resource",
|
|
|
|
|
|
"current_stage": stage,
|
|
|
|
|
|
"progress": 0,
|
|
|
|
|
|
"message": reason,
|
|
|
|
|
|
"resource_usage": usage,
|
|
|
|
|
|
"resource_profile": resource_profile,
|
|
|
|
|
|
"logs": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"time": datetime.now().isoformat(),
|
|
|
|
|
|
"level": "info",
|
|
|
|
|
|
"message": reason,
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
acquire_result = resource_lease.wait(update_state=_update_waiting_resource)
|
|
|
|
|
|
resource_acquired = True
|
|
|
|
|
|
print(f"[Celery] Resource lease granted for {task_id}: {acquire_result.get('usage')}")
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"[Celery] Resource governor unavailable, continuing without lease: {exc}")
|
|
|
|
|
|
|
|
|
|
|
|
# 更新任务状态
|
|
|
|
|
|
self.update_state(
|
|
|
|
|
|
task_id=celery_task_id,
|
|
|
|
|
|
state='RUNNING',
|
|
|
|
|
|
meta={
|
|
|
|
|
|
'current_stage': stage,
|
|
|
|
|
|
'progress': 10,
|
|
|
|
|
|
'message': (
|
|
|
|
|
|
f"开始执行 {stage}(请求CPU {params.get('requested_cpu') or params.get('cpu')},"
|
|
|
|
|
|
f"调度CPU {params.get('cpu')},{resource_profile.get('resource_class')}队列)..."
|
|
|
|
|
|
),
|
|
|
|
|
|
'logs': [
|
|
|
|
|
|
{
|
|
|
|
|
|
'time': datetime.now().isoformat(),
|
|
|
|
|
|
'level': 'info',
|
|
|
|
|
|
'message': (
|
|
|
|
|
|
f"开始执行 {stage}; requested_cpu={params.get('requested_cpu')}, "
|
|
|
|
|
|
f"effective_cpu={params.get('cpu')}, queue_class={resource_profile.get('resource_class')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# /api/execute 是“用户已明确选择阶段”的入口。这里直接调用
|
|
|
|
|
|
# StageToolRegistry,避免再走自然语言规划/PreFlight,把
|
|
|
|
|
|
# rnaseq_enrich 误展开成 rnaseq_qc 并错误要求 FASTQ。
|
|
|
|
|
|
try:
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
from gfa_core.mas.tools.stage_tools import stage_registry
|
|
|
|
|
|
|
|
|
|
|
|
prefix = resolve_effective_prefix(job_id, prefix, stage)
|
|
|
|
|
|
if params is not None:
|
|
|
|
|
|
params["prefix"] = prefix
|
|
|
|
|
|
params = attach_semantic_execution_context(job_id, stage, prefix, params)
|
|
|
|
|
|
print(f"[Celery] Effective prefix for {stage}: {prefix or '<empty>'}")
|
|
|
|
|
|
if params.get("resolvedArtifacts"):
|
|
|
|
|
|
print(f"[Celery] Resolved artifacts for {stage}: {params.get('resolvedArtifacts')}")
|
|
|
|
|
|
qa_preflight = _diagnose_stage_gate(job_id, stage, prefix, gate="preflight", params=params)
|
|
|
|
|
|
qa_repair = {}
|
|
|
|
|
|
if qa_preflight.get("runtime_repair_candidates"):
|
|
|
|
|
|
execute_heavy = bool((params or {}).get("diagnosisHeavyRepair", True))
|
|
|
|
|
|
qa_repair = _apply_runtime_repairs(
|
|
|
|
|
|
job_id,
|
|
|
|
|
|
stage,
|
|
|
|
|
|
prefix,
|
|
|
|
|
|
qa_preflight,
|
|
|
|
|
|
execute_heavy=execute_heavy,
|
|
|
|
|
|
)
|
|
|
|
|
|
qa_preflight = _diagnose_stage_gate(job_id, stage, prefix, gate="preflight", params=params)
|
|
|
|
|
|
blocking_preflight = [
|
|
|
|
|
|
issue for issue in qa_preflight.get("issues", [])
|
|
|
|
|
|
if issue.get("severity") == "blocking" or issue.get("blocks_downstream")
|
|
|
|
|
|
]
|
|
|
|
|
|
if blocking_preflight:
|
|
|
|
|
|
error_msg = "❌ 诊断门控阻断执行:发现输入/上游产物存在会导致阶段失败的问题"
|
|
|
|
|
|
return _attach_diagnosis_to_failure({
|
|
|
|
|
|
'status': 'failed',
|
|
|
|
|
|
'success': False,
|
|
|
|
|
|
'current_stage': stage,
|
|
|
|
|
|
'progress': 0,
|
|
|
|
|
|
'session_id': session_id or job_id,
|
|
|
|
|
|
'message': error_msg,
|
|
|
|
|
|
'errors': [error_msg],
|
|
|
|
|
|
'diagnosis': qa_preflight,
|
|
|
|
|
|
'qa_preflight': qa_preflight,
|
|
|
|
|
|
'qa_runtime_repair': qa_repair,
|
|
|
|
|
|
'semantic_preflight': params.get("semanticPreflight"),
|
|
|
|
|
|
'resolved_artifacts': params.get("resolvedArtifacts", {}),
|
|
|
|
|
|
'resource_profile': resource_profile,
|
|
|
|
|
|
'logs': [
|
|
|
|
|
|
{'time': datetime.now().isoformat(), 'level': 'error', 'message': error_msg}
|
|
|
|
|
|
]
|
|
|
|
|
|
}, job_id, stage, prefix)
|
|
|
|
|
|
direct_result = asyncio.run(stage_registry.execute(
|
|
|
|
|
|
stage_id=stage,
|
|
|
|
|
|
job_id=job_id,
|
|
|
|
|
|
prefix=prefix,
|
|
|
|
|
|
extra_params=params or {},
|
|
|
|
|
|
))
|
|
|
|
|
|
print(f"[Celery] Direct single-stage result: {direct_result}")
|
|
|
|
|
|
|
|
|
|
|
|
success = direct_result.get("status") == "success"
|
|
|
|
|
|
if success:
|
|
|
|
|
|
result_manifest = direct_result.get("manifest") if isinstance(direct_result.get("manifest"), dict) else {}
|
|
|
|
|
|
result_stage = _nonempty_prefix(result_manifest.get("stage")) or stage
|
|
|
|
|
|
result_prefix = _nonempty_prefix(result_manifest.get("prefix")) or prefix
|
|
|
|
|
|
artifact_failure = validate_single_stage_artifacts(job_id, result_prefix, result_stage, {
|
|
|
|
|
|
"final_output": {
|
|
|
|
|
|
"message": direct_result.get("message", ""),
|
|
|
|
|
|
"manifest": result_manifest,
|
|
|
|
|
|
"manifest_path": direct_result.get("manifest_path"),
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
if artifact_failure:
|
|
|
|
|
|
artifact_failure["session_id"] = session_id or job_id
|
|
|
|
|
|
return _attach_diagnosis_to_failure(artifact_failure, job_id, result_stage, result_prefix)
|
|
|
|
|
|
postflight_params = _merge_manifest_identity_params(params, result_manifest)
|
|
|
|
|
|
qa_postflight = _diagnose_stage_gate(job_id, result_stage, result_prefix, gate="postflight", params=postflight_params)
|
|
|
|
|
|
qa_post_repair = {}
|
|
|
|
|
|
if qa_postflight.get("runtime_repair_candidates"):
|
|
|
|
|
|
qa_post_repair = _apply_runtime_repairs(
|
|
|
|
|
|
job_id,
|
|
|
|
|
|
result_stage,
|
|
|
|
|
|
result_prefix,
|
|
|
|
|
|
qa_postflight,
|
|
|
|
|
|
execute_heavy=bool((params or {}).get("diagnosisHeavyRepair", True)),
|
|
|
|
|
|
)
|
|
|
|
|
|
qa_postflight = _diagnose_stage_gate(job_id, result_stage, result_prefix, gate="postflight", params=postflight_params)
|
|
|
|
|
|
blocking_postflight = [
|
|
|
|
|
|
issue for issue in qa_postflight.get("issues", [])
|
|
|
|
|
|
if issue.get("severity") == "blocking" or issue.get("blocks_downstream")
|
|
|
|
|
|
]
|
|
|
|
|
|
if blocking_postflight:
|
|
|
|
|
|
error_msg = "❌ 阶段已运行,但诊断 postflight 发现关键产物不可安全下游使用"
|
|
|
|
|
|
return _attach_diagnosis_to_failure({
|
|
|
|
|
|
'status': 'failed',
|
|
|
|
|
|
'success': False,
|
|
|
|
|
|
'current_stage': result_stage,
|
|
|
|
|
|
'progress': 100,
|
|
|
|
|
|
'session_id': session_id or job_id,
|
|
|
|
|
|
'message': error_msg,
|
|
|
|
|
|
'errors': [error_msg],
|
|
|
|
|
|
'result': direct_result,
|
|
|
|
|
|
'diagnosis': qa_postflight,
|
|
|
|
|
|
'qa_preflight': qa_preflight,
|
|
|
|
|
|
'qa_runtime_repair': qa_repair,
|
|
|
|
|
|
'qa_postflight': qa_postflight,
|
|
|
|
|
|
'qa_postflight_repair': qa_post_repair,
|
|
|
|
|
|
'semantic_preflight': params.get("semanticPreflight"),
|
|
|
|
|
|
'resolved_artifacts': params.get("resolvedArtifacts", {}),
|
|
|
|
|
|
'resource_profile': resource_profile,
|
|
|
|
|
|
'logs': [
|
|
|
|
|
|
{'time': datetime.now().isoformat(), 'level': 'error', 'message': error_msg}
|
|
|
|
|
|
]
|
|
|
|
|
|
}, job_id, result_stage, result_prefix)
|
|
|
|
|
|
resource_outcome = "completed"
|
|
|
|
|
|
return {
|
|
|
|
|
|
'status': 'completed',
|
|
|
|
|
|
'success': True,
|
|
|
|
|
|
'current_stage': result_stage,
|
|
|
|
|
|
'progress': 100,
|
|
|
|
|
|
'message': f'{result_stage} 执行完成',
|
|
|
|
|
|
'result': direct_result,
|
|
|
|
|
|
'qa_preflight': qa_preflight,
|
|
|
|
|
|
'qa_runtime_repair': qa_repair,
|
|
|
|
|
|
'qa_postflight': qa_postflight,
|
|
|
|
|
|
'qa_postflight_repair': qa_post_repair,
|
|
|
|
|
|
'semantic_preflight': params.get("semanticPreflight"),
|
|
|
|
|
|
'resolved_artifacts': params.get("resolvedArtifacts", {}),
|
|
|
|
|
|
'resource_profile': resource_profile,
|
|
|
|
|
|
'logs': [
|
|
|
|
|
|
{'time': datetime.now().isoformat(), 'level': 'success', 'message': direct_result.get("message", f'{stage} 执行成功')}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
error_msg = direct_result.get("error") or direct_result.get("message") or f"{stage} 执行失败"
|
|
|
|
|
|
qa_failure = _diagnose_stage_gate(job_id, stage, prefix, gate="failure", params=params)
|
|
|
|
|
|
return _attach_diagnosis_to_failure({
|
|
|
|
|
|
'status': 'failed',
|
|
|
|
|
|
'success': False,
|
|
|
|
|
|
'current_stage': stage,
|
|
|
|
|
|
'progress': 0,
|
|
|
|
|
|
'session_id': session_id or job_id,
|
|
|
|
|
|
'message': error_msg,
|
|
|
|
|
|
'errors': [error_msg],
|
|
|
|
|
|
'result': direct_result,
|
|
|
|
|
|
'diagnosis': qa_failure,
|
|
|
|
|
|
'qa_preflight': qa_preflight,
|
|
|
|
|
|
'qa_runtime_repair': qa_repair,
|
|
|
|
|
|
'qa_failure': qa_failure,
|
|
|
|
|
|
'semantic_preflight': params.get("semanticPreflight"),
|
|
|
|
|
|
'resolved_artifacts': params.get("resolvedArtifacts", {}),
|
|
|
|
|
|
'resource_profile': resource_profile,
|
|
|
|
|
|
'logs': [
|
|
|
|
|
|
{'time': datetime.now().isoformat(), 'level': 'error', 'message': error_msg}
|
|
|
|
|
|
]
|
|
|
|
|
|
}, job_id, stage, prefix)
|
|
|
|
|
|
except Exception as direct_exc:
|
|
|
|
|
|
print(f"[Celery] Direct single-stage execution failed, falling back to orchestrator: {direct_exc}")
|
|
|
|
|
|
|
|
|
|
|
|
# 获取Orchestrator
|
|
|
|
|
|
orchestrator = get_orchestrator()
|
|
|
|
|
|
|
|
|
|
|
|
# 构建初始状态(单stage模式)
|
|
|
|
|
|
from gfa_core.mas.core.langgraph_orchestrator import GraphState
|
|
|
|
|
|
initial_state: GraphState = {
|
|
|
|
|
|
"query": f"执行{stage}分析",
|
|
|
|
|
|
"session_id": session_id or task_id,
|
|
|
|
|
|
"job_id": job_id,
|
|
|
|
|
|
"files": [],
|
|
|
|
|
|
"current_agent": "router",
|
|
|
|
|
|
"status": "running",
|
|
|
|
|
|
"errors": [],
|
|
|
|
|
|
"final_output": {},
|
|
|
|
|
|
"messages": [],
|
|
|
|
|
|
"retry_count": 0,
|
|
|
|
|
|
"max_retries": 3,
|
|
|
|
|
|
"user_input": {
|
|
|
|
|
|
"prefix": prefix,
|
|
|
|
|
|
"stage": stage, # 指定要执行的stage
|
|
|
|
|
|
"params": params or {}
|
|
|
|
|
|
},
|
|
|
|
|
|
"single_stage_mode": True, # 标记为单stage模式
|
|
|
|
|
|
"target_stage": stage # 目标stage
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
print(f"[tasks.py] 单stage模式: target={stage}, prefix={prefix}")
|
|
|
|
|
|
|
|
|
|
|
|
# 运行LangGraph
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
result = asyncio.run(orchestrator.run(initial_state))
|
|
|
|
|
|
|
|
|
|
|
|
# 处理结果
|
|
|
|
|
|
if result.get("status") in ["completed", "success"]:
|
|
|
|
|
|
# 单阶段任务的最终成功标准必须是真实产物,而不是仅依赖
|
|
|
|
|
|
# LangGraph/Celery 的运行状态。
|
|
|
|
|
|
artifact_failure = validate_single_stage_artifacts(job_id, prefix, stage, result)
|
|
|
|
|
|
if artifact_failure:
|
|
|
|
|
|
artifact_failure["session_id"] = session_id or job_id
|
|
|
|
|
|
return _attach_diagnosis_to_failure(artifact_failure, job_id, stage, prefix)
|
|
|
|
|
|
|
|
|
|
|
|
# ============ 生成累积解读报告(新增) ============
|
|
|
|
|
|
try:
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
from gfa_core.mas.agents.result_parser import create_result_parser_agent
|
|
|
|
|
|
|
|
|
|
|
|
# 获取已完成的阶段列表(统一 manifest/artifacts 校验)
|
|
|
|
|
|
from gfa_core.mas.tools.stage_status import list_completed_stages
|
|
|
|
|
|
|
|
|
|
|
|
completed_stages = list_completed_stages(job_id=job_id, prefix=prefix)
|
|
|
|
|
|
print(f"[Celery DEBUG] Unified completed_stages: {completed_stages}")
|
|
|
|
|
|
|
|
|
|
|
|
# 按顺序排序
|
|
|
|
|
|
stage_order = ["identify", "phylogeny", "structure", "motif", "domain", "promoter", "synteny", "interspecies_synteny", "karyotype", "expression", "kaks", "ppi"]
|
|
|
|
|
|
completed_stages = [s for s in stage_order if s in completed_stages] + [s for s in completed_stages if s not in stage_order]
|
|
|
|
|
|
|
|
|
|
|
|
print(f"[Celery] 生成累积解读,已完成阶段: {completed_stages}")
|
|
|
|
|
|
|
|
|
|
|
|
# 创建 ResultParserAgent(不需要 LLMManager)
|
|
|
|
|
|
agent = create_result_parser_agent(None)
|
|
|
|
|
|
|
|
|
|
|
|
# 生成累积解读报告
|
|
|
|
|
|
report = asyncio.run(agent.generate_cumulative_report(
|
|
|
|
|
|
job_id=job_id,
|
|
|
|
|
|
prefix=prefix,
|
|
|
|
|
|
completed_stages=completed_stages
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
# 保存解读报告
|
|
|
|
|
|
save_success = agent.save_interpretation(job_id, prefix, report)
|
|
|
|
|
|
|
|
|
|
|
|
if save_success:
|
|
|
|
|
|
print(f"[Celery] 解读报告已保存到 interpretation.json")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"[Celery] 保存解读报告失败")
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
# 解读失败不阻塞主流程
|
|
|
|
|
|
print(f"[Celery] 生成解读报告失败(非阻塞): {e}")
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
|
|
|
|
|
|
resource_outcome = "completed"
|
|
|
|
|
|
return {
|
|
|
|
|
|
'status': 'completed',
|
|
|
|
|
|
'success': True, # 添加 success 字段,供前端判断
|
|
|
|
|
|
'current_stage': stage,
|
|
|
|
|
|
'progress': 100,
|
|
|
|
|
|
'message': f'{stage} 执行完成',
|
|
|
|
|
|
'result': result.get("final_output", {}),
|
|
|
|
|
|
'resource_profile': resource_profile,
|
|
|
|
|
|
'logs': [
|
|
|
|
|
|
{'time': datetime.now().isoformat(), 'level': 'success', 'message': f'{stage} 执行成功'}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 检查是否有等待确认的状态(优先检查 result.status)
|
|
|
|
|
|
if result.get("status") == "waiting_confirmation":
|
|
|
|
|
|
pending_confirmation = result.get("pending_confirmation", {})
|
|
|
|
|
|
# 将确认状态存储到Celery任务meta中
|
|
|
|
|
|
return {
|
|
|
|
|
|
'status': 'waiting_confirmation',
|
|
|
|
|
|
'current_stage': stage,
|
|
|
|
|
|
'progress': 50,
|
|
|
|
|
|
'message': '等待用户确认修复方案',
|
|
|
|
|
|
'pending_confirmation': pending_confirmation,
|
|
|
|
|
|
'logs': [
|
|
|
|
|
|
{'time': datetime.now().isoformat(), 'level': 'warning', 'message': '检测到错误,等待用户确认修复方案'}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 旧逻辑:检查 pending_confirmation.status == "waiting"(兼容)
|
|
|
|
|
|
pending_confirmation = result.get("pending_confirmation")
|
|
|
|
|
|
if pending_confirmation and pending_confirmation.get("status") == "waiting":
|
|
|
|
|
|
# 将确认状态存储到Celery任务meta中
|
|
|
|
|
|
return {
|
|
|
|
|
|
'status': 'waiting_confirmation',
|
|
|
|
|
|
'current_stage': stage,
|
|
|
|
|
|
'progress': 50,
|
|
|
|
|
|
'message': '等待用户确认修复方案',
|
|
|
|
|
|
'pending_confirmation': pending_confirmation,
|
|
|
|
|
|
'logs': [
|
|
|
|
|
|
{'time': datetime.now().isoformat(), 'level': 'warning', 'message': '检测到错误,等待用户确认修复方案'}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 构建详细错误消息:优先保留 run_stage/stage_tools 返回的业务错误,
|
|
|
|
|
|
# 避免被后续 manifest 校验错误覆盖成 "Manifest not found"。
|
|
|
|
|
|
final_output = result.get("final_output", {}) or {}
|
|
|
|
|
|
execution_results = result.get("execution_results", []) or []
|
|
|
|
|
|
business_error = (
|
|
|
|
|
|
final_output.get("response")
|
|
|
|
|
|
or final_output.get("message")
|
|
|
|
|
|
or final_output.get("error")
|
|
|
|
|
|
or result.get("execution_error")
|
|
|
|
|
|
)
|
|
|
|
|
|
if not business_error:
|
|
|
|
|
|
for item in execution_results:
|
|
|
|
|
|
stage_result = item.get("result", {}) if isinstance(item, dict) else {}
|
|
|
|
|
|
candidate = stage_result.get("error") or stage_result.get("message")
|
|
|
|
|
|
if candidate:
|
|
|
|
|
|
business_error = candidate
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
if isinstance(business_error, str) and (
|
|
|
|
|
|
"❌" in business_error
|
|
|
|
|
|
or "failed" in business_error.lower()
|
|
|
|
|
|
or "无法" in business_error
|
|
|
|
|
|
or "缺少" in business_error
|
|
|
|
|
|
):
|
|
|
|
|
|
error_msg = business_error
|
|
|
|
|
|
else:
|
|
|
|
|
|
error_msg = f'{stage} 执行失败'
|
|
|
|
|
|
diagnosis = result.get("diagnosis_result", {})
|
|
|
|
|
|
if diagnosis:
|
|
|
|
|
|
suggestion = diagnosis.get("suggestion", "")
|
|
|
|
|
|
error_type = diagnosis.get("error_type", "")
|
|
|
|
|
|
if suggestion:
|
|
|
|
|
|
error_msg += f"\n\n诊断: {error_type}\n建议: {suggestion}"
|
|
|
|
|
|
|
|
|
|
|
|
return _attach_diagnosis_to_failure({
|
|
|
|
|
|
'status': 'failed',
|
|
|
|
|
|
'success': False, # 添加 success 字段,供前端判断
|
|
|
|
|
|
'current_stage': stage,
|
|
|
|
|
|
'progress': 0,
|
|
|
|
|
|
'session_id': session_id or job_id,
|
|
|
|
|
|
'message': error_msg,
|
|
|
|
|
|
'errors': result.get("errors", ["Unknown error"]),
|
|
|
|
|
|
'diagnosis': diagnosis,
|
|
|
|
|
|
'logs': [
|
|
|
|
|
|
{'time': datetime.now().isoformat(), 'level': 'error', 'message': error_msg}
|
|
|
|
|
|
]
|
|
|
|
|
|
}, job_id, stage, prefix)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
error_msg = f"任务异常: {str(e)}\n{traceback.format_exc()}"
|
|
|
|
|
|
print(f"[Celery] Single stage task {task_id} failed: {error_msg}")
|
|
|
|
|
|
celery_task_id = self.request.id if self.request else task_id
|
|
|
|
|
|
self.update_state(
|
|
|
|
|
|
task_id=celery_task_id,
|
|
|
|
|
|
state='FAILURE',
|
|
|
|
|
|
meta={
|
|
|
|
|
|
'exc_type': type(e).__name__,
|
|
|
|
|
|
'exc_message': str(e),
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
raise
|
|
|
|
|
|
finally:
|
|
|
|
|
|
if resource_acquired and resource_lease is not None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
resource_lease.release(status=resource_outcome, message=f"{stage} {resource_outcome}")
|
|
|
|
|
|
except Exception as release_exc:
|
|
|
|
|
|
print(f"[Celery] Failed to release resource lease for {task_id}: {release_exc}")
|