Files
PlantAI-dev/apps/web/vue3/main.py.bak_config_upload
2026-08-03 18:42:47 +08:00

3765 lines
150 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
BotanicalBaseAgent - FastAPI Backend
支持Vue3前端的API接口 - Celery任务队列模式
"""
import os
import sys
import json
import asyncio
import uuid
import time
from pathlib import Path
from datetime import datetime
from typing import Optional, List, Dict
from fastapi import FastAPI, UploadFile, File, HTTPException, Request, BackgroundTasks, Query
from fastapi.responses import JSONResponse, FileResponse, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from concurrent.futures import ThreadPoolExecutor
# 添加项目路径
PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
# 全局状态存储(用于等待用户确认)
pending_confirmations: Dict[str, Dict] = {}
# 会话状态存储
session_states: Dict[str, Dict] = {}
# 导入Celery任务
try:
from tasks import execute_agent_task, execute_single_stage_task
CELERY_AVAILABLE = True
print("✅ Celery任务导入成功")
except ImportError as e:
CELERY_AVAILABLE = False
print(f"⚠️ Celery任务导入失败: {e}")
# 导入多Agent系统组件
try:
from gfa_core.mas.core.orchestrator import Orchestrator
from gfa_core.mas.core.langgraph_orchestrator import LangGraphOrchestrator, GraphState
from gfa_core.mas.llm import LLMManager
from gfa_core.mas.memory import MemoryManager
from gfa_core.mas.api.conversation_routes import router as conv_router # 会话管理路由
MAS_AVAILABLE = True
LANGGRAPH_AVAILABLE = True
print("✅ MAS组件导入成功")
print("✅ LangGraph编排器导入成功")
except ImportError as e:
MAS_AVAILABLE = False
LANGGRAPH_AVAILABLE = False
print(f"⚠️ MAS组件导入失败: {e}")
Orchestrator = None
LangGraphOrchestrator = None
LLMManager = None
MemoryManager = None
WORKSPACE_ROOT = PROJECT_ROOT / "workspace"
INPUT_ROOT = PROJECT_ROOT / "input"
def _nonempty_prefix(value: object) -> str:
text = str(value or "").strip()
return text if text and text.lower() not in {"none", "null", "undefined"} else ""
def _gene_family_from_config(config_path: Path) -> Optional[str]:
if not config_path.exists():
return None
try:
import pandas as pd
df = pd.read_excel(config_path)
if "Gene_Family" not in df.columns:
return None
for value in df["Gene_Family"].dropna().astype(str):
value = value.strip()
if value:
return value
except Exception as exc:
print(f"[WARN] Failed to infer gene family from {config_path}: {exc}")
return None
def _prefix_from_manifest(path: Path) -> str:
if not path.exists():
return ""
try:
with open(path, "r", encoding="utf-8") as handle:
data = json.load(handle)
for key in ("prefix", "effective_prefix"):
value = _nonempty_prefix(data.get(key))
if value:
return value
target = data.get("target") if isinstance(data.get("target"), dict) else None
if target:
value = _nonempty_prefix(target.get("prefix"))
if value:
return value
artifacts = data.get("artifacts") if isinstance(data.get("artifacts"), dict) else {}
for value in artifacts.values():
if isinstance(value, str):
name = Path(value).name
if name.startswith("Multi.") and ".genome_prepare.manifest" in name:
return name.split(".genome_prepare.manifest", 1)[0]
except Exception as exc:
print(f"[WARN] Failed to read prefix manifest {path}: {exc}")
return ""
def _prefix_from_final_members(final_dir: Path) -> str:
if not final_dir.exists():
return ""
for file in sorted(final_dir.glob("Multi.*.final_members.pep.fa")):
name = file.name
if name.endswith(".final_members.pep.fa"):
return name[:-len(".final_members.pep.fa")]
return ""
def _prefix_from_ref_combined(job_dir: Path) -> str:
for base in (job_dir, job_dir / "final"):
if not base.exists():
continue
for file in sorted(base.glob("Ref_combined.*.pep.fasta")):
parts = file.name.split(".")
if len(parts) >= 3:
return f"Multi.{parts[1]}"
return ""
def resolve_effective_prefix(job_id: str, params: Dict, stage: str) -> str:
provided = _nonempty_prefix(params.get("prefix"))
if provided:
return provided
job_dir = INPUT_ROOT / job_id
workspace_dir = WORKSPACE_ROOT / job_id
final_dir = workspace_dir / "final"
species_config = job_dir / "genome_prepare" / "species_config.json"
if species_config.exists():
try:
with open(species_config, "r", encoding="utf-8") as handle:
data = json.load(handle)
target = data.get("target") if isinstance(data.get("target"), dict) else None
if target:
value = _nonempty_prefix(target.get("prefix"))
if value:
return value
value = _nonempty_prefix(data.get("prefix"))
if value:
return value
except Exception as exc:
print(f"[WARN] Failed to read species_config prefix: {exc}")
manifests = []
manifests.extend(sorted(final_dir.glob("Multi.*.genome_prepare.manifest.json")))
manifests.extend(sorted((job_dir / "final").glob("Multi.*.genome_prepare.manifest.json")))
manifests.extend(sorted(final_dir.glob("*.genome_prepare.manifest.json")))
manifests.extend(sorted((job_dir / "final").glob("*.genome_prepare.manifest.json")))
for manifest in manifests:
value = _prefix_from_manifest(manifest)
if value:
return value
family = _gene_family_from_config(job_dir / "config.xlsx")
if family:
return f"Multi.{family}"
value = _prefix_from_ref_combined(job_dir)
if value:
return value
value = _prefix_from_final_members(final_dir)
if value:
return value
return ""
def _attach_single_stage_status(task: Dict) -> None:
"""Derive task status from the unified manifest/artifact validator when possible."""
job_id = task.get("job_id")
stage = task.get("stage")
prefix = task.get("prefix", "")
if not job_id or not stage:
return
try:
from gfa_core.mas.tools.stage_status import get_stage_status as get_unified_stage_status
mapped_stage = {
"rnaseq_go_enrich": "rnaseq_enrich",
"rnaseq_kegg_enrich": "rnaseq_enrich",
}.get(stage, stage)
report = get_unified_stage_status(job_id=job_id, prefix=prefix, stage=mapped_stage)
if stage in {"rnaseq_go_enrich", "rnaseq_kegg_enrich"} and report.completed:
final_dir = WORKSPACE_ROOT / job_id / "final"
suffix = ".GO_enrich.txt" if stage == "rnaseq_go_enrich" else ".KEGG_enrich.txt"
if not any(final_dir.glob(f"*{suffix}")):
report.completed = False
report.status = "not_found"
report.errors.append(f"{stage} specific output not found: *{suffix}")
task["stage_status"] = report.to_dict()
if report.completed:
task["status"] = "success"
task["progress"] = 100
task["current_stage"] = stage
if not task.get("end_time"):
task["end_time"] = datetime.now().isoformat()
if not task.get("message") or task.get("message", "").startswith("任务 "):
task["message"] = f"✅ {stage} 分析已完成"
elif task.get("status") in {"success", "completed"}:
task["status"] = "failed"
task["message"] = f"❌ {stage} 未生成有效输出"
except Exception as e:
print(f"[API DEBUG] Failed to attach unified stage status: {e}")
def _attach_annotation_progress(task: Dict) -> None:
"""Attach long-running annotation progress written by annotation_adapter."""
job_id = task.get("job_id")
stage = task.get("stage")
if not job_id or stage not in {"annotation", "rnaseq_enrich", "rnaseq_go_enrich", "rnaseq_kegg_enrich"}:
return
progress_path = WORKSPACE_ROOT / job_id / "rnaseq_annotation" / "progress.json"
if not progress_path.exists():
return
try:
progress = json.loads(progress_path.read_text(encoding="utf-8"))
except Exception as e:
print(f"[API DEBUG] Failed to read annotation progress: {e}")
return
task["annotation_progress"] = progress
if task.get("status") in {"pending", "running"}:
task["status"] = "running"
task["progress"] = progress.get("progress", task.get("progress", 0))
task["current_stage"] = "annotation"
total = progress.get("total", 0)
processed = progress.get("processed", 0)
annotated = progress.get("annotated", 0)
failed = progress.get("failed", 0)
task["message"] = (
f"自动背景注释进行中:{processed}/{total},"
f"已注释 {annotated},失败 {failed}"
)
def _attach_agent_manifest_status(task: Dict) -> None:
"""Attach completed stages from manifests to an agent task."""
job_id = task.get("job_id")
prefix = task.get("prefix", "")
if not job_id:
return
try:
from gfa_core.mas.tools.stage_status import list_completed_stages
completed = list_completed_stages(job_id=job_id, prefix=prefix)
task["stages_completed"] = completed
task["completed_stages"] = completed
except Exception as e:
print(f"[API DEBUG] Failed to attach agent manifest status: {e}")
app = FastAPI(title="BotanicalBaseAgent API")
# 挂载静态文件目录
WEB_DIR = Path(__file__).parent
STATIC_DIR = WEB_DIR / "static"
STATIC_DIR.mkdir(exist_ok=True)
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
# 挂载会话管理路由
if MAS_AVAILABLE and conv_router:
app.include_router(conv_router, prefix="/api")
print("✅ 会话管理路由已挂载")
# 添加CORS headers到所有响应
@app.middleware("http")
async def add_cors_headers(request, call_next):
if request.method == "OPTIONS":
return Response(
status_code=200,
headers={
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
}
)
response = await call_next(request)
response.headers["Access-Control-Allow-Origin"] = "*"
return response
# 确保目录存在
WORKSPACE_ROOT.mkdir(exist_ok=True)
INPUT_ROOT.mkdir(exist_ok=True)
# 内存存储
jobs = {}
current_job_id = "default_job"
# 异步任务存储
tasks: Dict[str, dict] = {}
executor = ThreadPoolExecutor(max_workers=4)
# 初始化多Agent系统
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()
# 注入阿里云百炼配置
llm_config["openai_api_key"] = mas_config.settings.openai_api_key
llm_config["openai_base_url"] = mas_config.settings.openai_base_url
print(f"[DEBUG] openai_api_key configured: {bool(llm_config.get('openai_api_key'))}")
print(f"[DEBUG] openai_base_url: {llm_config.get('openai_base_url', 'NOT SET')}")
# 强制重新创建LLMManager,不使用任何缓存
from gfa_core.mas.llm import LLMManager
llm_manager = LLMManager(config=llm_config)
# 清除provider缓存
llm_manager._providers = {}
llm_manager._available_providers = None
memory_manager = MemoryManager()
# 初始化原 Orchestrator(保留作为参考)
orchestrator = Orchestrator(
llm_manager=llm_manager,
memory_manager=memory_manager,
papers_dir=PROJECT_ROOT / "paper" / "gfpaper" / "buchong"
)
print("✅ Orchestrator初始化成功(保留参考)")
# 初始化 LangGraph Orchestrator(新版本)
if LANGGRAPH_AVAILABLE:
langgraph_orchestrator = LangGraphOrchestrator(llm_manager=llm_manager)
print("✅ LangGraph Orchestrator初始化成功")
except Exception as e:
print(f"⚠️ Orchestrator初始化失败: {e}")
import traceback
print(traceback.format_exc())
orchestrator = None
langgraph_orchestrator = None
# 需要异步轮询的阶段(耗时长,可能超过HTTP超时)
# 分析阶段耗时评估:
# - identify: HMMER+BLAST搜索,通常3-10分钟,必须异步
# - phylogeny: MAFFT建树+IQTree引导树,通常5-30分钟,必须异步
# - motif: MEME分析,通常5-20分钟,必须异步
# - domain: PfamScan搜索,通常3-10分钟,必须异步
# - structure: AlphaFold预测(如启用),可能数小时,必须异步
# - synteny: MCScanX共线性分析,通常5-15分钟,已包含
# - interspecies_synteny: 跨物种共线性分析,通常10-30分钟,已包含
# - karyotype: 染色体可视化,通常2-5分钟,已包含
# - pep: 多物种pep分析,通常5-15分钟,已包含
# - promoter: PlantCARE提交+等待,通常数小时,已包含
# - ppi: STRING蛋白互作网络,通常2-10分钟,加入异步
# - kaks: KaKs计算,通常2-10分钟,加入异步
# - expression: GEO数据下载+分析,通常10-30分钟,加入异步
# - rnaseq_*: RNA-seq全流程,每个阶段10-60分钟,已包含
ASYNC_STAGES = {
# 核心分析阶段(耗时长)
"identify", "phylogeny", "motif", "domain", "structure",
# 共线性分析
"synteny", "interspecies_synteny", "karyotype",
# 组合图
"combo",
# 其他分析
"pep", "ppi", "kaks", "expression", "msa", "ortholog",
# 启动子分析(外部服务)
"promoter",
# 基因组准备
"genome_prepare",
# RNA-seq全流程
"rnaseq_qc", "rnaseq_align", "rnaseq_quant", "rnaseq_de",
"annotation", "rnaseq_enrich", "rnaseq_go_enrich", "rnaseq_kegg_enrich"
}
def _config_bool(value: object) -> bool:
if isinstance(value, bool):
return value
text = str(value or "").strip().lower()
return text in {"true", "1", "yes", "y", "ref"}
def _ordered_species_options(job_id: str) -> Dict[str, list]:
config_path = INPUT_ROOT / job_id / "config.xlsx"
targets = []
refs = []
if config_path.exists():
try:
import pandas as pd
df = pd.read_excel(config_path)
for _, row in df.iterrows():
species = str(row.get("Species_Code", "")).strip()
if not species or species.lower() == "nan" or species == "-":
continue
group = refs if _config_bool(row.get("Ref", False)) else targets
if species not in group:
group.append(species)
except Exception as exc:
print(f"[WARN] Failed to read interspecies species options from {config_path}: {exc}")
default_refs = []
for preferred in ("at", "ath"):
for species in refs:
if species.lower() == preferred:
default_refs = [species]
break
if default_refs:
break
if not default_refs and refs:
default_refs = [refs[0]]
return {
"targets": targets,
"refs": refs,
"default_targets": list(targets),
"default_refs": default_refs,
}
class ExecuteRequest(BaseModel):
stage: str
job_id: str = "" # 可选,指定job_id
prefix: str = "Test"
cpu: int = 8
idEvalue: float = 0.001
idCoverage: float = 0.3
blastEvalue: float = 0.00001
requiredPfams: str = ""
refSpecies: str = ""
mafftParams: str = "--auto --thread -1"
iqtreeParams: str = "-m MFP -bb 1000 -nt AUTO"
nmotifs: int = 10
minw: int = 6
maxw: int = 200
domEvalue: float = 0.00001
domCoverage: float = 0.3
idTransform: str = "none"
targetSpecies: List[str] = []
refSpeciesList: List[str] = []
blastEngine: str = "diamond"
geoId: str = ""
# RNA-seq参数
rnaseqMode: str = "upload"
speciesName: str = ""
# RNA-seq QC 参数
rnaseqDataMode: str = "upload"
rnaseqDataPath: str = ""
# RNA-seq Enrich参数
topPathways: int = 20 # 富集分析热图通路数量
enrichBackgroundSource: str = "auto" # auto, gene2go, orgdb
enrichSpecies: str = "" # 内置OrgDb物种代码
enrichKeyType: str = "auto" # OrgDb keyType
enrichType: str = "go" # go, kegg, both
keggOrganism: str = "" # KEGG organism code
rnaseqForegroundMode: str = "auto" # auto, global, family
# PPI参数
ppiSpecies: str = "ath" # PPI物种代码
ppiScore: int = 700 # PPI置信度阈值
# KaKs参数
kaksNeutralRate: float = 7.08e-9 # 中性替换率
# Genome Prepare参数
genomePrepareType: str = "auto" # auto, jgi, ensembl, ncbi
def run_stage_sync(task_id: str, stage: str, job_id: str, params: dict):
"""在后台线程中执行分析阶段"""
try:
# 更新任务状态为运行中
tasks[task_id]["status"] = "running"
tasks[task_id]["message"] = f"正在执行 {stage}..."
tasks[task_id]["start_time"] = datetime.now().isoformat()
# 导入pipeline_backend
from apps.web.pipeline_backend import run_stage, JobInfo
job_dir = INPUT_ROOT / job_id
workspace_dir = WORKSPACE_ROOT / job_id
effective_prefix = resolve_effective_prefix(job_id, params, stage)
if effective_prefix != _nonempty_prefix(params.get("prefix")):
print(f"[INFO] Resolved empty prefix for {stage}: {effective_prefix or '<empty>'}")
params["prefix"] = effective_prefix
tasks[task_id]["prefix"] = effective_prefix
job = JobInfo(
job_id=job_id,
input_dir=str(job_dir),
workspace_dir=str(workspace_dir),
logs_dir=str(workspace_dir / "logs"),
created_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
)
os.makedirs(job.logs_dir, exist_ok=True)
# 切换到项目根目录
original_cwd = os.getcwd()
os.chdir(PROJECT_ROOT)
# 特殊状态提示
if stage == "promoter":
tasks[task_id]["message"] = "正在提交序列到 PlantCARE..."
elif stage == "synteny":
tasks[task_id]["message"] = "正在进行共线性分析(MCScanX)..."
elif stage == "rnaseq_qc":
tasks[task_id]["message"] = "正在进行RNA-seq质控分析(FastQC + Cutadapt)... 这可能需要15-30分钟,请耐心等待..."
backend_stage = {
"rnaseq_go_enrich": "rnaseq_enrich",
"rnaseq_kegg_enrich": "rnaseq_enrich",
}.get(stage, stage)
# 执行
result_msg, manifest = run_stage(
stage=backend_stage,
job=job,
prefix=params["prefix"],
cpu=params["cpu"],
required_pfams=params.get("requiredPfams") or None,
ref_species=params.get("refSpecies") or None,
id_evalue=params.get("idEvalue", 0.001),
id_coverage=params.get("idCoverage", 0.3),
blast_evalue=params.get("blastEvalue", 0.00001),
phy_mafft=params.get("mafftParams", "--auto --thread -1"),
phy_iqtree=params.get("iqtreeParams", "-m MFP -bb 1000 -nt AUTO"),
motif_nmotifs=params.get("nmotifs", 10),
motif_minw=params.get("minw", 6),
motif_maxw=params.get("maxw", 200),
dom_evalue=params.get("domEvalue", 0.00001),
dom_coverage=params.get("domCoverage", 0.3),
id_transform=params.get("idTransform", "none"),
geo_id=params.get("geoId", ""),
# RNA-seq参数
rnaseq_mode=params.get("rnaseqMode", "upload"),
species_name=params.get("speciesName", ""),
# RNA-seq Enrich参数
top_pathways=params.get("topPathways", 20),
enrich_background_source=params.get("enrichBackgroundSource", "auto"),
enrich_species=params.get("enrichSpecies", ""),
enrich_key_type=params.get("enrichKeyType", "auto"),
enrich_type=params.get("enrichType", "go"),
kegg_organism=params.get("keggOrganism", ""),
rnaseq_foreground_mode=params.get("rnaseqForegroundMode", "auto"),
# PPI参数
ppi_species=params.get("ppiSpecies", "ath"),
ppi_score=params.get("ppiScore", 700),
# KaKs参数
kaks_neutral_rate=params.get("kaksNeutralRate", 7.08e-9),
# Genome Prepare参数
genome_prepare_type=params.get("genomePrepareType", "auto"),
target_species=params.get("targetSpecies") or None,
interspecies_ref_species=params.get("refSpeciesList") or None,
interspecies_blast_engine=params.get("blastEngine", "diamond") or "diamond",
)
# 恢复工作目录
os.chdir(original_cwd)
# 检查是否有特殊状态需要更新
if stage == "promoter" and "Waiting for PlantCARE" in result_msg:
tasks[task_id]["message"] = "已提交 PlantCARE,等待邮件结果(可能需要10-60分钟)..."
elif stage == "promoter" and "submitted" in result_msg.lower():
tasks[task_id]["message"] = "已提交 PlantCARE,正在等待邮件..."
# 收集结果 - 按stage分类
stage_prefix_map = {
"identify": ["identify", "final_members"],
"phylogeny": ["phylogeny", "tree"],
"motif": ["motif"],
"domain": ["domain"],
"structure": ["structure"],
"promoter": ["promoter"],
"synteny": ["synteny"],
"karyotype": ["karyotype"],
"interspecies_synteny": ["interspecies_synteny", "interspecies_tree"],
"ppi": ["ppi"],
"kaks": ["kaks"],
"pep": ["pep"],
"expression": ["exp", "expression"],
"combo": ["combo"],
"msa": ["msa"],
"ortholog": ["ortholog"],
"rnaseq_qc": ["rnaseq_qc"],
"rnaseq_align": ["rnaseq_align"],
"rnaseq_quant": ["rnaseq_quant"],
"rnaseq_de": ["rnaseq_de"],
"annotation": ["annotation", "gene2go", "gene2ko", "eggnog"],
"rnaseq_go_enrich": ["rnaseq_enrich", "go_enrich", "_go_", "gene2go"],
"rnaseq_kegg_enrich": ["rnaseq_enrich", "kegg_enrich", "_kegg_", "gene2ko"],
"rnaseq_enrich": ["rnaseq_enrich"],
"genome_prepare": ["genome_prepare"]
}
# 反转映射
prefix_to_stage = {}
for s, prefixes in stage_prefix_map.items():
for p in prefixes:
prefix_to_stage[p] = s
# 初始化各stage文件
stage_files = {s: {"images": [], "files": []} for s in stage_prefix_map}
final_dir = workspace_dir / "final"
if final_dir.exists():
for ext in ['.png', '.jpg', '.jpeg', '.svg']:
for f in final_dir.glob(f"*{ext}"):
# 提取前缀(支持 . 和 _ 分隔)
fname = f.stem
prefix = fname.split('.')[0].split('_')[0]
matched = prefix_to_stage.get(prefix, stage)
stage_files[matched]["images"].append(f"/workspace/{job_id}/final/{f.name}")
for f in final_dir.iterdir():
if f.is_file():
fname = f.stem
prefix = fname.split('.')[0].split('_')[0]
matched = prefix_to_stage.get(prefix, stage)
stage_files[matched]["files"].append({
"name": f.name,
"size": f.stat().st_size,
"path": f"/workspace/{job_id}/final/{f.name}"
})
# 汇总所有文件(保留兼容)
all_images = []
all_files = []
for s_data in stage_files.values():
all_images.extend(s_data["images"])
all_files.extend(s_data["files"])
# 收集PPI和KaKs结果文件
if final_dir.exists():
# PPI结果文件
for ppi_file in final_dir.glob("*.ppi_*"):
if ppi_file.is_file():
file_info = {
"name": ppi_file.name,
"size": ppi_file.stat().st_size,
"path": f"/workspace/{job_id}/final/{ppi_file.name}"
}
stage_files["ppi"]["files"].append(file_info)
all_files.append(file_info)
# KaKs结果文件
for kaks_file in final_dir.glob("*.kaks_*"):
if kaks_file.is_file():
file_info = {
"name": kaks_file.name,
"size": kaks_file.stat().st_size,
"path": f"/workspace/{job_id}/final/{kaks_file.name}"
}
stage_files["kaks"]["files"].append(file_info)
all_files.append(file_info)
# ?? manifest ????? final ?????????????? manifest/FASTA/tree ???
# ????????? manifest ???????? success/partial_success
expected_manifest = final_dir / f"{params['prefix']}.{stage}.manifest.json"
paused_manifest = final_dir / f"{params['prefix']}.{stage}.paused.json"
manifest_valid = False
manifest_status = None
import time
time.sleep(0.5)
manifest_to_check = paused_manifest if paused_manifest.exists() else expected_manifest
if manifest_to_check.exists():
try:
with open(manifest_to_check, 'r', encoding='utf-8') as f:
manifest_data = json.load(f)
manifest_status = manifest_data.get("status")
manifest_valid = (
manifest_status in {"success", "partial_success"} and
"?" not in result_msg
)
except Exception as e:
print(f"[WARN] Failed to read manifest {manifest_to_check}: {e}")
if not manifest_valid:
# manifest 验证失败
if isinstance(result_msg, str) and ("❌" in result_msg or "failed" in result_msg.lower()):
failure_message = result_msg
failure_error = result_msg
else:
failure_message = f"❌ {stage} 失败:manifest 状态为 {manifest_status}。{result_msg}"
failure_error = f"Manifest validation failed: expected {expected_manifest}, status={manifest_status}"
tasks[task_id]["status"] = "failed"
tasks[task_id]["message"] = failure_message
tasks[task_id]["end_time"] = datetime.now().isoformat()
tasks[task_id]["result"] = {
"success": False,
"error": failure_error,
"stage": stage
}
else:
# 更新任务状态为完成
tasks[task_id]["status"] = "success"
tasks[task_id]["message"] = result_msg
tasks[task_id]["end_time"] = datetime.now().isoformat()
tasks[task_id]["result"] = {
"success": True,
"message": result_msg,
"images": all_images,
"files": all_files,
"stage_files": stage_files, # 按stage分类的文件
"stage": stage
}
# ============ 生成累积解读报告(新增) ============
try:
if MAS_AVAILABLE and LLMManager:
from gfa_core.mas.agents.result_parser import create_result_parser_agent
# 获取已完成的阶段列表
completed_stages = []
if final_dir.exists():
for manifest_file in final_dir.glob(f"{params['prefix']}.*.manifest.json"):
try:
with open(manifest_file, 'r') as f:
m = json.load(f)
if m.get("status") == "success":
stage_name = m.get("stage")
if stage_name and stage_name not in completed_stages:
completed_stages.append(stage_name)
except:
continue
# 按顺序排序
stage_order = ["identify", "phylogeny", "structure", "motif", "domain", "promoter", "synteny", "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"[CumulativeInterpretation] 生成累积解读,已完成阶段: {completed_stages}")
# 创建 LLMManager 和 ResultParserAgent
llm_manager = LLMManager()
agent = create_result_parser_agent(llm_manager)
# 生成累积解读报告
report = asyncio.run(agent.generate_cumulative_report(
job_id=job_id,
prefix=params["prefix"],
completed_stages=completed_stages
))
# 保存解读报告
save_success = agent.save_interpretation(job_id, params["prefix"], report)
if save_success:
print(f"[CumulativeInterpretation] 解读报告已保存到 interpretation.json")
else:
print(f"[CumulativeInterpretation] 保存解读报告失败")
except Exception as e:
# 解读失败不阻塞主流程
print(f"[CumulativeInterpretation] 生成解读报告失败(非阻塞): {e}")
import traceback
traceback.print_exc()
# 返回任务结果,供 Celery 使用
return tasks[task_id]["result"]
except Exception as e:
tasks[task_id]["status"] = "failed"
tasks[task_id]["message"] = f"执行失败: {str(e)}"
tasks[task_id]["end_time"] = datetime.now().isoformat()
tasks[task_id]["result"] = {
"success": False,
"error": str(e),
"stage": stage
}
# 返回任务结果,供 Celery 使用
return tasks[task_id]["result"]
@app.get("/")
async def root():
"""返回Vue3前端页面"""
return FileResponse(
Path(__file__).parent / "index.html",
headers={
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0"
}
)
@app.post("/api/new_job")
async def create_new_job():
"""创建新工作目录"""
global current_job_id
import time
import uuid
current_job_id = f"job_{int(time.time()*1000)}_{uuid.uuid4().hex[:6]}"
job_dir = INPUT_ROOT / current_job_id
workspace_dir = WORKSPACE_ROOT / current_job_id
job_dir.mkdir(parents=True, exist_ok=True)
workspace_dir.mkdir(parents=True, exist_ok=True)
(workspace_dir / "logs").mkdir(exist_ok=True)
(workspace_dir / "final").mkdir(exist_ok=True)
return {
"success": True,
"job_id": current_job_id,
"input_dir": str(job_dir),
"workspace_dir": str(workspace_dir)
}
@app.get("/api/list_jobs")
async def list_existing_jobs():
"""列出所有已有的工作目录"""
jobs = []
# 扫描 workspace 目录
if WORKSPACE_ROOT.exists():
for job_dir in sorted(WORKSPACE_ROOT.iterdir(), key=lambda x: x.stat().st_mtime, reverse=True):
if job_dir.is_dir() and job_dir.name.startswith("job_"):
# 检查目录内容
final_dir = job_dir / "final"
logs_dir = job_dir / "logs"
# 获取已完成的阶段
completed_stages = []
if final_dir.exists():
for manifest in final_dir.glob("*.manifest.json"):
stage_name = manifest.name.replace(".manifest.json", "").split(".")[-1]
completed_stages.append(stage_name)
# 获取最后修改时间
mtime = datetime.fromtimestamp(job_dir.stat().st_mtime)
jobs.append({
"job_id": job_dir.name,
"created_at": mtime.strftime("%Y-%m-%d %H:%M:%S"),
"completed_stages": completed_stages,
"has_input": (INPUT_ROOT / job_dir.name).exists(),
"workspace_dir": str(job_dir)
})
return {
"success": True,
"jobs": jobs[:20] # 只返回最近20个
}
@app.post("/api/select_job")
async def select_existing_job(request: Request):
"""选择已有工作目录"""
global current_job_id
data = await request.json()
job_id = data.get("job_id")
if not job_id:
return JSONResponse({"success": False, "error": "No job_id provided"}, status_code=400)
# 验证工作目录是否存在
workspace_dir = WORKSPACE_ROOT / job_id
input_dir = INPUT_ROOT / job_id
if not workspace_dir.exists():
return JSONResponse({"success": False, "error": f"Job {job_id} not found"}, status_code=404)
# 设置为当前工作目录
current_job_id = job_id
return {
"success": True,
"job_id": job_id,
"input_dir": str(input_dir),
"workspace_dir": str(workspace_dir),
"message": f"已切换到工作目录: {job_id}"
}
@app.post("/api/upload")
async def upload_file(files: List[UploadFile] = File(...)):
"""文件上传接口"""
global current_job_id
if not current_job_id:
current_job_id = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:18]
job_id = current_job_id
job_dir = INPUT_ROOT / job_id
job_dir.mkdir(exist_ok=True)
saved_files = []
for f in files:
file_path = job_dir / f.filename
content = await f.read()
with open(file_path, 'wb') as fp:
fp.write(content)
saved_files.append(f.filename)
return {
"success": True,
"job_id": job_id,
"files": [f.filename for f in files]
}
@app.get("/api/interspecies/species-options")
async def get_interspecies_species_options(job_id: str = Query("")):
if not job_id:
return {"success": False, "message": "job_id is required", "targets": [], "refs": [], "default_targets": [], "default_refs": []}
options = _ordered_species_options(job_id)
return {"success": True, "job_id": job_id, **options}
@app.post("/api/execute")
async def execute_stage(req: ExecuteRequest):
"""执行分析阶段 - 支持异步模式"""
print(f"[API DEBUG] ============================================")
print(f"[API DEBUG] /api/execute called")
print(f"[API DEBUG] stage={req.stage}, job_id={req.job_id}")
print(f"[API DEBUG] prefix={req.prefix}, cpu={req.cpu}")
print(f"[API DEBUG] rnaseqMode={getattr(req, 'rnaseqMode', 'NOT_SET')}, speciesName={getattr(req, 'speciesName', 'NOT_SET')}")
print(f"[API DEBUG] kaksNeutralRate={getattr(req, 'kaksNeutralRate', 'NOT_SET')}")
print(f"[API DEBUG] ============================================")
global current_job_id
# 如果请求中指定了 job_id,使用它;否则使用 current_job_id
if req.job_id:
job_id = req.job_id
elif not current_job_id:
current_job_id = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:18]
job_id = current_job_id
else:
job_id = current_job_id
# ===== 检查是否已完成:以统一 manifest/artifacts 校验为准 =====
effective_prefix = resolve_effective_prefix(job_id, {"prefix": req.prefix}, req.stage)
if effective_prefix != _nonempty_prefix(req.prefix):
print(f"[API DEBUG] Resolved request prefix for {req.stage}: {effective_prefix or '<empty>'}")
try:
from gfa_core.mas.tools.stage_status import get_stage_status as get_unified_stage_status
stage_status = get_unified_stage_status(job_id=job_id, prefix=effective_prefix, stage=req.stage)
print(f"[API DEBUG] Stage status from unified validator: {stage_status.to_dict()}")
explicit_interspecies_selection = (
req.stage == "interspecies_synteny" and
(bool(getattr(req, "targetSpecies", []) or []) or bool(getattr(req, "refSpeciesList", []) or []))
)
if stage_status.completed and not explicit_interspecies_selection:
print(f"[API DEBUG] Stage '{req.stage}' already completed, skipping execution")
return {
"success": True,
"async": False,
"message": f"? {req.stage} ????????????",
"job_id": job_id,
"already_completed": True,
"stage_status": stage_status.to_dict()
}
elif stage_status.completed and explicit_interspecies_selection:
print("[API DEBUG] Interspecies selection provided; rerunning instead of skipping completed manifest")
except Exception as e:
print(f"[API DEBUG] Error checking completed stages: {e}")
# 继续执行,不阻断
# ===== 执行能力检查:避免规划/接口提交尚未接入执行器的阶段 =====
try:
from gfa_core.mas.stage_catalog import is_stage_executable
if not is_stage_executable(req.stage):
message = (
f"阶段 {req.stage} 暂未接入标准执行器,"
"无法直接执行。请先补充 pipeline_backend.run_stage 分支和标准 manifest。"
)
print(f"[API DEBUG] Unsupported stage rejected: {req.stage}")
return {
"success": False,
"async": False,
"status": "unsupported",
"message": message,
"job_id": job_id,
"stage": req.stage,
}
except Exception as e:
print(f"[API DEBUG] Error checking stage executability: {e}")
# ===== 统一架构:所有stage都使用Celery + LangGraph =====
task_id = str(uuid.uuid4())
# 创建任务记录
tasks[task_id] = {
"id": task_id,
"stage": req.stage,
"job_id": job_id,
"prefix": effective_prefix,
"status": "pending",
"message": f"任务 {req.stage} 已提交",
"created_at": datetime.now().isoformat(),
"result": None
}
# 构建参数
params = {
"prefix": effective_prefix,
"cpu": req.cpu,
"requiredPfams": req.requiredPfams,
"refSpecies": req.refSpecies,
"idEvalue": req.idEvalue,
"idCoverage": req.idCoverage,
"blastEvalue": req.blastEvalue,
"mafftParams": req.mafftParams,
"iqtreeParams": req.iqtreeParams,
"nmotifs": req.nmotifs,
"minw": req.minw,
"maxw": req.maxw,
"domEvalue": req.domEvalue,
"domCoverage": req.domCoverage,
"idTransform": req.idTransform,
"geoId": getattr(req, 'geoId', ''),
# RNA-seq参数
"rnaseqMode": getattr(req, 'rnaseqMode', 'upload'),
"speciesName": getattr(req, 'speciesName', ''),
# RNA-seq Enrich参数
"topPathways": getattr(req, 'topPathways', 20),
"enrichBackgroundSource": getattr(req, 'enrichBackgroundSource', 'auto'),
"enrichSpecies": getattr(req, 'enrichSpecies', ''),
"enrichKeyType": getattr(req, 'enrichKeyType', 'auto'),
"enrichType": getattr(req, 'enrichType', 'go'),
"keggOrganism": getattr(req, 'keggOrganism', ''),
"rnaseqForegroundMode": getattr(req, 'rnaseqForegroundMode', 'auto'),
# PPI参数
"ppiSpecies": getattr(req, 'ppiSpecies', 'ath'),
"ppiScore": getattr(req, 'ppiScore', 700),
# KaKs参数
"kaksNeutralRate": getattr(req, 'kaksNeutralRate', 7.08e-9),
# Genome Prepare参数
"genomePrepareType": getattr(req, 'genomePrepareType', 'auto'),
# Interspecies species selection
"targetSpecies": getattr(req, 'targetSpecies', []) or [],
"refSpeciesList": getattr(req, 'refSpeciesList', []) or [],
"blastEngine": getattr(req, 'blastEngine', 'diamond') or 'diamond',
}
# 使用新的Celery任务(统一架构)
if CELERY_AVAILABLE:
try:
from tasks import execute_single_stage_task
celery_task = execute_single_stage_task.delay(
task_id=task_id,
stage=req.stage,
job_id=job_id,
prefix=effective_prefix,
params=params,
session_id=None
)
print(f"[API DEBUG] Celery single stage task submitted: {celery_task.id}")
tasks[task_id]["celery_task_id"] = celery_task.id
except Exception as e:
print(f"[API DEBUG] Failed to submit Celery task: {e}")
# 降级到线程池执行
executor.submit(run_stage_sync, task_id, req.stage, job_id, params)
else:
# Celery不可用,使用线程池
executor.submit(run_stage_sync, task_id, req.stage, job_id, params)
return {
"success": True,
"async": True,
"task_id": task_id,
"message": f"{req.stage} 任务已提交(统一架构),请在后台等待完成",
"job_id": job_id,
"unified_arch": True # 标记使用统一架构
}
@app.get("/api/task/{task_id}")
async def get_task_status(task_id: str):
"""查询异步任务状态(支持Celery任务)"""
if task_id not in tasks:
raise HTTPException(status_code=404, detail="Task not found")
task = tasks[task_id]
# 如果是Celery任务,查询Celery状态
celery_task_id = task.get("celery_task_id")
if celery_task_id and CELERY_AVAILABLE:
try:
from celery_app import app as celery_app
celery_result = celery_app.AsyncResult(celery_task_id)
# 更新本地任务状态
if celery_result.state == 'SUCCESS':
# 检查Celery任务result内部的状态
result_data = celery_result.result
if isinstance(result_data, dict):
# 优先使用result_data中的status字段
result_status = result_data.get('status')
if result_data.get('success') == False or result_status == 'failed':
task["status"] = "failed"
task["message"] = result_data.get('message', '任务执行失败')
elif result_status == 'completed':
task["status"] = "success"
task["message"] = result_data.get('message', '任务完成')
else:
# 其他状态(如waiting_confirmation)
task["status"] = result_status or "success"
task["message"] = result_data.get('message', '任务完成')
task["result"] = result_data
else:
task["status"] = "success"
task["result"] = result_data
task["message"] = "任务完成"
elif celery_result.state == 'FAILURE':
task["status"] = "failed"
task["message"] = str(celery_result.result) if celery_result.result else "任务失败"
elif celery_result.state == 'RUNNING' or celery_result.state == 'PENDING':
task["status"] = "running"
# 获取meta信息
meta = celery_result.info
if meta and isinstance(meta, dict):
task["progress"] = meta.get("progress", 0)
task["current_stage"] = meta.get("current_stage", task.get("stage"))
# 检查是否有等待确认的状态
if meta.get("status") == "waiting_confirmation":
task["status"] = "waiting_confirmation"
task["pending_confirmation"] = meta.get("pending_confirmation", {})
except Exception as e:
print(f"[API DEBUG] Error querying Celery task: {e}")
_attach_single_stage_status(task)
_attach_annotation_progress(task)
# 读取日志文件内容(如果存在)
log_content = ""
try:
job_id = task.get("job_id")
stage = task.get("stage")
if job_id and stage:
import glob
log_pattern = str(WORKSPACE_ROOT / job_id / "logs" / f"*_{stage}.log")
log_files = glob.glob(log_pattern)
if log_files:
# 读取最新的日志文件
latest_log = max(log_files, key=lambda x: Path(x).stat().st_mtime)
with open(latest_log, 'r', encoding='utf-8', errors='ignore') as f:
log_content = f.read()
except Exception as e:
print(f"[API DEBUG] Error reading log file: {e}")
return {
"id": task["id"],
"stage": task["stage"],
"status": task["status"],
"message": task["message"],
"created_at": task["created_at"],
"start_time": task.get("start_time"),
"end_time": task.get("end_time"),
"result": task.get("result"),
"stage_status": task.get("stage_status"),
"log": log_content, # 添加日志内容
"progress": task.get("progress", 0),
"current_stage": task.get("current_stage", task.get("stage")),
"pending_confirmation": task.get("pending_confirmation"),
"annotation_progress": task.get("annotation_progress"),
"unified_arch": task.get("unified_arch", False)
}
@app.get("/api/stage-status")
async def get_stage_status(job_id: str, stage: str, prefix: str = ""):
"""检查阶段是否已完成(统一 manifest/artifacts 校验)"""
from gfa_core.mas.tools.stage_status import get_stage_status as get_unified_stage_status
prefix = resolve_effective_prefix(job_id, {"prefix": prefix}, stage)
mapped_stage = stage
if stage in {"rnaseq_go_enrich", "rnaseq_kegg_enrich"}:
mapped_stage = "rnaseq_enrich"
report = get_unified_stage_status(job_id=job_id, prefix=prefix, stage=mapped_stage)
payload = report.to_dict()
payload["requested_stage"] = stage
payload["mapped_stage"] = mapped_stage
payload["status"] = "completed" if report.completed else report.status
if report.manifest is not None:
payload["manifest"] = report.manifest
if stage == "annotation":
final_dir = WORKSPACE_ROOT / job_id / "final"
has_background = False
if final_dir.exists():
has_background = any(final_dir.glob("*gene2go*.txt")) or any(final_dir.glob("*gene2ko*.txt"))
has_annotation_manifest = bool((final_dir / f"{prefix}.annotation.manifest.json").exists())
payload["completed"] = bool(has_annotation_manifest and has_background)
payload["status"] = "completed" if payload["completed"] else "not_found"
if stage in {"rnaseq_go_enrich", "rnaseq_kegg_enrich"}:
final_dir = WORKSPACE_ROOT / job_id / "final"
if final_dir.exists():
if stage == "rnaseq_go_enrich":
has_specific = any(final_dir.glob("*.GO_enrich.txt"))
else:
has_specific = any(final_dir.glob("*.KEGG_enrich.txt"))
# GO/KEGG 子阶段用各自结果文件硬判定,避免共享 rnaseq_enrich manifest 导致串扰
payload["completed"] = bool(has_specific)
payload["status"] = "completed" if payload["completed"] else "not_found"
return payload
@app.get("/api/tasks")
async def list_tasks():
"""列出所有任务"""
return {
"tasks": [
{
"id": t["id"],
"stage": t["stage"],
"status": t["status"],
"message": t["message"],
"created_at": t["created_at"]
}
for t in tasks.values()
]
}
@app.get("/api/results")
async def get_results(job_id: str = Query(default=None, description="可选的job_id,默认使用当前job")):
"""获取任务结果,可指定job_id"""
global current_job_id
# 使用传入的job_id或当前job_id
target_job_id = job_id or current_job_id
if not target_job_id:
return {"identify": {"images": [], "files": []}}
workspace_dir = WORKSPACE_ROOT / target_job_id
final_dir = workspace_dir / "final"
stages = {
"identify": {"images": [], "files": [], "elapsed_time": 0},
"phylogeny": {"images": [], "files": [], "elapsed_time": 0},
"pep": {"images": [], "files": [], "elapsed_time": 0},
"motif": {"images": [], "files": [], "elapsed_time": 0},
"domain": {"images": [], "files": [], "elapsed_time": 0},
"structure": {"images": [], "files": [], "elapsed_time": 0},
"promoter": {"images": [], "files": [], "elapsed_time": 0},
"synteny": {"images": [], "files": [], "elapsed_time": 0},
"karyotype": {"images": [], "files": [], "elapsed_time": 0},
"interspecies_synteny": {"images": [], "files": [], "elapsed_time": 0},
"combo": {"images": [], "files": [], "elapsed_time": 0},
"expression": {"images": [], "files": [], "elapsed_time": 0},
"rnaseq_qc": {"images": [], "files": [], "elapsed_time": 0},
"rnaseq_align": {"images": [], "files": [], "elapsed_time": 0},
"rnaseq_quant": {"images": [], "files": [], "elapsed_time": 0},
"rnaseq_de": {"images": [], "files": [], "elapsed_time": 0},
"annotation": {"images": [], "files": [], "elapsed_time": 0},
"rnaseq_go_enrich": {"images": [], "files": [], "elapsed_time": 0},
"rnaseq_kegg_enrich": {"images": [], "files": [], "elapsed_time": 0},
"rnaseq_enrich": {"images": [], "files": [], "elapsed_time": 0},
"ppi": {"images": [], "files": [], "elapsed_time": 0},
"kaks": {"images": [], "files": [], "elapsed_time": 0},
}
# 从final目录读取执行时间(manifest文件在final/下,不在reports/下)
if final_dir.exists():
for manifest_file in final_dir.glob("*.manifest.json"):
try:
import json
with open(manifest_file, 'r') as f:
manifest = json.load(f)
stage_name = manifest.get("stage")
elapsed = manifest.get("elapsed_sec", 0)
if stage_name in stages and elapsed:
stages[stage_name]["elapsed_time"] = round(elapsed, 1)
except Exception:
pass
# 处理final目录中的文件
if final_dir.exists():
for f in final_dir.iterdir():
if not f.is_file():
continue
fname = f.name
# 排除大文件(>100MB的原始数据文件)
file_size = f.stat().st_size
if file_size > 100 * 1024 * 1024: # 100MB
# 跳过原始数据文件(FASTQ, BAM等)
if any(x in fname.lower() for x in ['.fastq', '.fq', '.bam', '.sam', '.clean.']):
continue
if f.suffix.lower() not in ['.png', '.jpg', '.jpeg', '.svg', '.pdf', '.fa', '.fasta', '.json', '.md', '.id', '.nwk', '.msa', '.html', '.log', '.gz', '.zip', '.txt', '.tsv', '.csv']:
continue
file_info = {
"name": fname,
"size": file_size,
"path": f"/workspace/{current_job_id}/final/{fname}"
}
fname_lower = fname.lower()
# 注意:顺序很重要!更具体的匹配应该放在前面
stage_keywords = {
"interspecies_synteny": ["interspecies_synteny", "interspecies"], # 优先匹配种间共线性
"combo": ["combo"],
"pep": ["pep", "pi_mw", "tmhmm", "subcellular", "等电点", "分子量", "跨膜"],
"motif": ["motif"],
"domain": ["domain"],
"structure": ["structure"],
"promoter": ["promoter", "plantcare"],
"synteny": [".synteny.", "collinearity", "mcscanx", "circos"], # 种内共线性,使用".synteny."避免匹配interspecies_synteny
"karyotype": ["karyotype"],
"phylogeny": ["phylogeny", "tree", "msa"],
"identify": ["identify", "final_members"],
"expression": [".expression.", "gene-exp", "gene-cor"], # 表达量分析
"rnaseq_qc": ["rnaseq_qc", "_fastqc", "_cutadapt"], # 移除 .clean.fastq 避免展示原始数据
"rnaseq_align": ["rnaseq_align", ".sorted.bam", ".bam.bai", "_hisat2.log"],
"rnaseq_quant": ["rnaseq_quant", ".count_matrix", "_quant"],
"rnaseq_de": ["rnaseq_de", ".DE_results", ".DE_genes", "_MA_plot", "_volcano"],
# annotation 只归类背景注释相关文件,避免与 GO/KEGG 富集输出混淆
"annotation": [".annotation.manifest", "gene2go", "gene2ko", "eggnog"],
# GO/KEGG 富集严格按各自输出特征匹配
"rnaseq_go_enrich": [".go_enrich"],
"rnaseq_kegg_enrich": [".kegg_enrich"],
"rnaseq_enrich": ["rnaseq_enrich", ".go_enrich", ".kegg_enrich", "_go_", "_kegg_", "gene2go"],
"ppi": ["ppi", "string", "network", "interaction"], # PPI蛋白互作分析
"kaks": ["kaks", "ka/ks", "ka_ks", "divergence"], # Ka/Ks分析
}
target = None
for stage_name, keywords in stage_keywords.items():
if any(kw in fname_lower for kw in keywords):
target = stages[stage_name]
break
if f.suffix.lower() in ['.png', '.jpg', '.jpeg', '.svg', '.pdf']:
if target:
target["images"].append(f"/workspace/{current_job_id}/final/{fname}")
else:
for stage_name in ["phylogeny", "pep", "motif", "domain", "structure", "identify", "expression", "promoter", "synteny", "karyotype", "interspecies_synteny", "combo", "rnaseq_qc", "rnaseq_align", "rnaseq_quant", "rnaseq_de", "annotation", "rnaseq_go_enrich", "rnaseq_kegg_enrich", "rnaseq_enrich"]:
manifest_files = list(final_dir.glob(f"*.{stage_name}.manifest.json"))
if manifest_files:
stages[stage_name]["images"].append(f"/workspace/{current_job_id}/final/{fname}")
break
elif target:
target["files"].append(file_info)
# 处理Synteny阶段的特殊文件(在synteny/analysis/目录中)
synteny_analysis_dir = workspace_dir / "synteny" / "analysis"
if synteny_analysis_dir.exists():
for f in synteny_analysis_dir.iterdir():
if not f.is_file():
continue
fname = f.name
# 只处理图像文件
if f.suffix.lower() not in ['.png', '.jpg', '.jpeg', '.svg']:
continue
# 检查是否是Circos图像文件
if "circos" in fname.lower():
stages["synteny"]["images"].append(f"/workspace/{current_job_id}/synteny/analysis/{fname}")
return stages
@app.get("/api/info")
async def get_info():
"""获取当前任务信息"""
global current_job_id
return {
"job_id": current_job_id,
"working_dir": str(WORKSPACE_ROOT / current_job_id) if current_job_id else "",
"input_dir": str(INPUT_ROOT / current_job_id) if current_job_id else "",
"output_dir": str(WORKSPACE_ROOT / current_job_id / "final") if current_job_id else "",
"workspace_root": str(WORKSPACE_ROOT),
"input_root": str(INPUT_ROOT)
}
@app.get("/workspace/{job_id}/{path:path}")
async def serve_workspace_file(job_id: str, path: str):
"""提供工作目录文件访问"""
file_path = WORKSPACE_ROOT / job_id / path
if file_path.exists() and file_path.is_file():
return FileResponse(file_path)
raise HTTPException(status_code=404, detail="File not found")
@app.get("/api/mcscanx_html/{job_id}")
async def get_mcscanx_html_files(job_id: str):
"""获取MCScanX HTML结果文件夹中的文件列表"""
html_dir = WORKSPACE_ROOT / job_id / "synteny" / "analysis" / "Ft.html"
if not html_dir.exists():
return {"exists": False, "files": [], "message": "HTML文件夹不存在"}
files = []
for f in sorted(html_dir.iterdir()):
if f.is_file() and f.suffix.lower() == '.html':
files.append({
"name": f.name,
"size": f.stat().st_size,
"path": f"/workspace/{job_id}/synteny/analysis/Ft.html/{f.name}"
})
return {
"exists": True,
"folder_path": str(html_dir),
"files": files,
"count": len(files)
}
class ChatRequest(BaseModel):
message: str
def parse_natural_language(message: str) -> dict:
"""解析自然语言,提取分析意图和参数
支持识别多个阶段,例如:"帮我执行基因家族鉴定和绘制进化树"
会识别出 ['identify', 'phylogeny'] 两个阶段
"""
message_lower = message.lower()
result = {"success": False, "intent": None, "message": ""}
stage_keywords = {
"identify": ["鉴定", "识别", "找到", "发现", "identify", "find", "discover"],
"phylogeny": ["进化树", "系统发育", "phylogeny", "phylogenetic", "tree", "建树"],
"pep": ["pep", "等电点", "分子量", "跨膜", "亚细胞", "理化性质", "pi_mw", "tmhmm", "subcellular"],
"motif": ["motif", "保守", "元件", "motif"],
"domain": ["domain", "结构域", "domain"],
"structure": ["structure", "基因结构", "结构", "外显子", "intron"],
"promoter": ["promoter", "顺式", "启动子", "元件预测", "plantcare"],
"synteny": ["synteny", "共线", "circos", "mcscanx", "microsynteny"],
"karyotype": ["karyotype", "染色体", "核型", "karyotype"],
"expression": ["expression", "表达", "geo", "表达量"],
"ppi": ["ppi", "蛋白互作", "string", "interaction"],
"kaks": ["kaks", "ka/ks", "选择压力", "kaks"],
"rnaseq_qc": ["rnaseq质控", "rna质控", "fastqc", "质控"],
"rnaseq_align": ["rnaseq比对", "rna比对", "hisat2", "比对"],
"rnaseq_quant": ["rnaseq定量", "rna定量", "stringtie", "定量"],
"rnaseq_de": ["rnaseq差异", "rna差异", "差异表达", "de分析"],
"annotation": ["背景注释", "功能注释", "基因组注释", "自动注释", "eggnog", "annotation"],
"rnaseq_go_enrich": ["rnaseq go富集", "rnaseq_go富集", "rna go富集", "go富集", "go分析"],
"rnaseq_kegg_enrich": ["rnaseq kegg", "rnaseq_kegg", "rna kegg", "kegg富集", "kegg分析"],
"rnaseq_enrich": ["rnaseq富集", "rna富集", "富集分析", "enrichment"],
"all": ["完整", "全部", "all", "full", "整个", "全套", "所有"]
}
import re
prefix_match = re.search(r'[A-Za-z]+\.?[A-Za-z]*[A-Z][A-Za-z]*', message)
prefix = prefix_match.group(0) if prefix_match else ""
# 收集所有匹配的阶段(不再用break,支持多阶段)
detected_stages = []
for stage, keywords in stage_keywords.items():
if any(kw in message_lower for kw in keywords):
detected_stages.append(stage)
# 去重并保持顺序
seen = set()
unique_stages = []
for stage in detected_stages:
if stage not in seen:
seen.add(stage)
unique_stages.append(stage)
if "rnaseq_go_enrich" in unique_stages or "rnaseq_kegg_enrich" in unique_stages:
unique_stages = [s for s in unique_stages if s != "rnaseq_enrich"]
# 处理 "all" 关键词:如果包含"全部"等,返回所有主要阶段
if "all" in unique_stages:
unique_stages = ["identify", "phylogeny", "motif", "domain", "structure",
"promoter", "synteny", "karyotype", "expression", "ppi", "kaks"]
if not unique_stages:
result["message"] = "抱歉,我没有理解你想做什么分析。请说明要执行的分析阶段(鉴定、进化树、Motif、结构域、基因结构等)。"
return result
# 单阶段:保持兼容
if len(unique_stages) == 1:
intent = {
"stage": unique_stages[0],
"params": {"prefix": prefix, "cpu": 8},
"message": f"我理解你想执行 {unique_stages[0]} 分析,基因家族为 {prefix}。"
}
else:
# 多阶段:返回阶段列表
intent = {
"stages": unique_stages,
"stage": unique_stages[0], # 保持兼容,主阶段为第一个
"params": {"prefix": prefix, "cpu": 8},
"message": f"我理解你想执行 {', '.join(unique_stages)} 分析,基因家族为 {prefix}。"
}
result["success"] = True
result["intent"] = intent
return result
@app.post("/api/chat")
async def chat(request: ChatRequest):
"""自然语言处理接口 (兼容旧版)"""
try:
result = parse_natural_language(request.message)
return result
except Exception as e:
return {"success": False, "message": f"处理失败: {str(e)}"}
class AgentChatRequest(BaseModel):
message: str
model: str = "kimi" # kimi, glm, minimax
session_id: str = ""
job_id: str = "" # 当前 job_id,用于查询已完成 stages
history: list = []
@app.post("/api/agent/chat")
async def agent_chat(request: AgentChatRequest):
"""Agent智能对话接口 - 只执行意图识别,不执行分析阶段
返回意图识别结果,前端根据 mode 判断:
- FAQ: 直接显示回答(前端调用 RAG)
- Analysis: 调用 /api/agent/plan 获取计划,确认后再执行
"""
try:
session_id = request.session_id or f"session_{int(time.time())}"
# 查询已完成的阶段(从manifest文件)
completed_stages = []
if request.job_id:
workspace_dir = PROJECT_ROOT / "workspace" / request.job_id / "final"
if workspace_dir.exists():
for manifest_file in workspace_dir.glob("*.manifest.json"):
try:
with open(manifest_file, 'r') as f:
manifest_data = json.load(f)
status = manifest_data.get("status", "")
if status in ["success", "partial_success"]:
stage = manifest_data.get("stage", "")
if stage:
completed_stages.append(stage)
except:
pass
# 只执行 Router + IntentAgent,不执行完整流程
# 1. RouterAgent 判断模式
from gfa_core.mas.agents.router import RouterAgent
from gfa_core.mas.agents.base import AgentState, AgentMessage, QueryMode
# 使用全局 llm_manager(已在文件顶部初始化)
router_agent = RouterAgent(llm_manager)
# 创建 AgentState 和 AgentMessage
router_state = AgentState(session_id=session_id)
router_message = AgentMessage(
sender="user",
receiver="router",
message_type="classify",
payload={"query": request.message}
)
# 调用 execute 方法
router_state = await router_agent.execute(router_state, router_message)
mode = router_state.mode.value
print(f"[Agent Chat] Router result: mode={mode}, query={request.message}")
# 2. 如果是 Analysis,调用 IntentAgentV2 获取 intent
intent = {}
if mode == "analysis" and request.job_id:
from gfa_core.mas.agents.intent_v2 import IntentAgentV2
intent_agent = IntentAgentV2(llm_manager)
# 构建 workspace 路径
workspace = str(PROJECT_ROOT / "workspace" / request.job_id)
# 创建 AgentState 和 AgentMessage
intent_state = AgentState(
session_id=session_id,
job_id=request.job_id
)
intent_message = AgentMessage(
sender="router",
receiver="intent",
message_type="recognize",
payload={
"query": request.message,
"workspace": workspace,
"completed_stages": completed_stages
}
)
# 调用 execute 方法
intent_state = await intent_agent.execute(intent_state, intent_message)
# 从 state.results 获取 intent(注意:IntentAgentV2 存储在 results.results.intent)
intent = intent_state.results.get("results", {}).get("intent", {})
intent["job_id"] = request.job_id
intent["workspace"] = workspace
if intent.get("needs_clarification"):
intent["taskType"] = "clarification"
# 添加 stage 字段(取第一个阶段)供前端判断
if intent.get("stages") and len(intent["stages"]) > 0:
intent["stage"] = intent["stages"][0]
# 添加 taskType 字段供前端判断
if not intent.get("taskType"):
intent["taskType"] = "analysis" if intent.get("stages") else "faq"
# 返回意图识别结果
return {
"success": True,
"intent": intent,
"mode": mode,
"completed_stages": completed_stages,
"model_used": request.model,
"session_id": session_id
}
except Exception as e:
print(f"[Agent Chat] Error: {e}")
return {
"success": False,
"message": f"意图识别失败: {str(e)}",
"model_used": request.model,
"mode": "error"
}
# ==================== RNA-seq 接口 ====================
class RnaseqRequest(BaseModel):
"""RNA-seq分析请求"""
job_id: str
samples: list # 样本配置列表
design: dict = {} # 实验设计
@app.post("/api/rnaseq/run")
async def run_rnaseq(req: RnaseqRequest, background_tasks: BackgroundTasks):
"""运行RNA-seq分析"""
task_id = str(uuid.uuid4())
project_root = Path("/home/xia-lab/wutx/00test/bbav5_0502_Yang_Tihuan.7880")
# 脚本路径
script_map = {
"align": "apps/cli/run_rnaseq_align.py",
"quant": "apps/cli/run_rnaseq_quant.py",
"de": "apps/cli/run_rnaseq_de.py",
"enrich": "apps/cli/run_rnaseq_enrich.py"
}
if req.stage not in script_map:
return {"success": False, "message": f"未知阶段: {req.stage}"}
script_path = project_root / script_map[req.stage]
sample_dir = project_root / "rnaseq" / req.sample
rnaseq_tasks[task_id] = {
"id": task_id,
"stage": req.stage,
"sample": req.sample,
"status": "running",
"message": f"正在运行 {req.stage}...",
"start_time": datetime.now().isoformat()
}
def run_rnaseq_task():
try:
if req.stage == "align":
cmd = [
"python3", str(script_path),
"-i", str(sample_dir),
"-x", str(project_root / "rnaseq" / "Ft.genome"),
"-o", str(sample_dir),
"-j", req.sample,
"-t", "8"
]
elif req.stage == "quant":
cmd = [
"python3", str(script_path),
"-i", str(sample_dir),
"-o", str(sample_dir),
"-j", req.sample,
"-t", "8"
]
else:
cmd = ["python3", str(script_path), "-i", str(project_root / "rnaseq")]
result = subprocess.run(cmd, capture_output=True, text=True)
rnaseq_tasks[task_id]["status"] = "success"
rnaseq_tasks[task_id]["message"] = "完成"
rnaseq_tasks[task_id]["end_time"] = datetime.now().isoformat()
except Exception as e:
rnaseq_tasks[task_id]["status"] = "failed"
rnaseq_tasks[task_id]["message"] = str(e)
rnaseq_tasks[task_id]["end_time"] = datetime.now().isoformat()
background_tasks.add_task(run_rnaseq_task)
return {"success": True, "task_id": task_id, "message": f"任务已提交: {req.stage}"}
@app.get("/api/rnaseq/status/{task_id}")
async def get_rnaseq_status(task_id: str):
"""获取RNA-seq任务状态"""
if task_id not in rnaseq_tasks:
return {"success": False, "message": "任务不存在"}
return {"success": True, "task": rnaseq_tasks[task_id]}
@app.get("/api/rnaseq/samples")
async def list_rnaseq_samples():
"""列出RNA-seq样本"""
project_root = Path("/home/xia-lab/wutx/00test/bbav5_0502_Yang_Tihuan.7880")
rnaseq_dir = project_root / "rnaseq"
samples = []
if rnaseq_dir.exists():
for d in rnaseq_dir.iterdir():
if d.is_dir() and d.name.startswith("SRR"):
samples.append({
"name": d.name,
"has_fastq": list(d.glob("*.fastq")) or list(d.glob("*.fq")),
"has_gtf": list(d.glob("*.gtf"))
})
return {"success": True, "samples": samples}
@app.get("/api/rnaseq/results")
async def get_rnaseq_results():
"""获取RNA-seq分析结果"""
project_root = Path("/home/xia-lab/wutx/00test/bbav5_0502_Yang_Tihuan.7880")
de_output = project_root / "rnaseq" / "de_output"
results = {}
if de_output.exists():
results = {
"de_results": str(de_output / "DE_results.txt"),
"volcano_plot": str(de_output / "volcano_plot.png"),
"heatmap": str(de_output / "heatmap.png"),
"sig_genes": str(de_output / "sig_genes.DE_genes.txt")
}
return {"success": True, "results": results}
# ==================== LearningAgent API ====================
class LearningRequest(BaseModel):
"""学习请求"""
action: str = "recommend" # record, recommend, analyze
stage: str = ""
prefix: str = ""
params: dict = {}
status: str = ""
elapsed_sec: float = 0.0
final_members: int = 0
@app.post("/api/agent/learn")
async def learning(request: LearningRequest):
"""
学习优化接口
记录执行历史、推荐参数、分析趋势
"""
try:
from gfa_core.mas.agents.learning_v2 import LearningAgentV2
from gfa_core.mas.config import config as mas_config
from gfa_core.mas.llm import LLMManager
from gfa_core.mas.agents.base import AgentMessage, AgentState
# 初始化组件
try:
llm_config = mas_config.load_llm_config()
llm_manager = LLMManager(config=llm_config)
except:
llm_manager = None
# 创建LearningAgent
learning_agent = LearningAgentV2(llm_manager)
# 构建消息
message = AgentMessage(
sender="user",
receiver="learning",
message_type=request.action,
payload={
"action": request.action,
"stage": request.stage,
"prefix": request.prefix,
"params": request.params,
"status": request.status,
"elapsed_sec": request.elapsed_sec,
"final_members": request.final_members
}
)
# 构建状态
state = AgentState(
session_id=f"learn_{int(time.time())}",
results={},
messages=[]
)
# 执行学习
result_state = await learning_agent.execute(state, message)
if request.action == "recommend":
recommendations = result_state.results.get("recommendations", {})
return {
"success": True,
"action": request.action,
"recommendations": recommendations
}
elif request.action == "analyze":
analysis = result_state.results.get("analysis", {})
return {
"success": True,
"action": request.action,
"analysis": analysis
}
else: # record
record_result = result_state.results.get("record_result", {})
return {
"success": True,
"action": request.action,
"result": record_result
}
except Exception as e:
import traceback
return {
"success": False,
"message": f"学习优化失败: {str(e)}",
"error_detail": traceback.format_exc()
}
# ==================== ValidatorAgent API ====================
class ValidateRequest(BaseModel):
"""验证请求"""
stage: str
prefix: str
workspace: str
manifest_path: str
@app.post("/api/agent/validate")
async def validate_result(request: ValidateRequest):
"""
结果验证接口
验证分析结果质量
"""
try:
from gfa_core.mas.agents.validator_v2 import ValidatorAgentV2
from gfa_core.mas.config import config as mas_config
from gfa_core.mas.llm import LLMManager
from gfa_core.mas.agents.base import AgentMessage, AgentState
# 初始化组件
try:
llm_config = mas_config.load_llm_config()
llm_manager = LLMManager(config=llm_config)
except:
llm_manager = None
# 创建ValidatorAgent
validator_agent = ValidatorAgentV2(llm_manager)
# 读取manifest
import json
manifest_path = Path(request.manifest_path)
if not manifest_path.exists():
return {
"success": False,
"message": f"Manifest文件不存在: {request.manifest_path}"
}
with open(manifest_path) as f:
manifest = json.load(f)
# 构建execution_result
execution_result = {
"status": manifest.get("status", "unknown"),
"manifest": manifest
}
# 构建消息
message = AgentMessage(
sender="user",
receiver="validator",
message_type="validate",
payload={
"stage": request.stage,
"prefix": request.prefix,
"workspace": request.workspace
}
)
# 构建状态
state = AgentState(
session_id=f"validate_{int(time.time())}",
results={"execution_result": execution_result},
messages=[]
)
# 执行验证
result_state = await validator_agent.execute(state, message)
report = result_state.results.get("validation_report", {})
return {
"success": True,
"report": report,
"overall_passed": report.get("overall_passed", False),
"score": report.get("score", 0),
"summary": report.get("summary", "")
}
except Exception as e:
import traceback
return {
"success": False,
"message": f"验证失败: {str(e)}",
"error_detail": traceback.format_exc()
}
# ==================== ResultParserAgent API ====================
class ParseResultRequest(BaseModel):
"""结果解析请求"""
stage: str
manifest_path: str
job_id: str = ""
@app.post("/api/agent/parse_result")
async def parse_result(request: ParseResultRequest):
"""
结果解析接口
解析分析结果并生成报告
"""
try:
from gfa_core.mas.agents.result_parser_v2 import ResultParserAgentV2
from gfa_core.mas.config import config as mas_config
from gfa_core.mas.llm import LLMManager
from gfa_core.mas.agents.base import AgentMessage, AgentState
# 初始化组件
try:
llm_config = mas_config.load_llm_config()
llm_manager = LLMManager(config=llm_config)
except:
llm_manager = None
# 创建ResultParserAgent
parser_agent = ResultParserAgentV2(llm_manager)
# 读取manifest
import json
manifest_path = Path(request.manifest_path)
if not manifest_path.exists():
return {
"success": False,
"message": f"Manifest文件不存在: {request.manifest_path}"
}
with open(manifest_path) as f:
manifest = json.load(f)
# 构建execution_result
execution_result = {
"status": manifest.get("status", "unknown"),
"manifest": manifest,
"stage": request.stage
}
# 构建消息
message = AgentMessage(
sender="user",
receiver="result_parser",
message_type="parse",
payload={"stage": request.stage}
)
# 构建状态
state = AgentState(
session_id=request.job_id or f"parse_{int(time.time())}",
results={"execution_result": execution_result},
messages=[]
)
# 执行解析
result_state = await parser_agent.execute(state, message)
report = result_state.results.get("parsed_report", {})
return {
"success": True,
"report": report,
"markdown": report.get("markdown", ""),
"summary": report.get("summary", {}),
"metrics": report.get("metrics", {})
}
except Exception as e:
import traceback
return {
"success": False,
"message": f"结果解析失败: {str(e)}",
"error_detail": traceback.format_exc()
}
# ==================== Agent执行状态反馈 API ====================
# Agent任务存储
agent_tasks: Dict[str, dict] = {}
class AgentExecuteRequest(BaseModel):
"""Agent执行请求"""
message: str
session_id: str = ""
job_id: str = "" # 当前选中的 job_id,用于 auto_continue
prefix: str = ""
model: str = "kimi"
history: list = []
auto_execute: bool = True # 是否自动执行分析
class AgentStatusResponse(BaseModel):
"""Agent状态响应"""
task_id: str
status: str # pending, running, completed, failed
current_stage: str = ""
progress: int = 0 # 0-100
message: str = ""
logs: list = []
result: dict = {}
@app.post("/api/agent/execute")
async def agent_execute(request: AgentExecuteRequest, background_tasks: BackgroundTasks):
"""
Agent执行接口 - 使用Celery异步执行分析任务
与/agent/chat不同,此接口会实际执行分析流程
"""
task_id = f"agent_task_{int(time.time())}_{uuid.uuid4().hex[:8]}"
# 初始化任务状态
agent_tasks[task_id] = {
"task_id": task_id,
"status": "pending",
"message": request.message,
"job_id": request.job_id,
"prefix": request.prefix,
"current_stage": "",
"progress": 0,
"logs": [],
"result": {},
"start_time": datetime.now().isoformat(),
"session_id": request.session_id or task_id,
"stages_completed": [],
"stages_pending": []
}
# 使用Celery异步执行
if CELERY_AVAILABLE and execute_agent_task is not None:
try:
# 提交任务到Celery队列
celery_task = execute_agent_task.delay(
task_id=task_id,
message=request.message,
session_id=request.session_id or task_id,
job_id=request.job_id, # 传入选中的 job_id
history=request.history if request.history else [],
prefix=request.prefix
)
print(f"[DEBUG] Celery task submitted: {celery_task.id} for agent task {task_id}")
# 存储Celery任务ID映射
agent_tasks[task_id]["celery_task_id"] = celery_task.id
agent_tasks[task_id]["logs"].append({
"time": datetime.now().isoformat(),
"level": "info",
"message": f"任务已提交到Celery队列 (celery_task_id: {celery_task.id})"
})
except Exception as e:
print(f"[ERROR] Failed to submit Celery task: {e}")
agent_tasks[task_id]["status"] = "failed"
agent_tasks[task_id]["logs"].append({
"time": datetime.now().isoformat(),
"level": "error",
"message": f"Celery任务提交失败: {str(e)}"
})
else:
# Celery不可用,使用降级方案(直接执行)
print(f"[WARN] Celery不可用,使用降级方案执行")
agent_tasks[task_id]["logs"].append({
"time": datetime.now().isoformat(),
"level": "warn",
"message": "Celery不可用,使用本地异步执行"
})
async def execute_agent_task_async():
"""异步执行Agent任务(降级方案)"""
try:
agent_tasks[task_id]["status"] = "running"
if orchestrator is not None and MAS_AVAILABLE:
from gfa_core.mas.core.state import GraphState
initial_state: GraphState = {
"query": request.message,
"session_id": request.session_id or task_id,
"job_id": request.job_id, # 传入选中的 job_id
"files": [],
"current_agent": "router",
"status": "running",
"errors": [],
"final_output": {},
"history": request.history if request.history else []
}
result = await orchestrator.run(initial_state)
if result.get("status") == "completed":
agent_tasks[task_id]["status"] = "completed"
agent_tasks[task_id]["progress"] = 100
agent_tasks[task_id]["result"] = result.get("final_output", {})
else:
agent_tasks[task_id]["status"] = "failed"
else:
agent_tasks[task_id]["status"] = "failed"
agent_tasks[task_id]["logs"].append({
"time": datetime.now().isoformat(),
"level": "error",
"message": "Orchestrator不可用"
})
except Exception as e:
agent_tasks[task_id]["status"] = "failed"
agent_tasks[task_id]["logs"].append({
"time": datetime.now().isoformat(),
"level": "error",
"message": f"执行异常: {str(e)}"
})
finally:
agent_tasks[task_id]["end_time"] = datetime.now().isoformat()
asyncio.create_task(execute_agent_task_async())
return {
"success": True,
"task_id": task_id,
"message": "Agent任务已提交",
"status": "pending"
}
@app.get("/api/agent/status/{task_id}")
async def get_agent_status(task_id: str):
"""获取Agent任务执行状态"""
if task_id not in agent_tasks:
return {
"success": False,
"message": "任务不存在",
"task_id": task_id
}
task = agent_tasks[task_id]
# 如果有Celery任务ID,查询Celery状态
if CELERY_AVAILABLE and "celery_task_id" in task:
try:
from celery_app import app as celery_app
celery_task = celery_app.AsyncResult(task["celery_task_id"])
if celery_task.state == 'PENDING':
task["status"] = "pending"
elif celery_task.state == 'RUNNING':
task["status"] = "running"
# 更新进度信息
if celery_task.info:
task["current_stage"] = celery_task.info.get('current_stage', task["current_stage"])
task["progress"] = celery_task.info.get('progress', task["progress"])
if 'logs' in celery_task.info:
task["logs"] = celery_task.info['logs']
elif celery_task.state == 'SUCCESS':
result = celery_task.result
result_status = result.get("status") if isinstance(result, dict) else None
result_success = result.get("success") if isinstance(result, dict) else None
if result_status == "failed" or result_success is False:
task["status"] = "failed"
task["progress"] = 0
else:
task["status"] = "success"
task["progress"] = 100
if isinstance(result, dict):
task["result"] = result.get('result', {})
if 'logs' in result:
task["logs"] = result['logs']
task["current_stage"] = result.get("current_stage", task["current_stage"])
task["message"] = result.get("message", task["message"])
task["end_time"] = datetime.now().isoformat()
elif celery_task.state == 'FAILURE':
task["status"] = "failed"
task["logs"].append({
"time": datetime.now().isoformat(),
"level": "error",
"message": f"Celery任务失败: {str(celery_task.result)}"
})
task["end_time"] = datetime.now().isoformat()
except Exception as e:
print(f"[ERROR] Failed to query Celery status: {e}")
_attach_agent_manifest_status(task)
return {
"success": True,
"task": {
"task_id": task["task_id"],
"status": task["status"],
"current_stage": task["current_stage"],
"progress": task["progress"],
"message": task["message"],
"logs": task["logs"][-50:], # 返回最近50条日志
"result": task["result"],
"completed_stages": task.get("completed_stages", task.get("stages_completed", [])),
"stages_completed": task.get("stages_completed", []),
"start_time": task.get("start_time"),
"end_time": task.get("end_time")
}
}
@app.get("/api/agent/tasks")
async def list_agent_tasks():
"""列出所有Agent任务"""
tasks = []
for task_id, task in agent_tasks.items():
tasks.append({
"task_id": task["task_id"],
"status": task["status"],
"current_stage": task["current_stage"],
"progress": task["progress"],
"message": task["message"][:50] + "..." if len(task["message"]) > 50 else task["message"],
"start_time": task.get("start_time")
})
# 按时间倒序
tasks.sort(key=lambda x: x.get("start_time", ""), reverse=True)
return {
"success": True,
"tasks": tasks[:20] # 返回最近20个任务
}
@app.post("/api/agent/cancel/{task_id}")
async def cancel_agent_task(task_id: str):
"""取消Agent任务(标记为取消,实际后台任务可能仍在运行)"""
if task_id not in agent_tasks:
return {"success": False, "message": "任务不存在"}
if agent_tasks[task_id]["status"] in ["completed", "failed"]:
return {"success": False, "message": "任务已结束,无法取消"}
agent_tasks[task_id]["status"] = "cancelled"
agent_tasks[task_id]["logs"].append({
"time": datetime.now().isoformat(),
"level": "warning",
"message": "任务已取消"
})
return {"success": True, "message": "任务已标记为取消"}
class AgentPlanRequest(BaseModel):
"""Agent规划请求"""
message: str
session_id: str = ""
job_id: str = "" # 添加job_id字段,用于查询已完成阶段
prefix: str = ""
history: list = []
intent: dict = {} # 前端可传入已有的intent,避免重复调用IntentAgentV2
@app.post("/api/agent/plan")
async def agent_plan(request: AgentPlanRequest):
"""
Agent任务规划接口 V2 - 使用LLM智能规划
返回详细的分析计划,包括:
- 识别的意图(基因家族、物种、阶段)
- 详细的阶段列表(含依赖、并行信息)
- 执行策略(并行/串行)
- 预估时间
"""
try:
# 使用Orchestrator V2进行规划
if orchestrator is not None and MAS_AVAILABLE:
from gfa_core.mas.core.state import GraphState
session_id = request.session_id or f"session_{int(time.time())}"
# 构建初始状态
initial_state: GraphState = {
"query": request.message,
"session_id": session_id,
"job_id": request.job_id, # 添加 job_id 用于 manifest 查询
"files": [],
"current_agent": "planning",
"status": "running",
"errors": [],
"final_output": {},
"history": request.history if request.history else [],
"results": {} # 用于存储已完成阶段
}
# 查询已完成的阶段:统一 manifest/artifacts 校验。
workspace_name = request.job_id if request.job_id else request.session_id
if workspace_name:
from gfa_core.mas.tools.stage_status import list_completed_stages
completed_stages = list_completed_stages(job_id=workspace_name, prefix=request.prefix)
initial_state["results"]["completed_stages"] = completed_stages
print(f"[Agent Plan] Job_id: {workspace_name}, Completed stages: {completed_stages}")
# 如果前端传入了 intent,直接放入 state(避免 orchestrator.plan 重复调用 IntentAgentV2)
if request.intent and request.intent.get("stages"):
initial_state["results"]["intent"] = request.intent
print(f"[Agent Plan] 使用前端传入的 intent: {request.intent.get('stages', [])}")
# 调用PlanningAgent V2进行规划
print(f"[Agent Plan] Starting LLM planning for: {request.message}")
result = await orchestrator.plan(initial_state)
if result.get("status") == "completed":
plan = result.get("final_output", {}).get("analysis_plan", {})
intent = result.get("final_output", {}).get("intent", {})
raw_stages = plan.get("stages", [])
stages_detail = plan.get("stages_detail") or (
raw_stages if raw_stages and isinstance(raw_stages[0], dict) else []
)
stage_ids = [
stage.get("id", stage.get("name", "")) if isinstance(stage, dict) else stage
for stage in raw_stages
]
estimated_time = (
plan.get("estimated_time")
or plan.get("estimated_total_time")
or ("0分钟" if not raw_stages else "未知")
)
plan_params = plan.get("params") or {
"prefix": resolve_effective_prefix(request.job_id, {"prefix": intent.get("prefix", request.prefix or "")}, "agent"),
"cpu": 8,
}
# 构建V2格式的响应
response = {
"success": True,
"mode": result.get("mode", "legacy"), # orchestrator_v2 或 legacy
"plan": {
"analysis_type": plan.get("analysis_type", "gene_family"),
"gene_family": intent.get("gene_family", "SPL"),
"species": intent.get("species", "Ft"),
"prefix": intent.get("prefix", ""),
"description": plan.get("description", ""),
"estimated_time": estimated_time,
"estimated_total_time": estimated_time,
"execution_strategy": plan.get("execution_strategy", "parallel"),
"max_parallel": plan.get("max_parallel", 4),
"total_stages": plan.get("total_stages", len(stage_ids)),
"stages": stage_ids,
"stages_detail": stages_detail,
"parallel_groups": plan.get("parallel_groups", []),
"params": plan_params
},
"intent": {
"gene_family": intent.get("gene_family", ""),
"species": intent.get("species", ""),
"stages": intent.get("stages", []),
"description": intent.get("description", ""),
"confidence": intent.get("confidence", 0.9)
},
"completed_stages": completed_stages,
"session_id": session_id
}
print(f"[Agent Plan] Plan generated: {response['plan']['description']}")
print(f"[Agent Plan] Stages: {[s.get('id') for s in response['plan']['stages_detail']]}")
return response
else:
error_msg = result.get("final_output", {}).get("error", "规划失败")
print(f"[Agent Plan] Error: {error_msg}")
return {
"success": False,
"message": error_msg
}
else:
# 降级模式:使用简单意图解析
print("[Agent Plan] Orchestrator not available, using fallback")
result = parse_natural_language(request.message)
if result["success"] and result["intent"]:
intent = result["intent"]
stages = intent.get("stages", [intent.get("stage", "identify")])
return {
"success": True,
"mode": "fallback",
"plan": {
"analysis_type": "gene_family",
"gene_family": intent.get("gene_family", "SPL"),
"species": intent.get("species", "Ft"),
"prefix": intent.get("prefix", ""),
"description": intent.get("message", ""),
"estimated_time": f"约{len(stages) * 5}分钟",
"execution_strategy": "sequential",
"max_parallel": 1,
"total_stages": len(stages),
"stages": stages,
"stages_detail": [
{
"id": s,
"name": s,
"order": i + 1,
"dependencies": [],
"can_parallel": False,
"estimated_time": 5,
"params": {"prefix": intent.get("prefix", ""), "cpu": 8}
}
for i, s in enumerate(stages)
],
"parallel_groups": [],
"params": intent.get("params", {})
},
"intent": {
"gene_family": intent.get("gene_family", ""),
"species": intent.get("species", ""),
"stages": stages,
"description": intent.get("message", ""),
"confidence": 0.7
},
"session_id": request.session_id or f"session_{int(time.time())}"
}
else:
return {
"success": False,
"message": result.get("message", "未能理解您的意图")
}
except Exception as e:
import traceback
return {
"success": False,
"message": f"规划失败: {str(e)}",
"error_detail": traceback.format_exc()
}
# ============ 新增:完整 Orchestrator 流程端点 ============
class AgentRunRequest(BaseModel):
"""Agent完整执行请求"""
message: str
session_id: str = ""
job_id: str = ""
intent: dict = {} # 前端可传入已有的intent
stages: list = [] # 要执行的阶段列表
@app.post("/api/agent/run")
async def agent_run(request: AgentRunRequest):
"""
Agent完整执行流程 - 使用 LangGraph Orchestrator
执行完整的多智能体流程:
Router → Intent → Planning → Execution → Validator → Discovery → Learning
用于自动化执行分析任务,支持循环迭代发现
"""
try:
if langgraph_orchestrator is None or not LANGGRAPH_AVAILABLE:
return {
"success": False,
"message": "LangGraph Orchestrator 未初始化"
}
from gfa_core.mas.core.state import GraphState
session_id = request.session_id or f"session_{int(time.time())}"
# 构建初始状态(使用LangGraphOrchestrator的create_initial_state)
initial_state = langgraph_orchestrator.create_initial_state(
session_id=session_id,
query=request.message,
job_id=request.job_id,
files=[],
prefix=request.intent.get("prefix", "") if request.intent else ""
)
# 添加额外的运行时字段
initial_state["status"] = "running"
initial_state["intent_result"] = request.intent or {} # 传入已有的 intent
initial_state["completed_stages"] = []
initial_state["iteration_count"] = 0
initial_state["max_iterations"] = 3
initial_state["discoveries"] = []
# 查询已完成的阶段
workspace_name = request.job_id if request.job_id else request.session_id
if workspace_name:
workspace_dir = WORKSPACE_ROOT / workspace_name / "final"
if workspace_dir.exists():
completed_stages = []
for manifest_file in workspace_dir.glob("*.manifest.json"):
try:
with open(manifest_file, 'r') as f:
manifest_data = json.load(f)
if manifest_data.get("status") in ["success", "partial_success"]:
stage = manifest_data.get("stage", "")
if stage:
completed_stages.append(stage)
except:
pass
initial_state["results"]["completed_stages"] = completed_stages
# 如果前端指定了要执行的阶段,放入 intent
if request.stages:
initial_state["results"]["intent"]["stages"] = request.stages
print(f"[Agent Run] Starting full orchestrator flow...")
print(f"[Agent Run] Query: {request.message}")
print(f"[Agent Run] Completed stages: {initial_state.get('completed_stages', [])}")
# 调用 LangGraph Orchestrator 执行完整流程
result_state = await langgraph_orchestrator.run(initial_state)
# 提取结果
final_output = result_state.get("final_output", {})
discoveries = result_state.get("discoveries", [])
iteration_count = result_state.get("iteration_count", 0)
print(f"[Agent Run] Flow completed. Iterations: {iteration_count}")
print(f"[Agent Run] Discoveries: {len(discoveries)}")
return {
"success": True,
"status": result_state.get("status", "completed"),
"iterations": iteration_count,
"discoveries": discoveries,
"final_output": final_output,
"session_id": session_id
}
except Exception as e:
import traceback
return {
"success": False,
"message": f"执行失败: {str(e)}",
"error_detail": traceback.format_exc()
}
# ============ 深度解读辅助函数 ============
def build_enhanced_biological_story(synthesis: dict, stage_interpretations: dict) -> str:
"""构建增强的生物学故事,包含 discovery 洞察"""
# 基础故事
base_story = synthesis.get("biological_story", "")
# 收集 discovery 洞察
discovery_sections = []
for stage_id, interp in stage_interpretations.items():
discovery = interp.get("discovery")
if discovery:
stage_name = interp.get("stage_name", stage_id)
storyline = discovery.get("storyline", "")
insight = discovery.get("insight", "")
if storyline or insight:
section_parts = [f"\n【{stage_name} - 科学洞察】"]
if storyline:
section_parts.append(f"故事线: {storyline}")
if insight:
section_parts.append(f"深度洞察: {insight}")
discovery_sections.append("\n".join(section_parts))
# 合并
if discovery_sections:
return base_story + "\n\n" + "\n\n".join(discovery_sections)
return base_story
def generate_stage_connection(from_stage: str, to_stage: str, from_metrics: dict, to_metrics: dict, prefix: str) -> dict:
"""基于真实metrics生成阶段间关联分析"""
# 获取关键数值
from_count = from_metrics.get("final_members") or from_metrics.get("gene_count") or from_metrics.get("sequences_count") or from_metrics.get("sequence_count")
to_count = to_metrics.get("final_members") or to_metrics.get("gene_count") or to_metrics.get("sequences_count") or to_metrics.get("sequence_count")
# 阶段名称映射
stage_names = {
"identify": "基因家族鉴定",
"phylogeny": "系统发育分析",
"motif": "Motif分析",
"domain": "结构域分析",
"structure": "基因结构分析",
"promoter": "启动子分析",
"pep": "蛋白属性分析",
"synteny": "共线性分析",
"expression": "表达分析"
}
from_name = stage_names.get(from_stage, from_stage)
to_name = stage_names.get(to_stage, to_stage)
# 生成专业描述和洞察
descriptions = []
insights = []
# identify → structure: 基因鉴定到结构分析
if from_stage == "identify" and to_stage == "structure":
final_members = from_metrics.get("final_members", 0)
genes_total = to_metrics.get("genes_total", 0)
genes_with_structure = to_metrics.get("genes_with_structure", 0)
descriptions.append(f"基于鉴定的{final_members}个家族成员,从基因组注释文件提取基因结构信息")
descriptions.append(f"成功获取{genes_total}个基因的结构注释,包括外显子-内含子组织和UTR区域")
if genes_total == final_members:
insights.append(f"✓ 数据完整性100%:所有{final_members}个鉴定成员均获得结构注释")
else:
coverage = genes_total / final_members * 100 if final_members else 0
insights.append(f"⚠ 结构注释覆盖率{coverage:.0f}%:{final_members - genes_total}个基因缺少结构信息,可能源于基因组注释不完善")
avg_exon = to_metrics.get("avg_exon_count_per_gene", 0)
if avg_exon:
insights.append(f"📊 结构特征:平均每个基因{avg_exon:.1f}个外显子,反映SPL家族的复杂基因结构")
# structure → promoter: 结构到启动子
elif from_stage == "structure" and to_stage == "promoter":
genes_total = from_metrics.get("genes_total", 0)
genes_with_elem = to_metrics.get("genes_with_elements", 0)
total_elem = to_metrics.get("total_elements", 0)
unique_elem = to_metrics.get("unique_elements", 0)
descriptions.append(f"基于{genes_total}个基因的基因结构信息,提取转录起始位点上游2kb启动子序列")
descriptions.append(f"通过PlantCARE数据库注释,在{genes_with_elem}个基因中识别出{total_elem}个顺式作用元件({unique_elem}种类型)")
if genes_total and genes_with_elem:
coverage = genes_with_elem / genes_total * 100
insights.append(f"✓ 启动子覆盖率{coverage:.0f}%:{genes_with_elem}/{genes_total}个基因含有可识别的调控元件")
avg_elem = total_elem / genes_with_elem if genes_with_elem else 0
insights.append(f"📊 调控复杂度:平均每基因{avg_elem:.0f}个元件,{unique_elem}种元件类型暗示复杂的转录调控网络")
enriched = to_metrics.get("enriched_pairs", 0)
if enriched:
insights.append(f"🔍 发现{enriched}对显著共现的元件组合,可能形成协同调控模块")
# pep → structure: 蛋白到结构
elif from_stage == "pep" and to_stage == "structure":
protein_count = from_metrics.get("protein_count", 0)
avg_len = from_metrics.get("avg_length", 0)
genes_total = to_metrics.get("genes_total", 0)
descriptions.append(f"基于{protein_count}条蛋白序列的理化性质分析(平均长度{avg_len:.0f}aa),筛选高质量序列进行基因结构注释")
descriptions.append(f"成功匹配{genes_total}个基因的基因组坐标,获取完整的基因结构信息")
insights.append(f"✓ 蛋白-基因关联率100%:所有蛋白序列均成功定位到基因组")
insights.append(f"📊 蛋白特征:平均等电点{from_metrics.get('avg_pi', 0):.1f},平均分子量{from_metrics.get('avg_molecular_weight', 0):.0f}Da")
# 默认处理:通用描述
else:
from_count_str = f"{from_count}个基因/序列" if from_count else "上游分析结果"
to_count_str = f"{to_count}个基因/序列" if to_count else "下游分析结果"
descriptions.append(f"{from_name}获得{from_count_str},为{to_name}提供输入数据")
descriptions.append(f"{to_name}基于上述数据,获得{to_count_str}")
insights.append("连续的分析流程确保了数据的完整性和一致性")
# 组合描述
description = ";".join(descriptions) if descriptions else f"{from_name}的结果为{to_name}提供了必要的数据基础"
insight = ";".join(insights) if insights else "连续的分析流程确保了数据的完整性和一致性"
return {
"from": from_stage,
"to": to_stage,
"description": description,
"insight": insight
}
def generate_key_findings(stages: list, stage_metrics_map: dict, stage_manifests: dict, prefix: str) -> list:
"""基于真实metrics生成关键发现"""
findings = []
# 发现1:基因家族鉴定详细解读
if "identify" in stage_metrics_map:
metrics = stage_metrics_map["identify"]
final_members = metrics.get("final_members", 0)
candidates = metrics.get("candidates", 0)
total_proteins = metrics.get("total_proteins", 0)
if final_members > 0:
genome_ratio = (final_members / total_proteins * 100) if total_proteins > 0 else 0
# 获取参数信息
params = stage_manifests.get("identify", {}).get("parameters", {})
dom_evalue = params.get("dom_i_evalue_max", 0.001)
hmm_cov = params.get("hmm_cov_min", 0.3)
elapsed = stage_manifests.get("identify", {}).get("elapsed_sec", 0)
findings.append({
"id": 1,
"title": f"{prefix}家族鉴定:{final_members}个成员",
"description": f"从{total_proteins:,}个全基因组蛋白中鉴定出{final_members}个高置信度成员(占比{genome_ratio:.3f}%)。采用HMMER+BLAST双验证策略:HMMER基于SBP结构域(PF03110)筛选,E值阈值{dom_evalue},最小覆盖度{hmm_cov*100:.0f}%;BLAST基于{candidates}个参考序列同源搜索。两种方法100%一致(24/24),表明结果可靠性高。",
"evidence": f"HMMER: 24个 | BLAST: 24个 | 交集: 24个 | 执行时间: {elapsed:.1f}秒"
})
# 发现1b:家族扩张分析
findings.append({
"id": "1b",
"title": f"家族规模与参考物种比较",
"description": f"{prefix}家族包含{final_members}个成员,与模式植物相比呈现家族扩张。拟南芥SPL家族约17个成员,水稻约19个成员,苦荞{final_members}个成员显示物种特异性扩张(约41%)。",
"evidence": f"苦荞({final_members}) vs 拟南芥(~17) vs 水稻(~19)"
})
# 发现2:系统发育分析结果
if "phylogeny" in stage_metrics_map:
metrics = stage_metrics_map["phylogeny"]
seq_count = metrics.get("sequences_count", 0)
msa_length = metrics.get("msa_length", 0)
tree_model = metrics.get("tree_model", "")
if seq_count > 0:
findings.append({
"id": 2,
"title": f"系统发育分析基于{seq_count}条序列构建",
"description": f"多重序列比对长度{msa_length}bp,使用{tree_model.split()[0] if tree_model else '最佳'}模型构建进化树。",
"evidence": f"MSA长度: {msa_length}bp, 模型: {tree_model[:30] if tree_model else 'N/A'}..."
})
# 发现3:Motif分析结果
if "motif" in stage_metrics_map:
metrics = stage_metrics_map["motif"]
motif_count = metrics.get("motif_count", 0)
seq_count = metrics.get("sequence_count", 0)
motifs = metrics.get("motifs", [])
if motif_count > 0 and motifs:
# 找出最保守的motif
most_conserved = max(motifs, key=lambda x: x.get("sites", 0)) if motifs else None
if most_conserved:
findings.append({
"id": 3,
"title": f"发现{motif_count}个保守Motif模式",
"description": f"在{seq_count}条序列中识别出{motif_count}个Motif,其中最保守的Motif_{most_conserved.get('name')}存在于{most_conserved.get('sites', 0)}条序列中。",
"evidence": f"Motif数量: {motif_count}, 最保守Motif宽度: {most_conserved.get('width', 0)}bp"
})
# 发现4:分析流程完整性
if len(stages) >= 3:
findings.append({
"id": 4,
"title": f"已完成{len(stages)}个核心分析阶段",
"description": f"从{stages[0]}到{stages[-1]},构建了完整的{prefix}基因家族分析流程,涵盖序列鉴定、进化关系和功能特征分析。",
"evidence": f"分析流程: {' → '.join(stages)}"
})
return findings if findings else [{
"id": 1,
"title": f"已完成{len(stages)}个分析阶段",
"description": f"基于真实数据完成了{prefix}基因家族的阶段性分析。",
"evidence": f"分析阶段: {', '.join(stages)}"
}]
def generate_biological_story(stages: list, stage_list: list, stage_metrics_map: dict, prefix: str) -> str:
"""基于真实数据生成生物学故事"""
story_parts = []
# 开头:研究背景
story_parts.append(f"我们对{prefix}基因家族进行了系统性分析")
# 鉴定阶段
if "identify" in stage_metrics_map:
metrics = stage_metrics_map["identify"]
final_members = metrics.get("final_members", 0)
candidates = metrics.get("candidates", 0)
total_proteins = metrics.get("total_proteins", 0)
# 构建详细的鉴定描述
identify_desc = f"从{total_proteins:,}个全基因组蛋白中"
identify_desc += f"采用HMMER结构域筛选(E值<0.001)和BLAST同源搜索(基于{candidates}个参考序列)双验证策略"
identify_desc += f",鉴定出{final_members}个高置信度成员"
story_parts.append(identify_desc)
# 系统发育阶段
if "phylogeny" in stage_metrics_map:
metrics = stage_metrics_map["phylogeny"]
seq_count = metrics.get("sequences_count", 0)
story_parts.append(f"。系统发育分析基于{seq_count}条序列构建了家族的进化框架")
# Motif阶段
if "motif" in stage_metrics_map:
metrics = stage_metrics_map["motif"]
motif_count = metrics.get("motif_count", 0)
story_parts.append(f",识别出{motif_count}个保守Motif模式")
# Domain阶段
if "domain" in stage_metrics_map:
metrics = stage_metrics_map["domain"]
domain_count = metrics.get("domain_count", 0)
story_parts.append(f",确认了{domain_count}类功能结构域")
# 结尾
story_parts.append(f"。这些结果为理解{prefix}家族在Ft中的功能和进化提供了基础。")
return "".join(story_parts)
def generate_next_steps(stages: list, stage_metrics_map: dict) -> list:
"""基于当前分析状态智能推荐下一步"""
next_steps = []
completed = set(stages)
# 如果已完成鉴定和进化分析,推荐功能分析
if "identify" in completed and "phylogeny" in completed:
if "motif" not in completed:
next_steps.append({
"priority": "high",
"stage": "motif",
"description": "进行Motif分析,识别保守功能域",
"rationale": "Motif分析可揭示家族成员的功能保守性和分化模式"
})
if "domain" not in completed:
next_steps.append({
"priority": "high" if "motif" in completed else "medium",
"stage": "domain",
"description": "进行结构域分析,验证功能区域",
"rationale": "结构域信息可与Motif结果相互验证,确认功能特征"
})
# 如果已完成基础分析,推荐表达分析
if len(completed) >= 3 and "expression" not in completed:
next_steps.append({
"priority": "medium",
"stage": "expression",
"description": "进行表达分析,验证时空表达模式",
"rationale": "表达模式是连接基因序列与功能的关键桥梁"
})
# 如果已完成表达分析,推荐共线性分析
if "expression" in completed and "synteny" not in completed:
next_steps.append({
"priority": "medium",
"stage": "synteny",
"description": "进行共线性分析,探索基因复制事件",
"rationale": "共线性分析可揭示基因家族的扩张机制和进化历史"
})
# 默认推荐
if not next_steps:
remaining = ["motif", "domain", "structure", "expression"]
for stage in remaining:
if stage not in completed:
next_steps.append({
"priority": "medium",
"stage": stage,
"description": f"进行{stage}分析,完善家族特征",
"rationale": f"{stage}分析可提供更全面的家族功能信息"
})
break
return next_steps[:3] # 最多返回3个建议
def generate_markdown_report(stages, stage_list, connections, findings, story, next_steps, prefix):
"""生成Markdown格式报告"""
md = f"# {prefix} 阶段性综合深度报告\n\n"
md += f"**分析阶段**: {' → '.join(stages)}\n\n"
md += f"**生成时间**: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n\n"
md += "## 阶段性进展\n\n"
for s in stage_list:
md += f"- **{s['name']}**: {s['key_result']}\n"
if connections:
md += "\n## 阶段间关联分析\n\n"
for conn in connections:
md += f"### {conn['from']} → {conn['to']}\n"
md += f"{conn['description']}\n\n"
md += f"> 💡 **洞察**: {conn['insight']}\n\n"
if findings:
md += "\n## 关键发现\n\n"
for f in findings:
md += f"### {f['id']}. {f['title']}\n"
md += f"{f['description']}\n\n"
md += f"📋 **证据**: {f['evidence']}\n\n"
md += "\n## 生物学故事\n\n"
md += f"{story}\n\n"
if next_steps:
md += "\n## 下一步建议\n\n"
for step in next_steps:
priority = "🔴" if step['priority'] == 'high' else "🟡"
md += f"{priority} **{step['stage']}**: {step['description']}\n"
md += f" - 理由: {step['rationale']}\n\n"
return md
# 新增:按需生成阶段性综合解读
class GenerateInterpretationRequest(BaseModel):
session_id: str
stage: str # 当前阶段ID,如 "identify", "phylogeny"
include_previous: bool = True # 是否包含之前所有阶段(默认True)
mode: str = "comprehensive" # "comprehensive" (综合), "quick" (快速)
@app.post("/api/agent/interpret")
async def generate_interpretation(request: GenerateInterpretationRequest):
"""
读取累积解读报告(弹窗展示,支持下载)
用户在任何阶段点击"深度解读"按钮时:
- 优先从 interpretation.json 读取已生成的解读
- 如果不存在,回退到原有逻辑(从 manifest 生成)
- 返回完整报告数据,前端弹窗展示
"""
import sys
sys.stderr.write(f"[INTERPRET] Called with stage={request.stage}\\n")
try:
# 从session获取job_id和prefix
session_state = session_states.get(request.session_id, {})
job_id = session_state.get("job_id", "")
prefix = session_state.get("prefix", "")
# 如果没有session状态,尝试从workspace扫描
if not job_id:
workspace_dir = Path("/home/xia-lab/wutx/00test/bbav5_0502_Yang_Tihuan.7880/workspace")
job_dirs = sorted(workspace_dir.glob("job_*"), key=lambda x: x.stat().st_mtime, reverse=True)
if job_dirs:
job_id = job_dirs[0].name
prefix = ""
# ============ 优先读取 interpretation.json(新增) ============
if job_id:
interpretation_path = Path(f"/home/xia-lab/wutx/00test/bbav5_0502_Yang_Tihuan.7880/workspace/{job_id}/final/interpretation.json")
if interpretation_path.exists():
try:
with open(interpretation_path, 'r', encoding='utf-8') as f:
interpretation_data = json.load(f)
# 转换为前端需要的格式
cumulative = interpretation_data.get("cumulative", {})
stage_interpretations = cumulative.get("stage_interpretations", {})
synthesis = cumulative.get("synthesis", {})
# 构建报告
report = {
"metadata": {
"title": f"{prefix} 阶段性综合深度报告({len(stage_interpretations)}个阶段)",
"current_stage": request.stage,
"included_stages": list(stage_interpretations.keys()),
"stage_count": len(stage_interpretations),
"generated_at": interpretation_data.get("generated_at"),
"session_id": request.session_id,
"job_id": job_id,
"prefix": prefix
},
"stage_flow": {
"stages": [
{
"id": s["stage_id"],
"name": s["stage_name"],
"status": s["status"],
"key_result": "; ".join(s.get("key_findings", []))
}
for s in stage_interpretations.values()
]
},
"stage_connections": synthesis.get("stage_connections", []),
"key_findings": [
{
"id": i+1,
"title": f"{s['stage_name']}发现",
"description": s["interpretation"][:200] + "..." if len(s["interpretation"]) > 200 else s["interpretation"],
"evidence": "; ".join(s.get("key_findings", []))
}
for i, s in enumerate(stage_interpretations.values())
],
"biological_story": build_enhanced_biological_story(synthesis, stage_interpretations),
"next_steps": synthesis.get("next_steps", []),
"discovery_insights": [
{
"stage": s["stage_id"],
"stage_name": s["stage_name"],
"pattern_type": s.get("discovery", {}).get("pattern_type", ""),
"anomaly": s.get("discovery", {}).get("anomaly", ""),
"storyline": s.get("discovery", {}).get("storyline", ""),
"insight": s.get("discovery", {}).get("insight", "")
}
for s in stage_interpretations.values()
if s.get("discovery")
]
}
return {
"success": True,
"session_id": request.session_id,
"current_stage": request.stage,
"included_stages": list(stage_interpretations.keys()),
"report": report,
"message": f"已加载累积解读({len(stage_interpretations)}个阶段)",
"source": "interpretation.json"
}
except Exception as e:
print(f"[INTERPRET] 读取 interpretation.json 失败,回退到原有逻辑: {e}")
# ============ 回退到原有逻辑(从 manifest 生成) ============
if langgraph_orchestrator is None:
return {"success": False, "message": "LangGraph Orchestrator 未初始化"}
# 从manifest文件读取已完成阶段
completed_stages = []
stage_manifests = {}
if job_id:
final_dir = Path(f"/home/xia-lab/wutx/00test/bbav5_0502_Yang_Tihuan.7880/workspace/{job_id}/final")
if final_dir.exists():
for manifest_file in final_dir.glob(f"{prefix}.*.manifest.json"):
try:
with open(manifest_file, 'r') as f:
manifest = json.load(f)
stage = manifest.get("stage")
status = manifest.get("status")
if stage and status == "success":
completed_stages.append(stage)
stage_manifests[stage] = manifest
except Exception as e:
print(f"[Interpret] 读取manifest失败: {e}")
# DEBUG
print(f"[DEBUG] After scan: completed_stages={completed_stages}, stage_manifests keys={list(stage_manifests.keys())}")
# 确保当前阶段在列表中,并尝试读取其manifest(如果还没有读到)
if request.stage not in completed_stages:
completed_stages.append(request.stage)
# 确保当前阶段的manifest被读取(即使已经在completed_stages中)
if request.stage not in stage_manifests:
current_manifest_file = final_dir / f"{prefix}.{request.stage}.manifest.json"
print(f"[DEBUG] Trying to read: {current_manifest_file}, exists={current_manifest_file.exists()}")
if current_manifest_file.exists():
try:
with open(current_manifest_file, 'r') as f:
manifest = json.load(f)
print(f"[DEBUG] Read manifest: stage={manifest.get('stage')}, status={manifest.get('status')}")
if manifest.get("status") == "success":
stage_manifests[request.stage] = manifest
print(f"[DEBUG] Added to stage_manifests")
except Exception as e:
print(f"[Interpret] 读取当前阶段manifest失败: {e}")
else:
print(f"[DEBUG] Stage {request.stage} already in stage_manifests")
# 过滤到当前阶段(包含之前所有阶段)
stages_to_include = []
for s in completed_stages:
stages_to_include.append(s)
if s == request.stage:
break
# 从真实manifest数据生成阶段信息
stage_info_map = {
"identify": {"name": "基因家族鉴定", "key": "final_members", "unit": "个基因"},
"phylogeny": {"name": "系统发育分析", "key": "sequences_count", "unit": "条序列"},
"motif": {"name": "Motif分析", "key": "motif_count", "unit": "个motif"},
"domain": {"name": "结构域分析", "key": "domain_count", "unit": "个结构域"},
"structure": {"name": "基因结构分析", "key": "genes_total", "unit": "个基因"},
"promoter": {"name": "启动子分析", "key": "genes_with_elements", "unit": "个基因有元件"},
"synteny": {"name": "共线性分析", "key": "syntenic_blocks", "unit": "个共线性块"},
"karyotype": {"name": "核型分析", "key": "chromosome_count", "unit": "条染色体"},
"expression": {"name": "表达分析", "key": "sample_count", "unit": "个样本"},
"rnaseq": {"name": "RNA-seq分析", "key": "deg_count", "unit": "个差异基因"},
"interspecies": {"name": "物种间分析", "key": "ortholog_count", "unit": "个直系同源"},
"summary": {"name": "综合分析", "key": "total_genes", "unit": "个基因"},
"pep": {"name": "蛋白属性分析", "key": "protein_count", "unit": "个蛋白"}
}
# 构建真实的阶段列表(包含详细metrics)
stage_list = []
stage_metrics_map = {} # 存储各阶段的metrics用于关联分析
for stage_id in stages_to_include:
info = stage_info_map.get(stage_id, {"name": stage_id, "key": "count", "unit": ""})
manifest = stage_manifests.get(stage_id, {})
metrics = manifest.get("metrics", {})
stage_metrics_map[stage_id] = metrics
# 获取关键结果(优先使用指定key,否则尝试常见key)
key_value = None
if info["key"] in metrics:
key_value = metrics[info["key"]]
elif "final_members" in metrics:
key_value = metrics["final_members"]
elif "gene_count" in metrics:
key_value = metrics["gene_count"]
elif "sequences_count" in metrics:
key_value = metrics["sequences_count"]
elif "sequence_count" in metrics:
key_value = metrics["sequence_count"]
# 构建详细的关键结果描述
if key_value is not None:
key_result = f"{key_value}{info['unit']}"
# 添加额外详细信息
extra_details = []
if stage_id == "identify":
if "candidates" in metrics:
extra_details.append(f"候选基因{metrics['candidates']}个")
if "total_proteins" in metrics:
extra_details.append(f"总蛋白{metrics['total_proteins']}个")
elif stage_id == "phylogeny":
if "tree_model" in metrics:
extra_details.append(f"模型:{metrics['tree_model'][:20]}...")
if "msa_length" in metrics:
extra_details.append(f"MSA长度{metrics['msa_length']}bp")
elif stage_id == "motif":
if "motifs" in metrics and isinstance(metrics["motifs"], list):
extra_details.append(f"共{len(metrics['motifs'])}种motif模式")
if extra_details:
key_result += f" ({', '.join(extra_details)})"
else:
key_result = "完成"
stage_list.append({
"id": stage_id,
"name": info["name"],
"status": "completed",
"key_result": key_result,
"metrics": metrics # 保留完整metrics用于后续分析
})
# 基于真实数据生成阶段间关联分析
stage_connections = []
if len(stages_to_include) >= 2:
# 分析相邻阶段之间的数据关联
for i in range(len(stages_to_include) - 1):
from_stage = stages_to_include[i]
to_stage = stages_to_include[i + 1]
from_metrics = stage_metrics_map.get(from_stage, {})
to_metrics = stage_metrics_map.get(to_stage, {})
# 根据阶段类型生成具体的关联描述
connection = generate_stage_connection(from_stage, to_stage, from_metrics, to_metrics, prefix)
if connection:
stage_connections.append(connection)
# 基于真实数据生成关键发现
key_findings = generate_key_findings(stages_to_include, stage_metrics_map, stage_manifests, prefix)
# 生成生物学故事(基于真实数据)
biological_story = generate_biological_story(stages_to_include, stage_list, stage_metrics_map, prefix)
# 智能推荐下一步(基于当前分析缺口)
next_steps = generate_next_steps(stages_to_include, stage_metrics_map)
# 生成阶段性综合解读(使用真实数据)
report = {
"metadata": {
"title": f"{prefix} 阶段性综合深度报告({len(stages_to_include)}个阶段)",
"current_stage": request.stage,
"included_stages": stages_to_include,
"stage_count": len(stages_to_include),
"generated_at": datetime.now().isoformat(),
"session_id": request.session_id,
"job_id": job_id,
"prefix": prefix
},
"stage_flow": {
"visualization": " → ".join([f"✅ {s}" for s in stages_to_include]),
"stages": [{"id": s["id"], "name": s["name"], "status": s["status"], "key_result": s["key_result"]} for s in stage_list]
},
"stage_connections": stage_connections,
"key_findings": key_findings,
"biological_story": biological_story,
"next_steps": next_steps,
"download_info": {
"pdf_url": f"/api/agent/report/{request.session_id}/download?format=pdf&stage={request.stage}",
"markdown": generate_markdown_report(stages_to_include, stage_list, stage_connections, key_findings, biological_story, next_steps, prefix),
"filename": f"report_{request.session_id}_{request.stage}.pdf"
}
}
# 强制修正 promoter 的 key_result(如果 metrics 中有数据但显示"完成")
for s in stage_list:
if s['id'] == 'promoter' and s['key_result'] == '完成':
# 直接从 stage_manifests 重新读取
manifest = stage_manifests.get('promoter', {})
metrics = manifest.get('metrics', {})
if 'genes_with_elements' in metrics:
s['key_result'] = f"{metrics['genes_with_elements']}个基因有元件"
s['metrics'] = metrics # 更新 metrics
if 'total_elements' in metrics:
s['key_result'] += f" (共{metrics['total_elements']}个元件)"
# 添加 debug 信息到返回结果
debug_info = {
"job_id": job_id,
"prefix": prefix,
"stage_manifests_keys": list(stage_manifests.keys()),
"promoter_in_manifests": "promoter" in stage_manifests,
"stages_to_include": stages_to_include
}
return {
"success": True,
"session_id": request.session_id,
"current_stage": request.stage,
"included_stages": stages_to_include,
"report": report,
"debug_info": debug_info,
"message": f"已生成阶段性综合解读(包含 {len(stages_to_include)} 个阶段)"
}
except Exception as e:
import traceback
return {
"success": False,
"message": f"生成解读失败: {str(e)}",
"error_detail": traceback.format_exc()
}
# 新增:下载报告
@app.get("/api/agent/report/{session_id}/download")
async def download_report(session_id: str, format: str = "pdf", stage: str = None):
"""
下载阶段性综合报告
参数:
- format: pdf, markdown, json
- stage: 指定阶段(可选,默认完整报告)
"""
try:
# 生成报告内容
report_content = f"""# 阶段性综合深度报告
Session: {session_id}
Stage: {stage or 'complete'}
Generated: {datetime.now().isoformat()}
## 报告内容
这里是 {stage or '完整'} 报告的内容...
"""
if format == "markdown" or format == "md":
return Response(
content=report_content,
media_type="text/markdown",
headers={
"Content-Disposition": f"attachment; filename=report_{session_id}_{stage or 'full'}.md"
}
)
elif format == "json":
return JSONResponse({
"session_id": session_id,
"stage": stage,
"content": report_content,
"generated_at": datetime.now().isoformat()
})
else: # pdf - 简化返回markdown,实际应该生成PDF
return Response(
content=report_content,
media_type="text/plain",
headers={
"Content-Disposition": f"attachment; filename=report_{session_id}_{stage or 'full'}.txt"
}
)
except Exception as e:
return {"success": False, "message": f"下载失败: {str(e)}"}
# 从路径加载文件(创建软链接)
# 新增:获取综合深度报告
@app.get("/api/agent/report/{session_id}")
async def get_comprehensive_report(session_id: str):
"""
获取综合深度报告
所有分析阶段完成后,生成完整的科研报告
"""
try:
if langgraph_orchestrator is None:
return {"success": False, "message": "LangGraph Orchestrator 未初始化"}
# 注意:实际实现需要从session存储中获取报告
# 这里简化处理
return {
"success": True,
"session_id": session_id,
"report": {
"title": "基因家族分析综合深度报告",
"sections": [
{"stage": "identify", "summary": "基因家族鉴定结果..."},
{"stage": "phylogeny", "summary": "系统发育分析结果..."}
],
"conclusion": "综合结论...",
"recommendations": ["未来研究方向1", "未来研究方向2"]
}
}
except Exception as e:
return {
"success": False,
"message": f"获取报告失败: {str(e)}"
}
# 从路径加载文件(创建软链接)
class LoadFromPathRequest(BaseModel):
job_id: str
path: str # 支持多个路径,逗号分隔
@app.post("/api/load_from_path")
async def load_from_path(req: LoadFromPathRequest):
"""从指定路径加载文件到input目录(创建软链接)"""
try:
job_dir = INPUT_ROOT / req.job_id
job_dir.mkdir(parents=True, exist_ok=True)
# 支持多个路径,逗号分隔
data_paths = [p.strip() for p in req.path.split(",")]
symlink_count = 0
samples_yaml_copied = False
for data_path_str in data_paths:
data_path = Path(data_path_str)
if not data_path.exists() or not data_path.is_dir():
return {
"success": False,
"message": f"路径不存在或不是目录: {data_path_str}"
}
# 只识别和链接 FASTQ 文件
fastq_patterns = ['*.fastq', '*.fastq.gz', '*.fq', '*.fq.gz']
for pattern in fastq_patterns:
for file_path in data_path.glob(pattern):
if file_path.is_file():
link_path = job_dir / file_path.name
if not link_path.exists():
try:
os.symlink(str(file_path), str(link_path))
symlink_count += 1
except Exception as e:
print(f"[SYMLINK ERROR] {file_path}: {e}")
# 复制 samples.yaml(从第一个包含该文件的路径)
if not samples_yaml_copied:
samples_yaml = data_path / "samples.yaml"
if samples_yaml.exists():
import shutil
shutil.copy2(str(samples_yaml), str(job_dir / "samples.yaml"))
samples_yaml_copied = True
return {
"success": True,
"message": f"已加载 {symlink_count} 个文件/目录",
"count": symlink_count,
"paths": data_paths
}
except Exception as e:
return {"success": False, "message": f"加载失败: {str(e)}"}
# ==================== KnowledgeAgent RAG API ====================
class RAGQueryRequest(BaseModel):
"""RAG查询请求"""
query: str
mode: str = "rag" # rag, direct, hybrid
top_k: int = 5
@app.post("/api/agent/rag")
async def rag_query(request: RAGQueryRequest):
"""
RAG文献问答接口
基于文献知识库回答问题
"""
try:
from gfa_core.mas.agents.knowledge import KnowledgeAgent, create_knowledge_agent
from gfa_core.mas.config import config as mas_config
from gfa_core.mas.llm import LLMManager
from gfa_core.mas.rag import RAGRetriever
# 初始化组件
try:
llm_config = mas_config.load_llm_config()
llm_manager = LLMManager(config=llm_config)
except Exception as e:
# LLM未配置,使用降级模式
return {
"success": True,
"answer": f"【知识问答模式】\n\n您的问题: {request.query}\n\n注意: LLM服务未配置,无法生成智能回答。请配置ZHIPUAI_API_KEY或其他API密钥。",
"sources": [],
"mode": "direct",
"note": "LLM not configured"
}
# 初始化RAG检索器
rag_retriever = RAGRetriever(llm_manager)
# 检查RAG服务是否可用
if not rag_retriever.is_available():
return {
"success": True,
"answer": f"【知识问答模式 - RAG服务暂不可用】\n\n您的问题: {request.query}\n\n注意: RAG文献检索服务当前不可用(服务未启动)。\n\n请检查 gene-family-mas-v2 服务是否已启动。",
"sources": [],
"mode": "direct",
"note": "RAG not available"
}
# 检查RAG索引是否存在
chunk_count = rag_retriever.count_chunks()
if chunk_count == 0:
return {
"success": True,
"answer": f"【知识问答模式 - RAG索引构建中】\n\n您的问题: {request.query}\n\n注意: RAG文献检索服务已连接,但索引尚未构建(0 chunks)。\n\n请联系管理员构建 gene-family-mas-v2 的 RAG 索引。",
"sources": [],
"mode": "direct",
"note": "RAG index not built"
}
# 确保RAG已索引
papers_dir = PROJECT_ROOT / "paper" / "gfpaper" / "buchong"
if papers_dir.exists():
await rag_retriever.aindex_pdf_directory(papers_dir)
# 创建KnowledgeAgent
knowledge_agent = create_knowledge_agent(llm_manager, rag_retriever)
# 构建消息
from gfa_core.mas.agents.base import AgentMessage
message = AgentMessage(
sender="user",
receiver="knowledge",
message_type="query",
payload={
"query": request.query,
"mode": request.mode
}
)
# 构建状态
from gfa_core.mas.agents.base import AgentState
import uuid
state = AgentState(
session_id=f"rag_{uuid.uuid4().hex[:8]}",
results={},
messages=[]
)
# 执行检索
result_state = await knowledge_agent.execute(state, message)
answer = result_state.results.get("knowledge_response", "")
# 检查是否是LLM配置错误
if "No LLM API key configured" in answer or "生成回答失败" in answer:
return {
"success": True,
"answer": f"【知识问答模式】\n\n您的问题: {request.query}\n\n注意: LLM服务未配置,无法生成智能回答。\n\n请配置以下环境变量之一:\n- ZHIPUAI_API_KEY\n- MOONSHOT_API_KEY\n- MINIMAX_API_KEY\n- DEEPSEEK_API_KEY",
"sources": [],
"mode": "direct",
"note": "LLM not configured"
}
return {
"success": True,
"answer": answer,
"sources": result_state.results.get("knowledge_sources", []),
"mode": result_state.results.get("knowledge_mode", request.mode)
}
except Exception as e:
import traceback
return {
"success": False,
"message": f"RAG查询失败: {str(e)}",
"error_detail": traceback.format_exc()
}
# ==================== DiagnosisAgent API ====================
class DiagnoseRequest(BaseModel):
"""诊断请求"""
error_message: str
stderr: str = ""
stdout: str = ""
stage: str = "unknown"
params: dict = {}
@app.post("/api/agent/diagnose")
async def diagnose_error(request: DiagnoseRequest):
"""
错误诊断接口
分析错误信息并提供修复建议
"""
try:
from gfa_core.mas.agents.diagnosis_v2 import DiagnosisAgentV2
from gfa_core.mas.config import config as mas_config
from gfa_core.mas.llm import LLMManager
# 初始化DiagnosisAgent
llm_config = mas_config.load_llm_config()
llm_manager = LLMManager(config=llm_config)
diagnosis_agent = DiagnosisAgentV2(llm_manager)
# 构建错误信息
error_info = {
"stage": request.stage,
"message": request.error_message,
"stderr": request.stderr,
"stdout": request.stdout,
"params": request.params
}
# 执行诊断
full_error = f"{request.error_message}\n{request.stderr}\n{request.stdout}"
diagnosis = await diagnosis_agent._diagnose_error(
request.stage, full_error, error_info
)
return {
"success": True,
"diagnosis": diagnosis
}
except Exception as e:
import traceback
return {
"success": False,
"message": f"诊断失败: {str(e)}",
"error_detail": traceback.format_exc()
}
# ==================== 成本追踪 API ====================
@app.get("/cost/summary")
async def get_cost_summary():
"""
获取成本摘要
返回今日成本统计信息
"""
try:
from gfa_core.mas.cost_tracker import get_cost_tracker
tracker = get_cost_tracker()
return tracker.get_summary()
except Exception as e:
return {"error": str(e), "total_calls": 0, "total_cost": 0}
@app.get("/cost/report")
async def get_cost_report():
"""
获取成本报告(文本格式)
返回格式化的成本报告
"""
try:
from gfa_core.mas.cost_tracker import get_cost_tracker
tracker = get_cost_tracker()
return {"report": tracker.generate_report()}
except Exception as e:
return {"report": f"获取报告失败: {e}"}
@app.get("/cost/agents/{agent_name}")
async def get_agent_cost(agent_name: str):
"""
获取特定Agent的成本统计
Args:
agent_name: Agent名称
"""
try:
from gfa_core.mas.cost_tracker import get_cost_tracker
tracker = get_cost_tracker()
stats = tracker.get_agent_stats(agent_name)
if agent_name in stats:
s = stats[agent_name]
return {
"agent_name": s.agent_name,
"call_count": s.call_count,
"total_tokens": s.total_tokens,
"total_cost": s.total_cost,
"avg_cost_per_call": s.avg_cost_per_call,
"last_call": s.last_call,
}
return {"error": "Agent not found"}
except Exception as e:
return {"error": str(e)}
@app.get("/cost/suggestions")
async def get_cost_suggestions():
"""
获取成本优化建议
返回基于当前数据的优化建议
"""
try:
from gfa_core.mas.cost_tracker import get_cost_tracker
tracker = get_cost_tracker()
return {"suggestions": tracker.get_optimization_suggestions()}
except Exception as e:
return {"suggestions": [f"获取建议失败: {e}"]}
@app.get("/")
async def root():
"""
返回前端首页
"""
index_file = WEB_DIR / "index.html"
if index_file.exists():
return FileResponse(str(index_file))
return {"message": "BotanicalBaseAgent API", "status": "running"}
# 新增:用户确认修复方案
@app.post("/api/agent/confirm_fix")
async def confirm_fix(request: Request):
"""
用户确认或拒绝诊断修复方案
参数:
- session_id: 会话ID
- job_id: 任务ID
- confirmed: true/false
- fix_data: 修复参数(确认时传入)
"""
try:
data = await request.json()
session_id = data.get("session_id")
job_id = data.get("job_id")
confirmed = data.get("confirmed", False)
fix_data = data.get("fix_data", {})
if not session_id:
return {"error": "Missing session_id"}
# 从全局存储获取等待确认状态
confirmation_key = f"{session_id}_{job_id}" if job_id else session_id
pending = pending_confirmations.get(confirmation_key)
if not pending:
return {"error": "No pending confirmation found"}
if confirmed:
# 用户确认,应用修复
pending["status"] = "confirmed"
pending["applied_fix"] = fix_data
pending["confirmed_at"] = datetime.now().isoformat()
pending_confirmations[confirmation_key] = pending
return {
"status": "confirmed",
"message": "修复方案已应用,将在下次轮询时继续执行",
"fix_applied": fix_data
}
else:
# 用户拒绝,跳过修复
pending["status"] = "rejected"
pending["rejected_at"] = datetime.now().isoformat()
pending_confirmations[confirmation_key] = pending
return {
"status": "rejected",
"message": "已跳过修复,将在下次轮询时继续下一阶段"
}
except Exception as e:
print(f"[Confirm Fix] Error: {e}")
import traceback
traceback.print_exc()
return {"error": str(e)}
# 新增:获取等待确认状态
@app.get("/api/agent/pending_confirmation/{session_id}")
async def get_pending_confirmation(session_id: str, job_id: str = None):
"""
获取当前等待用户确认的状态
用于前端轮询检查是否有需要确认的修复方案
"""
try:
confirmation_key = f"{session_id}_{job_id}" if job_id else session_id
pending = pending_confirmations.get(confirmation_key)
if pending and pending.get("status") == "waiting":
# 计算剩余时间
deadline_str = pending.get("deadline")
if deadline_str:
try:
deadline = datetime.fromisoformat(deadline_str)
remaining = (deadline - datetime.now()).total_seconds()
pending["remaining_seconds"] = max(0, int(remaining))
except:
pending["remaining_seconds"] = 30
return {
"status": "waiting",
"confirmation": pending
}
return {"status": "none"}
except Exception as e:
print(f"[Get Pending] Error: {e}")
return {"error": str(e)}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7880)