2497 lines
92 KiB
Python
Executable File
2497 lines
92 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
BotanicalBaseAgent - FastAPI Backend
|
||
支持Vue3前端的API接口 - Celery任务队列模式
|
||
"""
|
||
import os
|
||
import sys
|
||
import json
|
||
import asyncio
|
||
import uuid
|
||
import time
|
||
from pathlib import Path
|
||
from datetime import datetime
|
||
from typing import Optional, List, Dict
|
||
|
||
from fastapi import FastAPI, UploadFile, File, HTTPException, Request, BackgroundTasks, Query
|
||
from fastapi.responses import JSONResponse, FileResponse, Response
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.staticfiles import StaticFiles
|
||
from pydantic import BaseModel
|
||
from concurrent.futures import ThreadPoolExecutor
|
||
|
||
# 添加项目路径
|
||
PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
|
||
sys.path.insert(0, str(PROJECT_ROOT))
|
||
|
||
# 导入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
|
||
from gfa_core.mas.api.conversation_routes import router as conv_router # 会话管理路由
|
||
MAS_AVAILABLE = True
|
||
LANGGRAPH_AVAILABLE = True
|
||
print("✅ MAS组件导入成功")
|
||
print("✅ LangGraph编排器导入成功")
|
||
except ImportError as e:
|
||
MAS_AVAILABLE = False
|
||
LANGGRAPH_AVAILABLE = False
|
||
print(f"⚠️ MAS组件导入失败: {e}")
|
||
Orchestrator = None
|
||
LangGraphOrchestrator = None
|
||
LLMManager = None
|
||
MemoryManager = None
|
||
|
||
WORKSPACE_ROOT = PROJECT_ROOT / "workspace"
|
||
INPUT_ROOT = PROJECT_ROOT / "input"
|
||
|
||
app = FastAPI(title="BotanicalBaseAgent API")
|
||
|
||
# 挂载静态文件目录
|
||
WEB_DIR = Path(__file__).parent
|
||
app.mount("/static", StaticFiles(directory=str(WEB_DIR)), name="static")
|
||
|
||
# 挂载会话管理路由
|
||
if MAS_AVAILABLE and conv_router:
|
||
app.include_router(conv_router, prefix="/api")
|
||
print("✅ 会话管理路由已挂载")
|
||
|
||
# 添加CORS headers到所有响应
|
||
@app.middleware("http")
|
||
async def add_cors_headers(request, call_next):
|
||
if request.method == "OPTIONS":
|
||
return Response(
|
||
status_code=200,
|
||
headers={
|
||
"Access-Control-Allow-Origin": "*",
|
||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||
}
|
||
)
|
||
response = await call_next(request)
|
||
response.headers["Access-Control-Allow-Origin"] = "*"
|
||
return response
|
||
|
||
# 确保目录存在
|
||
WORKSPACE_ROOT.mkdir(exist_ok=True)
|
||
INPUT_ROOT.mkdir(exist_ok=True)
|
||
|
||
# 内存存储
|
||
jobs = {}
|
||
current_job_id = "default_job"
|
||
|
||
# 异步任务存储
|
||
tasks: Dict[str, dict] = {}
|
||
executor = ThreadPoolExecutor(max_workers=4)
|
||
|
||
# 初始化多Agent系统
|
||
orchestrator = None
|
||
langgraph_orchestrator = None
|
||
|
||
# 导入RAG集成
|
||
try:
|
||
from rag_integration import init_rag, RAG_AVAILABLE as RAG_INTEGRATION_AVAILABLE
|
||
if RAG_INTEGRATION_AVAILABLE:
|
||
init_rag(app)
|
||
print("✅ RAG集成已初始化")
|
||
else:
|
||
print("⚠️ RAG集成不可用")
|
||
except ImportError as e:
|
||
print(f"⚠️ RAG集成导入失败: {e}")
|
||
RAG_INTEGRATION_AVAILABLE = False
|
||
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",
|
||
# 组合图
|
||
"combo",
|
||
# 其他分析
|
||
"pep", "ppi", "kaks", "expression", "msa", "ortholog",
|
||
# 启动子分析(外部服务)
|
||
"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"],
|
||
"combo": ["combo"],
|
||
"msa": ["msa"],
|
||
"ortholog": ["ortholog"],
|
||
"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(job_id: str = Query(default=None, description="可选的job_id,默认使用当前job")):
|
||
"""获取任务结果,可指定job_id"""
|
||
global current_job_id
|
||
|
||
# 使用传入的job_id或当前job_id
|
||
target_job_id = job_id or current_job_id
|
||
|
||
if not target_job_id:
|
||
return {"identify": {"images": [], "files": []}}
|
||
|
||
workspace_dir = WORKSPACE_ROOT / target_job_id
|
||
final_dir = workspace_dir / "final"
|
||
|
||
stages = {
|
||
"identify": {"images": [], "files": [], "elapsed_time": 0},
|
||
"phylogeny": {"images": [], "files": [], "elapsed_time": 0},
|
||
"pep": {"images": [], "files": [], "elapsed_time": 0},
|
||
"motif": {"images": [], "files": [], "elapsed_time": 0},
|
||
"domain": {"images": [], "files": [], "elapsed_time": 0},
|
||
"structure": {"images": [], "files": [], "elapsed_time": 0},
|
||
"promoter": {"images": [], "files": [], "elapsed_time": 0},
|
||
"synteny": {"images": [], "files": [], "elapsed_time": 0},
|
||
"karyotype": {"images": [], "files": [], "elapsed_time": 0},
|
||
"interspecies_synteny": {"images": [], "files": [], "elapsed_time": 0},
|
||
"combo": {"images": [], "files": [], "elapsed_time": 0},
|
||
"expression": {"images": [], "files": [], "elapsed_time": 0},
|
||
"rnaseq_qc": {"images": [], "files": [], "elapsed_time": 0},
|
||
"rnaseq_align": {"images": [], "files": [], "elapsed_time": 0},
|
||
"rnaseq_quant": {"images": [], "files": [], "elapsed_time": 0},
|
||
"rnaseq_de": {"images": [], "files": [], "elapsed_time": 0},
|
||
"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
|
||
history: list = []
|
||
|
||
|
||
@app.post("/api/agent/chat")
|
||
async def agent_chat(request: AgentChatRequest):
|
||
"""Agent智能对话接口 - 只执行意图识别,不执行分析阶段
|
||
|
||
返回意图识别结果,前端根据 mode 判断:
|
||
- FAQ: 直接显示回答(前端调用 RAG)
|
||
- Analysis: 调用 /api/agent/plan 获取计划,确认后再执行
|
||
"""
|
||
try:
|
||
session_id = request.session_id or f"session_{int(time.time())}"
|
||
|
||
# 查询已完成的阶段(从manifest文件)
|
||
completed_stages = []
|
||
if request.job_id:
|
||
workspace_dir = PROJECT_ROOT / "workspace" / request.job_id / "final"
|
||
if workspace_dir.exists():
|
||
for manifest_file in workspace_dir.glob("*.manifest.json"):
|
||
try:
|
||
with open(manifest_file, 'r') as f:
|
||
manifest_data = json.load(f)
|
||
status = manifest_data.get("status", "")
|
||
if status in ["success", "partial_success"]:
|
||
stage = manifest_data.get("stage", "")
|
||
if stage:
|
||
completed_stages.append(stage)
|
||
except:
|
||
pass
|
||
|
||
# 只执行 Router + IntentAgent,不执行完整流程
|
||
# 1. RouterAgent 判断模式
|
||
from gfa_core.mas.agents.router import RouterAgent
|
||
from gfa_core.mas.agents.base import AgentState, AgentMessage, QueryMode
|
||
|
||
# 使用全局 llm_manager(已在文件顶部初始化)
|
||
router_agent = RouterAgent(llm_manager)
|
||
|
||
# 创建 AgentState 和 AgentMessage
|
||
router_state = AgentState(session_id=session_id)
|
||
router_message = AgentMessage(
|
||
sender="user",
|
||
receiver="router",
|
||
message_type="classify",
|
||
payload={"query": request.message}
|
||
)
|
||
|
||
# 调用 execute 方法
|
||
router_state = await router_agent.execute(router_state, router_message)
|
||
mode = router_state.mode.value
|
||
|
||
print(f"[Agent Chat] Router result: mode={mode}, query={request.message}")
|
||
|
||
# 2. 如果是 Analysis,调用 IntentAgentV2 获取 intent
|
||
intent = {}
|
||
if mode == "analysis" and request.job_id:
|
||
from gfa_core.mas.agents.intent_v2 import IntentAgentV2
|
||
|
||
intent_agent = IntentAgentV2(llm_manager)
|
||
|
||
# 构建 workspace 路径
|
||
workspace = str(PROJECT_ROOT / "workspace" / request.job_id)
|
||
|
||
# 创建 AgentState 和 AgentMessage
|
||
intent_state = AgentState(
|
||
session_id=session_id,
|
||
job_id=request.job_id
|
||
)
|
||
intent_message = AgentMessage(
|
||
sender="router",
|
||
receiver="intent",
|
||
message_type="recognize",
|
||
payload={
|
||
"query": request.message,
|
||
"workspace": workspace,
|
||
"completed_stages": completed_stages
|
||
}
|
||
)
|
||
|
||
# 调用 execute 方法
|
||
intent_state = await intent_agent.execute(intent_state, intent_message)
|
||
|
||
# 从 state.results 获取 intent(注意:IntentAgentV2 存储在 results.results.intent)
|
||
intent = intent_state.results.get("results", {}).get("intent", {})
|
||
intent["job_id"] = request.job_id
|
||
intent["workspace"] = workspace
|
||
|
||
# 添加 stage 字段(取第一个阶段)供前端判断
|
||
if intent.get("stages") and len(intent["stages"]) > 0:
|
||
intent["stage"] = intent["stages"][0]
|
||
|
||
# 添加 taskType 字段供前端判断
|
||
intent["taskType"] = "analysis" if intent.get("stages") else "faq"
|
||
|
||
# 返回意图识别结果
|
||
return {
|
||
"success": True,
|
||
"intent": intent,
|
||
"mode": mode,
|
||
"completed_stages": completed_stages,
|
||
"model_used": request.model,
|
||
"session_id": session_id
|
||
}
|
||
|
||
except Exception as e:
|
||
print(f"[Agent Chat] Error: {e}")
|
||
return {
|
||
"success": False,
|
||
"message": f"意图识别失败: {str(e)}",
|
||
"model_used": request.model,
|
||
"mode": "error"
|
||
}
|
||
|
||
|
||
|
||
# ==================== RNA-seq 接口 ====================
|
||
|
||
class RnaseqRequest(BaseModel):
|
||
"""RNA-seq分析请求"""
|
||
job_id: str
|
||
samples: list # 样本配置列表
|
||
design: dict = {} # 实验设计
|
||
|
||
|
||
@app.post("/api/rnaseq/run")
|
||
async def run_rnaseq(req: RnaseqRequest, background_tasks: BackgroundTasks):
|
||
"""运行RNA-seq分析"""
|
||
task_id = str(uuid.uuid4())
|
||
project_root = Path("/home/xia-lab/wutx/00test/bbav5")
|
||
|
||
# 脚本路径
|
||
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 # 是否自动执行分析
|
||
|
||
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 []
|
||
)
|
||
print(f"[DEBUG] Celery task submitted: {celery_task.id} for agent task {task_id}")
|
||
|
||
# 存储Celery任务ID映射
|
||
agent_tasks[task_id]["celery_task_id"] = celery_task.id
|
||
agent_tasks[task_id]["logs"].append({
|
||
"time": datetime.now().isoformat(),
|
||
"level": "info",
|
||
"message": f"任务已提交到Celery队列 (celery_task_id: {celery_task.id})"
|
||
})
|
||
|
||
except Exception as e:
|
||
print(f"[ERROR] Failed to submit Celery task: {e}")
|
||
agent_tasks[task_id]["status"] = "failed"
|
||
agent_tasks[task_id]["logs"].append({
|
||
"time": datetime.now().isoformat(),
|
||
"level": "error",
|
||
"message": f"Celery任务提交失败: {str(e)}"
|
||
})
|
||
else:
|
||
# Celery不可用,使用降级方案(直接执行)
|
||
print(f"[WARN] Celery不可用,使用降级方案执行")
|
||
agent_tasks[task_id]["logs"].append({
|
||
"time": datetime.now().isoformat(),
|
||
"level": "warn",
|
||
"message": "Celery不可用,使用本地异步执行"
|
||
})
|
||
|
||
async def execute_agent_task_async():
|
||
"""异步执行Agent任务(降级方案)"""
|
||
try:
|
||
agent_tasks[task_id]["status"] = "running"
|
||
|
||
if orchestrator is not None and MAS_AVAILABLE:
|
||
from gfa_core.mas.core.state import GraphState
|
||
|
||
initial_state: GraphState = {
|
||
"query": request.message,
|
||
"session_id": request.session_id or task_id,
|
||
"job_id": request.job_id, # 传入选中的 job_id
|
||
"files": [],
|
||
"current_agent": "router",
|
||
"status": "running",
|
||
"errors": [],
|
||
"final_output": {},
|
||
"history": request.history if request.history else []
|
||
}
|
||
|
||
result = await orchestrator.run(initial_state)
|
||
|
||
if result.get("status") == "completed":
|
||
agent_tasks[task_id]["status"] = "completed"
|
||
agent_tasks[task_id]["progress"] = 100
|
||
agent_tasks[task_id]["result"] = result.get("final_output", {})
|
||
else:
|
||
agent_tasks[task_id]["status"] = "failed"
|
||
else:
|
||
agent_tasks[task_id]["status"] = "failed"
|
||
agent_tasks[task_id]["logs"].append({
|
||
"time": datetime.now().isoformat(),
|
||
"level": "error",
|
||
"message": "Orchestrator不可用"
|
||
})
|
||
|
||
except Exception as e:
|
||
agent_tasks[task_id]["status"] = "failed"
|
||
agent_tasks[task_id]["logs"].append({
|
||
"time": datetime.now().isoformat(),
|
||
"level": "error",
|
||
"message": f"执行异常: {str(e)}"
|
||
})
|
||
finally:
|
||
agent_tasks[task_id]["end_time"] = datetime.now().isoformat()
|
||
|
||
asyncio.create_task(execute_agent_task_async())
|
||
|
||
return {
|
||
"success": True,
|
||
"task_id": task_id,
|
||
"message": "Agent任务已提交",
|
||
"status": "pending"
|
||
}
|
||
|
||
@app.get("/api/agent/status/{task_id}")
|
||
async def get_agent_status(task_id: str):
|
||
"""获取Agent任务执行状态"""
|
||
if task_id not in agent_tasks:
|
||
return {
|
||
"success": False,
|
||
"message": "任务不存在",
|
||
"task_id": task_id
|
||
}
|
||
|
||
task = agent_tasks[task_id]
|
||
|
||
# 如果有Celery任务ID,查询Celery状态
|
||
if CELERY_AVAILABLE and "celery_task_id" in task:
|
||
try:
|
||
from celery_app import app as celery_app
|
||
celery_task = celery_app.AsyncResult(task["celery_task_id"])
|
||
|
||
if celery_task.state == 'PENDING':
|
||
task["status"] = "pending"
|
||
elif celery_task.state == 'RUNNING':
|
||
task["status"] = "running"
|
||
# 更新进度信息
|
||
if celery_task.info:
|
||
task["current_stage"] = celery_task.info.get('current_stage', task["current_stage"])
|
||
task["progress"] = celery_task.info.get('progress', task["progress"])
|
||
if 'logs' in celery_task.info:
|
||
task["logs"] = celery_task.info['logs']
|
||
elif celery_task.state == 'SUCCESS':
|
||
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 {
|
||
"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")
|
||
}
|
||
}
|
||
|
||
@app.get("/api/agent/tasks")
|
||
async def list_agent_tasks():
|
||
"""列出所有Agent任务"""
|
||
tasks = []
|
||
for task_id, task in agent_tasks.items():
|
||
tasks.append({
|
||
"task_id": task["task_id"],
|
||
"status": task["status"],
|
||
"current_stage": task["current_stage"],
|
||
"progress": task["progress"],
|
||
"message": task["message"][:50] + "..." if len(task["message"]) > 50 else task["message"],
|
||
"start_time": task.get("start_time")
|
||
})
|
||
|
||
# 按时间倒序
|
||
tasks.sort(key=lambda x: x.get("start_time", ""), reverse=True)
|
||
|
||
return {
|
||
"success": True,
|
||
"tasks": tasks[:20] # 返回最近20个任务
|
||
}
|
||
|
||
@app.post("/api/agent/cancel/{task_id}")
|
||
async def cancel_agent_task(task_id: str):
|
||
"""取消Agent任务(标记为取消,实际后台任务可能仍在运行)"""
|
||
if task_id not in agent_tasks:
|
||
return {"success": False, "message": "任务不存在"}
|
||
|
||
if agent_tasks[task_id]["status"] in ["completed", "failed"]:
|
||
return {"success": False, "message": "任务已结束,无法取消"}
|
||
|
||
agent_tasks[task_id]["status"] = "cancelled"
|
||
agent_tasks[task_id]["logs"].append({
|
||
"time": datetime.now().isoformat(),
|
||
"level": "warning",
|
||
"message": "任务已取消"
|
||
})
|
||
|
||
return {"success": True, "message": "任务已标记为取消"}
|
||
|
||
|
||
class AgentPlanRequest(BaseModel):
|
||
"""Agent规划请求"""
|
||
message: str
|
||
session_id: str = ""
|
||
job_id: str = "" # 添加job_id字段,用于查询已完成阶段
|
||
history: list = []
|
||
intent: dict = {} # 前端可传入已有的intent,避免重复调用IntentAgentV2
|
||
|
||
|
||
@app.post("/api/agent/plan")
|
||
async def agent_plan(request: AgentPlanRequest):
|
||
"""
|
||
Agent任务规划接口 V2 - 使用LLM智能规划
|
||
|
||
返回详细的分析计划,包括:
|
||
- 识别的意图(基因家族、物种、阶段)
|
||
- 详细的阶段列表(含依赖、并行信息)
|
||
- 执行策略(并行/串行)
|
||
- 预估时间
|
||
"""
|
||
try:
|
||
# 使用Orchestrator V2进行规划
|
||
if orchestrator is not None and MAS_AVAILABLE:
|
||
from gfa_core.mas.core.state import GraphState
|
||
|
||
session_id = request.session_id or f"session_{int(time.time())}"
|
||
|
||
# 构建初始状态
|
||
initial_state: GraphState = {
|
||
"query": request.message,
|
||
"session_id": session_id,
|
||
"job_id": request.job_id, # 添加 job_id 用于 manifest 查询
|
||
"files": [],
|
||
"current_agent": "planning",
|
||
"status": "running",
|
||
"errors": [],
|
||
"final_output": {},
|
||
"history": request.history if request.history else [],
|
||
"results": {} # 用于存储已完成阶段
|
||
}
|
||
|
||
# 查询已完成的阶段(包括 success 和 partial_success)
|
||
# 使用 job_id 查询 workspace,而不是 session_id
|
||
workspace_name = request.job_id if request.job_id else request.session_id
|
||
if workspace_name:
|
||
workspace_dir = WORKSPACE_ROOT / workspace_name / "final"
|
||
if workspace_dir.exists():
|
||
completed_stages = []
|
||
for manifest_file in workspace_dir.glob("*.manifest.json"):
|
||
try:
|
||
with open(manifest_file, 'r') as f:
|
||
manifest_data = json.load(f)
|
||
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] Job_id: {workspace_name}, Completed stages: {completed_stages}")
|
||
|
||
# 如果前端传入了 intent,直接放入 state(避免 orchestrator.plan 重复调用 IntentAgentV2)
|
||
if request.intent and request.intent.get("stages"):
|
||
initial_state["results"]["intent"] = request.intent
|
||
print(f"[Agent Plan] 使用前端传入的 intent: {request.intent.get('stages', [])}")
|
||
|
||
# 调用PlanningAgent V2进行规划
|
||
print(f"[Agent Plan] Starting LLM planning for: {request.message}")
|
||
result = await orchestrator.plan(initial_state)
|
||
|
||
if result.get("status") == "completed":
|
||
plan = result.get("final_output", {}).get("analysis_plan", {})
|
||
intent = result.get("final_output", {}).get("intent", {})
|
||
|
||
# 构建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()
|
||
}
|
||
|
||
|
||
# ============ 新增:完整 Orchestrator 流程端点 ============
|
||
|
||
class AgentRunRequest(BaseModel):
|
||
"""Agent完整执行请求"""
|
||
message: str
|
||
session_id: str = ""
|
||
job_id: str = ""
|
||
intent: dict = {} # 前端可传入已有的intent
|
||
stages: list = [] # 要执行的阶段列表
|
||
|
||
|
||
@app.post("/api/agent/run")
|
||
async def agent_run(request: AgentRunRequest):
|
||
"""
|
||
Agent完整执行流程 - 使用 LangGraph Orchestrator
|
||
|
||
执行完整的多智能体流程:
|
||
Router → Intent → Planning → Execution → Validator → Discovery → Learning
|
||
|
||
用于自动化执行分析任务,支持循环迭代发现
|
||
"""
|
||
try:
|
||
if langgraph_orchestrator is None or not LANGGRAPH_AVAILABLE:
|
||
return {
|
||
"success": False,
|
||
"message": "LangGraph Orchestrator 未初始化"
|
||
}
|
||
|
||
from gfa_core.mas.core.state import GraphState
|
||
|
||
session_id = request.session_id or f"session_{int(time.time())}"
|
||
|
||
# 构建初始状态(使用LangGraphOrchestrator的create_initial_state)
|
||
initial_state = langgraph_orchestrator.create_initial_state(
|
||
session_id=session_id,
|
||
query=request.message,
|
||
job_id=request.job_id,
|
||
files=[],
|
||
prefix=request.intent.get("prefix", "") if request.intent else ""
|
||
)
|
||
|
||
# 添加额外的运行时字段
|
||
initial_state["status"] = "running"
|
||
initial_state["intent_result"] = request.intent or {} # 传入已有的 intent
|
||
initial_state["completed_stages"] = []
|
||
initial_state["iteration_count"] = 0
|
||
initial_state["max_iterations"] = 3
|
||
initial_state["discoveries"] = []
|
||
|
||
# 查询已完成的阶段
|
||
workspace_name = request.job_id if request.job_id else request.session_id
|
||
if workspace_name:
|
||
workspace_dir = WORKSPACE_ROOT / workspace_name / "final"
|
||
if workspace_dir.exists():
|
||
completed_stages = []
|
||
for manifest_file in workspace_dir.glob("*.manifest.json"):
|
||
try:
|
||
with open(manifest_file, 'r') as f:
|
||
manifest_data = json.load(f)
|
||
if manifest_data.get("status") in ["success", "partial_success"]:
|
||
stage = manifest_data.get("stage", "")
|
||
if stage:
|
||
completed_stages.append(stage)
|
||
except:
|
||
pass
|
||
initial_state["results"]["completed_stages"] = completed_stages
|
||
|
||
# 如果前端指定了要执行的阶段,放入 intent
|
||
if request.stages:
|
||
initial_state["results"]["intent"]["stages"] = request.stages
|
||
|
||
print(f"[Agent Run] Starting full orchestrator flow...")
|
||
print(f"[Agent Run] Query: {request.message}")
|
||
print(f"[Agent Run] Completed stages: {initial_state.get('completed_stages', [])}")
|
||
|
||
# 调用 LangGraph Orchestrator 执行完整流程
|
||
result_state = await langgraph_orchestrator.run(initial_state)
|
||
|
||
# 提取结果
|
||
final_output = result_state.get("final_output", {})
|
||
discoveries = result_state.get("discoveries", [])
|
||
iteration_count = result_state.get("iteration_count", 0)
|
||
|
||
print(f"[Agent Run] Flow completed. Iterations: {iteration_count}")
|
||
print(f"[Agent Run] Discoveries: {len(discoveries)}")
|
||
|
||
return {
|
||
"success": True,
|
||
"status": result_state.get("status", "completed"),
|
||
"iterations": iteration_count,
|
||
"discoveries": discoveries,
|
||
"final_output": final_output,
|
||
"session_id": session_id
|
||
}
|
||
|
||
except Exception as e:
|
||
import traceback
|
||
return {
|
||
"success": False,
|
||
"message": f"执行失败: {str(e)}",
|
||
"error_detail": traceback.format_exc()
|
||
}
|
||
|
||
|
||
# 从路径加载文件(创建软链接)
|
||
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=7869)
|