Files
2026-08-03 18:42:47 +08:00

2456 lines
92 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
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)