2456 lines
92 KiB
Python
2456 lines
92 KiB
Python
#!/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
|
|||
|
|
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))
|
|||
|
|
|
|||
|
|
# 导入Celery任务
|
|||
|
|
try:
|
|||
|
|
from tasks import execute_agent_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
|
|||
|
|
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"
|
|||
|
|
|
|||
|
|
app = FastAPI(title="BotanicalBaseAgent API")
|
|||
|
|
|
|||
|
|
# 挂载静态文件目录
|
|||
|
|
WEB_DIR = Path(__file__).parent
|
|||
|
|
app.mount("/static", StaticFiles(directory=str(WEB_DIR)), name="static")
|
|||
|
|
|
|||
|
|
# 添加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: {llm_config.get('openai_api_key', 'NOT SET')[:20]}...")
|
|||
|
|
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",
|
|||
|
|
# 其他分析
|
|||
|
|
"pep", "ppi", "kaks", "expression",
|
|||
|
|
# 启动子分析(外部服务)
|
|||
|
|
"promoter",
|
|||
|
|
# 基因组准备
|
|||
|
|
"genome_prepare",
|
|||
|
|
# RNA-seq全流程
|
|||
|
|
"rnaseq_qc", "rnaseq_align", "rnaseq_quant", "rnaseq_de", "rnaseq_enrich"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
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"
|
|||
|
|
geoId: str = ""
|
|||
|
|
# RNA-seq参数
|
|||
|
|
rnaseqMode: str = "upload"
|
|||
|
|
speciesName: str = ""
|
|||
|
|
# RNA-seq QC 参数
|
|||
|
|
rnaseqDataMode: str = "upload"
|
|||
|
|
rnaseqDataPath: str = ""
|
|||
|
|
# RNA-seq Enrich参数
|
|||
|
|
topPathways: int = 20 # 富集分析热图通路数量
|
|||
|
|
# 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
|
|||
|
|
|
|||
|
|
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分钟,请耐心等待..."
|
|||
|
|
|
|||
|
|
# 执行
|
|||
|
|
result_msg, manifest = run_stage(
|
|||
|
|
stage=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),
|
|||
|
|
# 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"),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 恢复工作目录
|
|||
|
|
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"],
|
|||
|
|
"rnaseq_qc": ["rnaseq_qc"],
|
|||
|
|
"rnaseq_align": ["rnaseq_align"],
|
|||
|
|
"rnaseq_quant": ["rnaseq_quant"],
|
|||
|
|
"rnaseq_de": ["rnaseq_de"],
|
|||
|
|
"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.replace("Ft.SPL.", "")
|
|||
|
|
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.replace("Ft.SPL.", "")
|
|||
|
|
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)
|
|||
|
|
|
|||
|
|
# 整理 final 目录结构 (figures/reports/tables)
|
|||
|
|
try:
|
|||
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "scripts"))
|
|||
|
|
from reorganize_workspace import reorganize_workspace as reorganize
|
|||
|
|
reorganize(job_id)
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"[WARN] Failed to reorganize workspace: {e}")
|
|||
|
|
|
|||
|
|
# 严格验证:检查输出 manifest 是否存在且状态为 success
|
|||
|
|
expected_manifest = final_dir / f"{params['prefix']}.{stage}.manifest.json"
|
|||
|
|
manifest_valid = False
|
|||
|
|
manifest_status = None
|
|||
|
|
|
|||
|
|
if expected_manifest.exists():
|
|||
|
|
try:
|
|||
|
|
with open(expected_manifest, 'r', encoding='utf-8') as f:
|
|||
|
|
manifest_data = json.load(f)
|
|||
|
|
manifest_status = manifest_data.get("status")
|
|||
|
|
manifest_valid = (
|
|||
|
|
manifest_status == "success" and # manifest 状态必须是 success
|
|||
|
|
"❌" not in result_msg # 结果消息不包含失败标记
|
|||
|
|
)
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"[WARN] Failed to read manifest {expected_manifest}: {e}")
|
|||
|
|
|
|||
|
|
if not manifest_valid:
|
|||
|
|
# manifest 验证失败
|
|||
|
|
tasks[task_id]["status"] = "failed"
|
|||
|
|
tasks[task_id]["message"] = f"❌ {stage} 失败:manifest 状态为 {manifest_status}。{result_msg}"
|
|||
|
|
tasks[task_id]["end_time"] = datetime.now().isoformat()
|
|||
|
|
tasks[task_id]["result"] = {
|
|||
|
|
"success": False,
|
|||
|
|
"error": f"Manifest validation failed: expected {expected_manifest}, status={manifest_status}",
|
|||
|
|
"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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@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
|
|||
|
|
current_job_id = f"job_{int(time.time()*1000)}"
|
|||
|
|
|
|||
|
|
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.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
|
|||
|
|
|
|||
|
|
# ===== 新增:检查是否已完成 =====
|
|||
|
|
try:
|
|||
|
|
import json
|
|||
|
|
final_dir = WORKSPACE_ROOT / job_id / "final"
|
|||
|
|
if final_dir.exists():
|
|||
|
|
manifest_files = list(final_dir.glob("*.manifest.json"))
|
|||
|
|
completed_stages = []
|
|||
|
|
for mf in manifest_files:
|
|||
|
|
try:
|
|||
|
|
with open(mf) as f:
|
|||
|
|
data = json.load(f)
|
|||
|
|
if data.get("status") in ["success", "partial_success"]:
|
|||
|
|
completed_stages.append(data.get("stage"))
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
print(f"[API DEBUG] Completed stages from manifest: {completed_stages}")
|
|||
|
|
|
|||
|
|
if req.stage in completed_stages:
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"[API DEBUG] Error checking completed stages: {e}")
|
|||
|
|
# 继续执行,不阻断
|
|||
|
|
|
|||
|
|
# 对于耗时长的阶段,使用异步模式
|
|||
|
|
if req.stage in ASYNC_STAGES:
|
|||
|
|
task_id = str(uuid.uuid4())
|
|||
|
|
|
|||
|
|
# 创建任务记录
|
|||
|
|
tasks[task_id] = {
|
|||
|
|
"id": task_id,
|
|||
|
|
"stage": req.stage,
|
|||
|
|
"job_id": job_id,
|
|||
|
|
"status": "pending",
|
|||
|
|
"message": f"任务 {req.stage} 已提交",
|
|||
|
|
"created_at": datetime.now().isoformat(),
|
|||
|
|
"result": None
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 提交后台任务
|
|||
|
|
params = {
|
|||
|
|
"prefix": req.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),
|
|||
|
|
# 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'),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 其他阶段保持同步执行
|
|||
|
|
job_dir = INPUT_ROOT / job_id
|
|||
|
|
workspace_dir = WORKSPACE_ROOT / job_id
|
|||
|
|
|
|||
|
|
job_dir.mkdir(parents=True, exist_ok=True)
|
|||
|
|
workspace_dir.mkdir(parents=True, exist_ok=True)
|
|||
|
|
|
|||
|
|
# RNA-seq QC: 处理软链接模式
|
|||
|
|
if req.stage == "rnaseq_qc" and req.rnaseqDataMode == "symlink" and req.rnaseqDataPath:
|
|||
|
|
data_paths = [p.strip() for p in req.rnaseqDataPath.split(",")]
|
|||
|
|
symlink_count = 0
|
|||
|
|
|
|||
|
|
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,
|
|||
|
|
"async": False,
|
|||
|
|
"message": f"❌ 数据路径不存在或不是目录: {data_path_str}",
|
|||
|
|
"job_id": job_id
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 只创建 FASTQ 文件的软链接
|
|||
|
|
fastq_patterns = ['*.fastq', '*.fastq.gz', '*.fq', '*.fq.gz']
|
|||
|
|
for pattern in fastq_patterns:
|
|||
|
|
for fastq_file in data_path.glob(pattern):
|
|||
|
|
if fastq_file.is_file():
|
|||
|
|
link_path = job_dir / fastq_file.name
|
|||
|
|
if not link_path.exists():
|
|||
|
|
try:
|
|||
|
|
os.symlink(str(fastq_file), str(link_path))
|
|||
|
|
symlink_count += 1
|
|||
|
|
print(f"[SYMLINK] Created: {link_path} -> {fastq_file}")
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"[SYMLINK ERROR] {fastq_file}: {e}")
|
|||
|
|
|
|||
|
|
# 复制 samples.yaml(从第一个包含该文件的路径)
|
|||
|
|
samples_yaml = data_path / "samples.yaml"
|
|||
|
|
if samples_yaml.exists() and not (job_dir / "samples.yaml").exists():
|
|||
|
|
import shutil
|
|||
|
|
shutil.copy2(str(samples_yaml), str(job_dir / "samples.yaml"))
|
|||
|
|
print(f"[COPY] samples.yaml from {data_path} -> {job_dir}")
|
|||
|
|
|
|||
|
|
print(f"[SYMLINK] Total {symlink_count} files linked from {len(data_paths)} directories")
|
|||
|
|
|
|||
|
|
from apps.web.pipeline_backend import run_stage, JobInfo
|
|||
|
|
|
|||
|
|
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)
|
|||
|
|
|
|||
|
|
# DEBUG: 打印传入的RNA-seq参数
|
|||
|
|
print(f"[STAGE CALL DEBUG] rnaseq_mode={getattr(req, 'rnaseqMode', 'NOT_SET')}, species_name={getattr(req, 'speciesName', 'NOT_SET')}")
|
|||
|
|
try:
|
|||
|
|
result_msg, manifest = run_stage(
|
|||
|
|
stage=req.stage,
|
|||
|
|
job=job,
|
|||
|
|
prefix=req.prefix,
|
|||
|
|
cpu=req.cpu,
|
|||
|
|
required_pfams=req.requiredPfams or None,
|
|||
|
|
ref_species=req.refSpecies or None,
|
|||
|
|
id_evalue=req.idEvalue,
|
|||
|
|
id_coverage=req.idCoverage,
|
|||
|
|
blast_evalue=req.blastEvalue,
|
|||
|
|
phy_mafft=req.mafftParams,
|
|||
|
|
phy_iqtree=req.iqtreeParams,
|
|||
|
|
motif_nmotifs=req.nmotifs,
|
|||
|
|
motif_minw=req.minw,
|
|||
|
|
motif_maxw=req.maxw,
|
|||
|
|
dom_evalue=req.domEvalue,
|
|||
|
|
dom_coverage=req.domCoverage,
|
|||
|
|
id_transform=req.idTransform,
|
|||
|
|
geo_id=getattr(req, 'geoId', ''),
|
|||
|
|
# RNA-seq参数
|
|||
|
|
rnaseq_mode=getattr(req, 'rnaseqMode', 'upload'),
|
|||
|
|
species_name=getattr(req, 'speciesName', ''),
|
|||
|
|
# RNA-seq Enrich参数
|
|||
|
|
top_pathways=getattr(req, 'topPathways', 20),
|
|||
|
|
# PPI参数
|
|||
|
|
ppi_species=getattr(req, 'ppiSpecies', 'ath'),
|
|||
|
|
ppi_score=getattr(req, 'ppiScore', 700),
|
|||
|
|
# KaKs参数
|
|||
|
|
kaks_neutral_rate=getattr(req, 'kaksNeutralRate', 7.08e-9),
|
|||
|
|
# Genome Prepare参数
|
|||
|
|
genome_prepare_type=getattr(req, 'genomePrepareType', 'auto'),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
os.chdir(original_cwd)
|
|||
|
|
|
|||
|
|
# 读取日志
|
|||
|
|
log_file = Path(workspace_dir) / "logs"
|
|||
|
|
log_files = sorted(log_file.glob("*.log"), key=lambda p: p.stat().st_mtime, reverse=True)
|
|||
|
|
log_content = ""
|
|||
|
|
if log_files:
|
|||
|
|
log_content = log_files[0].read_text()[-5000:]
|
|||
|
|
|
|||
|
|
# 收集结果
|
|||
|
|
images = []
|
|||
|
|
files = []
|
|||
|
|
|
|||
|
|
final_dir = workspace_dir / "final"
|
|||
|
|
if final_dir.exists():
|
|||
|
|
for ext in ['.png', '.jpg', '.jpeg', '.svg']:
|
|||
|
|
for f in final_dir.glob(f"*{ext}"):
|
|||
|
|
images.append(f"/workspace/{job_id}/final/{f.name}")
|
|||
|
|
|
|||
|
|
for f in final_dir.iterdir():
|
|||
|
|
if f.is_file():
|
|||
|
|
files.append({
|
|||
|
|
"name": f.name,
|
|||
|
|
"size": f.stat().st_size,
|
|||
|
|
"path": f"/workspace/{job_id}/final/{f.name}"
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"success": True,
|
|||
|
|
"async": False,
|
|||
|
|
"message": result_msg,
|
|||
|
|
"log": log_content,
|
|||
|
|
"images": images,
|
|||
|
|
"files": files,
|
|||
|
|
"job_id": job_id
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
os.chdir(original_cwd)
|
|||
|
|
return {
|
|||
|
|
"success": False,
|
|||
|
|
"error": str(e)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.get("/api/task/{task_id}")
|
|||
|
|
async def get_task_status(task_id: str):
|
|||
|
|
"""查询异步任务状态"""
|
|||
|
|
if task_id not in tasks:
|
|||
|
|
raise HTTPException(status_code=404, detail="Task not found")
|
|||
|
|
|
|||
|
|
task = tasks[task_id]
|
|||
|
|
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")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.get("/api/stage-status")
|
|||
|
|
async def get_stage_status(job_id: str, stage: str):
|
|||
|
|
"""检查阶段是否已完成(通过manifest文件)"""
|
|||
|
|
from pathlib import Path
|
|||
|
|
import json
|
|||
|
|
|
|||
|
|
workspace_dir = WORKSPACE_ROOT / job_id
|
|||
|
|
final_dir = workspace_dir / "final"
|
|||
|
|
|
|||
|
|
# 查找manifest文件
|
|||
|
|
manifest_files = list(final_dir.glob(f"*.{stage}.manifest.json"))
|
|||
|
|
if not manifest_files:
|
|||
|
|
return {"completed": False, "stage": stage}
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
with open(manifest_files[0], 'r') as f:
|
|||
|
|
manifest = json.load(f)
|
|||
|
|
# manifest 中的 status 是 "success",但前端使用 "completed"
|
|||
|
|
is_completed = manifest.get("status") == "success"
|
|||
|
|
return {
|
|||
|
|
"completed": is_completed,
|
|||
|
|
"stage": stage,
|
|||
|
|
"status": "completed" if is_completed else manifest.get("status", "unknown"),
|
|||
|
|
"manifest": manifest
|
|||
|
|
}
|
|||
|
|
except Exception:
|
|||
|
|
return {"completed": False, "stage": stage}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@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():
|
|||
|
|
"""获取当前任务结果"""
|
|||
|
|
global current_job_id
|
|||
|
|
|
|||
|
|
if not current_job_id:
|
|||
|
|
return {"identify": {"images": [], "files": []}}
|
|||
|
|
|
|||
|
|
workspace_dir = WORKSPACE_ROOT / current_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},
|
|||
|
|
"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"],
|
|||
|
|
"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", "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分析"],
|
|||
|
|
"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 "Ft.SPL"
|
|||
|
|
|
|||
|
|
# 收集所有匹配的阶段(不再用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)
|
|||
|
|
|
|||
|
|
# 处理 "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
|
|||
|
|
prefix: str = "" # 前端设置的 prefix,用于解析物种和家族
|
|||
|
|
history: list = []
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.post("/api/agent/chat")
|
|||
|
|
async def agent_chat(request: AgentChatRequest):
|
|||
|
|
"""Agent智能对话接口 - 只做意图识别,不执行分析
|
|||
|
|
|
|||
|
|
修改说明:
|
|||
|
|
- 原版本调用 langgraph_orchestrator.run() 会执行完整分析流程
|
|||
|
|
- 新版本只做 Router + Intent 检测,返回意图结果
|
|||
|
|
- 前端根据意图结果决定下一步:
|
|||
|
|
- FAQ: 显示回答
|
|||
|
|
- Analysis: 调用 /api/agent/plan + 弹窗确认 + /api/agent/execute
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
# 优先使用 LangGraph Orchestrator(新版本)
|
|||
|
|
if langgraph_orchestrator is not None and LANGGRAPH_AVAILABLE:
|
|||
|
|
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
|
|||
|
|
|
|||
|
|
# 创建初始状态
|
|||
|
|
initial_state = langgraph_orchestrator.create_initial_state(
|
|||
|
|
session_id=session_id,
|
|||
|
|
query=request.message,
|
|||
|
|
job_id=request.job_id,
|
|||
|
|
files=[],
|
|||
|
|
prefix=request.prefix # 传递前端设置的 prefix
|
|||
|
|
)
|
|||
|
|
# 添加 completed_stages 到初始状态
|
|||
|
|
initial_state["completed_stages"] = completed_stages
|
|||
|
|
|
|||
|
|
# 【修改】只调用 Router 进行意图识别,不执行后续流程
|
|||
|
|
print(f"[Agent Chat] 只做意图识别,不执行分析: {request.message[:50]}...")
|
|||
|
|
router_result = await langgraph_orchestrator._router_node(initial_state)
|
|||
|
|
|
|||
|
|
# 获取意图结果
|
|||
|
|
intent_result = router_result.get("intent_result", {})
|
|||
|
|
mode = router_result.get("mode", "faq")
|
|||
|
|
|
|||
|
|
print(f"[Agent Chat] 意图识别结果: mode={mode}, gene_family={intent_result.get('gene_family', '未知')}")
|
|||
|
|
|
|||
|
|
# 返回意图识别结果(不执行分析)
|
|||
|
|
if mode == "analysis":
|
|||
|
|
# 分析任务:返回意图信息,前端需要调用 /api/agent/plan 获取详细计划
|
|||
|
|
return {
|
|||
|
|
"success": True,
|
|||
|
|
"intent": {
|
|||
|
|
"stage": intent_result.get("stage", ""),
|
|||
|
|
"taskType": "analysis",
|
|||
|
|
"gene_family": intent_result.get("gene_family", ""),
|
|||
|
|
"species": intent_result.get("species", ""),
|
|||
|
|
"params": intent_result,
|
|||
|
|
"confidence": intent_result.get("confidence", 0.8),
|
|||
|
|
"message": f"检测到分析任务: {intent_result.get('gene_family', '未知')} 基因家族分析"
|
|||
|
|
},
|
|||
|
|
"model_used": request.model,
|
|||
|
|
"session_id": session_id,
|
|||
|
|
"mode": "intent_only", # 标记:只做了意图识别
|
|||
|
|
"current_agent": "router"
|
|||
|
|
}
|
|||
|
|
else:
|
|||
|
|
# FAQ 模式:返回意图信息,前端调用 RAG
|
|||
|
|
return {
|
|||
|
|
"success": True,
|
|||
|
|
"intent": {
|
|||
|
|
"stage": None,
|
|||
|
|
"taskType": "faq",
|
|||
|
|
"params": {},
|
|||
|
|
"confidence": intent_result.get("confidence", 0.8),
|
|||
|
|
"message": intent_result.get("message", "检测到FAQ查询")
|
|||
|
|
},
|
|||
|
|
"model_used": request.model,
|
|||
|
|
"session_id": session_id,
|
|||
|
|
"mode": "faq",
|
|||
|
|
"current_agent": "router"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 降级:使用原 Orchestrator(保留兼容)- 也只做意图识别
|
|||
|
|
elif orchestrator is not None and MAS_AVAILABLE:
|
|||
|
|
# 使用意图解析器进行意图识别(不执行分析)
|
|||
|
|
print(f"[Agent Chat - Fallback] 使用意图解析器只做意图识别...")
|
|||
|
|
|
|||
|
|
session_id = request.session_id or f"session_{int(time.time())}"
|
|||
|
|
|
|||
|
|
# 使用 intent_parser 的正则匹配(已修复)
|
|||
|
|
try:
|
|||
|
|
from gfa_core.mas.utils.intent_parser import parse_gene_family, parse_intent_from_query
|
|||
|
|
|
|||
|
|
gene_family = parse_gene_family(request.message)
|
|||
|
|
intent_result = parse_intent_from_query(request.message)
|
|||
|
|
|
|||
|
|
mode = "analysis" if gene_family else "faq"
|
|||
|
|
|
|||
|
|
print(f"[Agent Chat - Fallback] 意图识别结果: mode={mode}, gene_family={gene_family}")
|
|||
|
|
|
|||
|
|
if mode == "analysis":
|
|||
|
|
return {
|
|||
|
|
"success": True,
|
|||
|
|
"intent": {
|
|||
|
|
"stage": intent_result.get("stage", "identify"),
|
|||
|
|
"taskType": "analysis",
|
|||
|
|
"gene_family": gene_family,
|
|||
|
|
"species": intent_result.get("species", "Ft"),
|
|||
|
|
"params": intent_result,
|
|||
|
|
"confidence": 0.85,
|
|||
|
|
"message": f"检测到分析任务: {gene_family} 基因家族分析"
|
|||
|
|
},
|
|||
|
|
"model_used": request.model,
|
|||
|
|
"session_id": session_id,
|
|||
|
|
"mode": "intent_only",
|
|||
|
|
"current_agent": "router"
|
|||
|
|
}
|
|||
|
|
else:
|
|||
|
|
return {
|
|||
|
|
"success": True,
|
|||
|
|
"intent": {
|
|||
|
|
"stage": None,
|
|||
|
|
"taskType": "faq",
|
|||
|
|
"params": {},
|
|||
|
|
"confidence": 0.85,
|
|||
|
|
"message": "检测到FAQ查询"
|
|||
|
|
},
|
|||
|
|
"model_used": request.model,
|
|||
|
|
"session_id": session_id,
|
|||
|
|
"mode": "faq",
|
|||
|
|
"current_agent": "router"
|
|||
|
|
}
|
|||
|
|
except Exception as intent_err:
|
|||
|
|
print(f"[Agent Chat - Fallback] 意图解析错误: {intent_err}")
|
|||
|
|
return {
|
|||
|
|
"success": False,
|
|||
|
|
"message": f"意图识别失败: {str(intent_err)}",
|
|||
|
|
"errors": [str(intent_err)]
|
|||
|
|
}
|
|||
|
|
else:
|
|||
|
|
# 降级:使用简单意图解析
|
|||
|
|
print("⚠️ Orchestrator不可用,使用降级方案")
|
|||
|
|
result = parse_natural_language(request.message)
|
|||
|
|
|
|||
|
|
if result["success"] and result["intent"]:
|
|||
|
|
intent = result["intent"]
|
|||
|
|
intent["model"] = request.model
|
|||
|
|
intent["taskType"] = "complex_analysis"
|
|||
|
|
intent["confidence"] = 0.85
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"success": True,
|
|||
|
|
"intent": intent,
|
|||
|
|
"model_used": request.model,
|
|||
|
|
"session_id": request.session_id or f"session_{int(time.time())}",
|
|||
|
|
"mode": "simple" # 标记使用简单方案
|
|||
|
|
}
|
|||
|
|
else:
|
|||
|
|
return {
|
|||
|
|
"success": False,
|
|||
|
|
"message": result.get("message", "未能理解您的意图"),
|
|||
|
|
"model_used": request.model,
|
|||
|
|
"mode": "simple"
|
|||
|
|
}
|
|||
|
|
except Exception as e:
|
|||
|
|
import traceback
|
|||
|
|
error_detail = traceback.format_exc()
|
|||
|
|
print(f"❌ Agent处理失败: {str(e)}\n{error_detail}")
|
|||
|
|
return {
|
|||
|
|
"success": False,
|
|||
|
|
"message": f"Agent处理失败: {str(e)}",
|
|||
|
|
"model_used": request.model,
|
|||
|
|
"error_detail": error_detail
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# RNA-seq分析相关
|
|||
|
|
import subprocess
|
|||
|
|
|
|||
|
|
# RNA-seq任务存储
|
|||
|
|
rnaseq_tasks: Dict[str, dict] = {}
|
|||
|
|
|
|||
|
|
class RnaseqRequest(BaseModel):
|
|||
|
|
stage: str # align, quant, de, enrich
|
|||
|
|
sample: str # 样本名
|
|||
|
|
params: dict = {}
|
|||
|
|
|
|||
|
|
class RnaseqJobRequest(BaseModel):
|
|||
|
|
sample_names: List[str]
|
|||
|
|
group_config: dict = {} # {sample: group}
|
|||
|
|
|
|||
|
|
@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")
|
|||
|
|
|
|||
|
|
# 脚本路径
|
|||
|
|
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")
|
|||
|
|
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")
|
|||
|
|
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
|
|||
|
|
model: str = "kimi"
|
|||
|
|
history: list = []
|
|||
|
|
auto_execute: bool = True # 是否自动执行分析
|
|||
|
|
prefix: str = "" # 前端传递的 prefix,格式如 "Ft.SPL"
|
|||
|
|
|
|||
|
|
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,
|
|||
|
|
"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 # 传递 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"
|
|||
|
|
|
|||
|
|
# 更新全局 current_job_id,确保 /api/results 能返回正确结果
|
|||
|
|
global current_job_id
|
|||
|
|
if request.job_id:
|
|||
|
|
current_job_id = request.job_id
|
|||
|
|
logger.info(f"[Agent] 更新 current_job_id: {current_job_id}")
|
|||
|
|
|
|||
|
|
if orchestrator is not None and MAS_AVAILABLE:
|
|||
|
|
from gfa_core.mas.core.state import GraphState
|
|||
|
|
|
|||
|
|
# 设置进度为运行中状态
|
|||
|
|
agent_tasks[task_id]["progress"] = 50
|
|||
|
|
agent_tasks[task_id]["current_stage"] = "执行中"
|
|||
|
|
|
|||
|
|
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 [],
|
|||
|
|
"user_input": {"prefix": request.prefix} # 传递 prefix
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 状态
|
|||
|
|
sync_celery_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"],
|
|||
|
|
"start_time": task.get("start_time"),
|
|||
|
|
"end_time": task.get("end_time"),
|
|||
|
|
"celery_task_id": task.get("celery_task_id", "")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def sync_celery_status(task: dict) -> dict:
|
|||
|
|
"""同步 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':
|
|||
|
|
task["status"] = "completed"
|
|||
|
|
task["progress"] = 100
|
|||
|
|
result = celery_task.result
|
|||
|
|
if result:
|
|||
|
|
task["result"] = result.get('result', {})
|
|||
|
|
if 'logs' in result:
|
|||
|
|
task["logs"] = result['logs']
|
|||
|
|
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}")
|
|||
|
|
return task
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.get("/api/agent/tasks")
|
|||
|
|
async def list_agent_tasks():
|
|||
|
|
"""列出所有Agent任务"""
|
|||
|
|
tasks = []
|
|||
|
|
for task_id, task in agent_tasks.items():
|
|||
|
|
# 同步 Celery 状态
|
|||
|
|
sync_celery_status(task)
|
|||
|
|
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 = ""
|
|||
|
|
history: list = []
|
|||
|
|
|
|||
|
|
|
|||
|
|
@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,
|
|||
|
|
"files": [],
|
|||
|
|
"current_agent": "planning",
|
|||
|
|
"status": "running",
|
|||
|
|
"errors": [],
|
|||
|
|
"final_output": {},
|
|||
|
|
"history": request.history if request.history else [],
|
|||
|
|
"results": {} # 用于存储已完成阶段
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 查询已完成的阶段(包括 success 和 partial_success)
|
|||
|
|
if request.session_id:
|
|||
|
|
workspace_dir = WORKSPACE_ROOT / request.session_id / "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)
|
|||
|
|
status = manifest_data.get("status", "")
|
|||
|
|
# 接受 success 和 partial_success 作为已完成状态
|
|||
|
|
if 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
|
|||
|
|
print(f"[Agent Plan] Completed stages: {completed_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", {})
|
|||
|
|
|
|||
|
|
# 构建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", "Ft.SPL"),
|
|||
|
|
"description": plan.get("description", ""),
|
|||
|
|
"estimated_time": plan.get("estimated_time", "未知"),
|
|||
|
|
"execution_strategy": plan.get("execution_strategy", "parallel"),
|
|||
|
|
"max_parallel": plan.get("max_parallel", 4),
|
|||
|
|
"total_stages": plan.get("total_stages", 0),
|
|||
|
|
"stages": plan.get("stages", []),
|
|||
|
|
"stages_detail": plan.get("stages_detail", []),
|
|||
|
|
"parallel_groups": plan.get("parallel_groups", []),
|
|||
|
|
"params": plan.get("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)
|
|||
|
|
},
|
|||
|
|
"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", "Ft.SPL"),
|
|||
|
|
"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", "Ft.SPL"), "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()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 从路径加载文件(创建软链接)
|
|||
|
|
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_v2 import KnowledgeAgentV2, 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已索引
|
|||
|
|
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"}
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
import uvicorn
|
|||
|
|
uvicorn.run(app, host="0.0.0.0", port=7868)
|