Files
2026-07-10 17:59:59 +08:00

1684 lines
58 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
# -*- coding: utf-8 -*-
"""
RAG搜索路由 - 支持新的索引格式
"""
import os
import sys
import pickle
import logging
from pathlib import Path
from typing import Optional, List, Dict, Any
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from src.rag.project_config import PROJECT_ROOT, conda_env_path
# 配置日志
logger = logging.getLogger(__name__)
# 添加项目路径
sys.path.insert(0, str(PROJECT_ROOT))
gf_site_packages = conda_env_path(os.environ.get("GF_PYTHON_ENV", "gfpy312")) / "lib/python3.12/site-packages"
if gf_site_packages.exists():
sys.path.insert(0, str(gf_site_packages))
from src.rag.base import SearchResult
from src.rag.vector_store import InMemoryStore
from src.rag.embeddings import BGEEmbedding
from src.rag.family_facts import ensure_family_fact
router = APIRouter()
# agentic模块已移除,使用纯RAG功能
# 初始化LLM客户端
_llm_manager = None
def get_llm_manager():
global _llm_manager
if _llm_manager is None:
try:
from src.llm.manager import LLMManager
import yaml
# 读取配置文件
config_path = PROJECT_ROOT / "config" / "llm.yaml"
if config_path.exists():
with open(config_path, 'r') as f:
config = yaml.safe_load(f)
_llm_manager = LLMManager(config)
provider = config.get("default", {}).get("provider", "unknown")
print(f"LLMManager初始化成功(使用{provider})")
else:
# 配置文件不存在,使用Ollama作为fallback
_llm_manager = LLMManager({
"default": {"provider": "ollama", "model": "qwen3:0.6b"}
})
print("LLMManager初始化成功(使用Ollama fallback)")
except Exception as e:
print(f"LLM客户端初始化失败: {e}")
import traceback
traceback.print_exc()
return _llm_manager
# 全局变量存储vector store
_vector_store = None
_embedding_model = None
def get_vector_store():
"""懒加载vector store"""
global _vector_store, _embedding_model
if _vector_store is None:
configured_index = os.environ.get("RAG_VECTOR_INDEX")
if configured_index:
index_path = Path(configured_index).expanduser()
if index_path.is_dir():
index_path = index_path / "vector_index.pkl"
else:
# 默认仍使用当前生产中的SPL索引;通用构建脚本可生成同格式索引。
index_path = PROJECT_ROOT / "data" / "rag_index_final_v15_spl" / "vector_index.pkl"
# 如果默认/配置索引不存在,回退到旧索引
if not index_path.exists():
index_path = PROJECT_ROOT / "data" / "rag_index_final_v15_1" / "vector_index.pkl"
if not index_path.exists():
raise HTTPException(status_code=500, detail="索引文件不存在,请先运行索引构建")
# 创建embedding model
_embedding_model = BGEEmbedding()
# 创建store
_vector_store = InMemoryStore(embedding_model=_embedding_model)
# 加载索引
with open(index_path, 'rb') as f:
data = pickle.load(f)
_vector_store._chunks = data['chunks']
# 将 embeddings 列表转换为字典(chunk_id -> embedding)
chunks_list = list(_vector_store._chunks.values())
embeddings_list = data['embeddings']
_vector_store._embeddings = {}
for i, chunk in enumerate(chunks_list):
if i < len(embeddings_list):
_vector_store._embeddings[chunk.chunk_id] = embeddings_list[i]
print(f"RAG索引加载完成: {len(_vector_store._chunks)} chunks, {len(_vector_store._embeddings)} embeddings")
return _vector_store
class RAGQueryRequest(BaseModel):
query: str
mode: str = "hybrid" # hybrid, normal, exact, alias, substring, bm25
top_k: int = 10
include_sources: bool = True
use_llm: bool = True # 是否使用LLM生成总结(默认启用)
strict_mode: bool = False # 严格模式:只返回文献原文,不生成总结(默认关闭)
class PhylogenyExtractionRequest(BaseModel):
"""系统发育提取请求"""
pdf_path: str # PDF文件路径
doi: Optional[str] = None # DOI(可选,用于获取补充材料)
pmcid: Optional[str] = None # PMCID(可选)
auto_fetch_supplementary: bool = True # 是否自动获取补充材料
class RAGSource(BaseModel):
doc_id: str
content: str
score: float
metadata: dict
pmid: Optional[str] = None # V15.1: 添加 PMID 字段
class KnowledgeGraphData(BaseModel):
nodes: List[dict] = []
edges: List[dict] = []
class KGEntityInfo(BaseModel):
"""知识图谱实体信息(用于前端可视化)"""
name: str
type: str # family, gene, species
display_name: str = "" # 用于展示的友好名称
class OmicsDataset(BaseModel):
"""多组学数据集信息"""
dataset_id: str
dataset_type: str # transcriptome, genome, etc.
species: str
source_paper: str
class RAGQueryResponse(BaseModel):
response: str
sources: List[RAGSource] = []
mode: str
total_chunks: int
knowledge_graph: Optional[KnowledgeGraphData] = None # 动态生成的知识图谱数据
kg_entities: List[KGEntityInfo] = [] # 可用于Neo4j可视化的实体列表
omics_datasets: Optional[List[OmicsDataset]] = None # 多组学数据集信息
class FamilyFactResponse(BaseModel):
family: str
source: str = ""
data: Dict[str, Any]
@router.post("/chat")
async def rag_chat(request: RAGQueryRequest):
"""RAG搜索端点"""
try:
logger.info(f"[RAG] 查询: '{request.query}' | 模式: {request.mode} | top_k: {request.top_k}")
store = get_vector_store()
# agentic模式已移除,使用hybrid
if request.mode == "agentic":
# 回退到hybrid search
results = await _hybrid_search(store, request.query, request.top_k)
elif request.mode == "exact":
results = await _exact_search(store, request.query, request.top_k)
elif request.mode == "substring":
results = await _substring_search(store, request.query, request.top_k)
elif request.mode == "bm25":
results = await _bm25_search(store, request.query, request.top_k)
else:
# hybrid search
results = await _hybrid_search(store, request.query, request.top_k)
logger.info(f"[RAG] 检索完成: {len(results)}条结果")
if results:
logger.info(f"[RAG] Top3: {[(r.chunk.doc_id, round(r.score,3)) for r in results[:3]]}")
# 格式化响应
sources = []
for result in results:
sources.append(RAGSource(
doc_id=result.chunk.doc_id,
content=result.chunk.content[:500],
score=result.score,
metadata=result.chunk.metadata,
pmid=result.chunk.metadata.get('pmid') # V15.1: 添加 PMID
))
# 生成回答(支持LLM总结或严格模式)
if request.strict_mode:
# 严格模式:只返回文献原文,不调用LLM
response_text = _generate_strict_response(request.query, sources)
logger.info(f"[RAG] 严格模式生成完成: {len(response_text)}字符")
else:
response_text = await _generate_response_async(request.query, sources, request.use_llm)
logger.info(f"[RAG] 生成完成: {len(response_text)}字符 | use_llm: {request.use_llm}")
# 从检索结果动态提取知识图谱
kg_data = _extract_knowledge_graph(sources, request.query)
# 提取可用于Neo4j可视化的实体
kg_entities = _extract_kg_entities(sources)
# 检测数据集查询意图,从知识图谱中查找多组学数据
omics_datasets = _query_omics_datasets(request.query)
if omics_datasets:
# 在回答中添加数据集信息
response_text = _add_omics_info_to_response(response_text, omics_datasets)
logger.info(f"[RAG] 找到 {len(omics_datasets)} 个多组学数据集")
return RAGQueryResponse(
response=response_text,
sources=sources if request.include_sources else [],
mode=request.mode,
total_chunks=len(store._chunks),
knowledge_graph=kg_data,
kg_entities=kg_entities,
omics_datasets=omics_datasets if omics_datasets else None
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"搜索失败: {str(e)}")
@router.get("/family/pfam")
async def family_pfam(family: str = "SPL"):
"""Return literature-backed Pfam IDs for a gene family."""
fact = ensure_family_fact(family)
pfam_items = fact.get("pfam") or []
pfam_ids = [item.get("pfam_id") for item in pfam_items if item.get("pfam_id")]
return {
"family": fact.get("family", family.upper()),
"pfam_ids": pfam_ids,
"pfams": pfam_items,
"confidence": "high" if any((item.get("support_count") or 0) >= 2 for item in pfam_items) else ("medium" if pfam_items else "none"),
"source": fact.get("source", ""),
"source_documents": fact.get("source_documents", []),
}
@router.get("/family/baseline")
async def family_baseline(family: str = "SPL"):
"""Return member-count baseline facts extracted from v15 literature outputs."""
fact = ensure_family_fact(family)
baseline = fact.get("baseline") or {}
return {
"family": fact.get("family", family.upper()),
"baseline": baseline,
"member_counts": fact.get("member_counts", []),
"source": fact.get("source", ""),
}
@router.get("/family/stage-context")
async def family_stage_context(family: str = "SPL", stage: str = "identify", limit: int = 12):
"""Return stage-specific methods/evidence context for analysis agents."""
fact = ensure_family_fact(family)
stage_key = (stage or "").strip().lower() or "identify"
contexts = (fact.get("stage_context") or {}).get(stage_key, [])
limit = max(1, min(int(limit or 12), 50))
return {
"family": fact.get("family", family.upper()),
"stage": stage_key,
"contexts": contexts[:limit],
"available_stages": sorted((fact.get("stage_context") or {}).keys()),
"source": fact.get("source", ""),
}
async def _exact_search(store, query: str, top_k: int):
"""精确匹配搜索"""
from src.rag.base import SearchResult
query_lower = query.lower().strip()
results = []
for chunk_id, chunk in store._chunks.items():
content = chunk.content.lower()
count = content.count(query_lower)
if count > 0:
score = 0.8 + 0.19 * min(count, 10) / 10
if content == query_lower:
score = 1.0
results.append(SearchResult(chunk=chunk, score=score))
results.sort(key=lambda x: x.score, reverse=True)
return results[:top_k]
def _expand_query(query_words: List[str]) -> List[str]:
"""查询扩展:添加中英文同义词,并拆分组合词"""
# 同义词映射
synonyms = {
# 中文 -> 英文
'转录组': ['transcriptome', 'rna-seq', 'rnaseq', 'rna seq'],
'转录': ['transcript', 'transcription'],
'基因组': ['genome', 'genomic'],
'蛋白': ['protein', 'peptide'],
'基因': ['gene', 'genetic'],
'表达': ['expression', 'express'],
'序列': ['sequence', 'seq'],
'数据': ['data', 'dataset'],
'编号': ['accession', 'id', 'number'],
'山核桃': ['pecan', 'carya', 'carya illinoinensis'],
'谷子': ['foxtail millet', 'setaria', 'setaria italica'],
'向日葵': ['sunflower', 'helianthus', 'helianthus annuus'],
'柑橘': ['citrus', 'orange', 'mandarin'],
# 英文 -> 相关词
'rna-seq': ['rnaseq', 'rna seq', 'transcriptome', '转录组'],
'transcriptome': ['rna-seq', 'rnaseq', '转录组'],
'pecan': ['carya', 'carya illinoinensis', '山核桃'],
}
# 先拆分组合词(如 "转录组数据" -> "转录组", "数据")
split_words = []
for word in query_words:
split_words.append(word)
# 尝试拆分中文组合词
for key in synonyms.keys():
if key in word and word != key:
split_words.append(key)
expanded = set(split_words)
for word in split_words:
word_lower = word.lower()
if word_lower in synonyms:
expanded.update(synonyms[word_lower])
return list(expanded)
async def _substring_search(store, query: str, top_k: int):
"""子串搜索(支持查询扩展)"""
import re
from src.rag.base import SearchResult
query_lower = query.lower().strip()
# 提取查询词
if ' ' in query_lower:
query_words = [w.strip() for w in query_lower.split() if len(w.strip()) >= 2]
else:
query_words = re.findall(r'[\u4e00-\u9fa5]{2,}|[a-zA-Z]+', query_lower)
if not query_words:
query_words = [query_lower] if len(query_lower) >= 2 else []
if not query_words:
query_words = [query_lower] if query_lower else []
# 查询扩展
original_words = query_words.copy()
query_words = _expand_query(query_words)
if set(original_words) != set(query_words):
logger.info(f"[RAG] 查询扩展: {original_words} -> {query_words}")
results = []
# 物种关键词(高权重)
species_keywords = ['pecan', 'carya', 'carya illinoinensis', '山核桃', '谷子', 'setaria', '向日葵', 'helianthus', '柑橘', 'citrus']
# 数据集关键词(高权重)
dataset_keywords = ['transcriptome', 'rna-seq', 'rnaseq', 'rna seq', '转录组', 'prjna', 'srp', 'gse', 'gsm', 'srr', 'srx']
for chunk_id, chunk in store._chunks.items():
content_lower = chunk.content.lower()
total_score = 0
matched_words = 0
has_species = False
has_dataset = False
for word in query_words:
if word in content_lower:
count = content_lower.count(word)
# 基础权重
word_weight = min(len(word) / 10, 1.0) + 0.5
# 物种关键词额外加权
if word in species_keywords:
word_weight *= 2.0
has_species = True
# 数据集关键词额外加权
if word in dataset_keywords:
word_weight *= 1.5
has_dataset = True
total_score += 0.1 * count * word_weight
matched_words += 1
if matched_words > 0:
coverage = matched_words / len(query_words) if query_words else 0
base_score = 0.5
# 如果同时匹配物种和数据集,额外加分
if has_species and has_dataset:
base_score += 0.2
coverage_bonus = 0.3 * coverage
density_score = min(total_score, 0.2)
score = base_score + coverage_bonus + density_score
results.append(SearchResult(chunk=chunk, score=min(score, 1.0)))
results.sort(key=lambda x: x.score, reverse=True)
return results[:top_k]
async def _bm25_search(store, query: str, top_k: int):
"""BM25搜索"""
from src.rag.base import SearchResult
try:
from rank_bm25 import BM25Okapi
except ImportError:
# 如果没有BM25,使用简单关键词匹配
return await _substring_search(store, query, top_k)
# 构建corpus
corpus = []
chunk_ids = []
for chunk_id, chunk in store._chunks.items():
corpus.append(chunk.content)
chunk_ids.append(chunk_id)
if not corpus:
return []
bm25 = BM25Okapi(corpus)
tokenized_query = query.lower().split()
scores = bm25.get_scores(tokenized_query)
# 获取top_k
top_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:top_k]
results = []
for idx in top_indices:
if scores[idx] > 0:
chunk_id = chunk_ids[idx]
chunk = store._chunks[chunk_id]
results.append(SearchResult(chunk=chunk, score=min(scores[idx] / 10, 1.0)))
return results
async def _hybrid_search(store, query: str, top_k: int, alpha: float = 0.7):
"""混合搜索"""
# 向量搜索
try:
query_embedding = await store.embedding_model.aembed(query)
vector_results = store.search(query_embedding, top_k * 2, threshold=0.0)
except:
vector_results = []
# BM25搜索
bm25_results = await _bm25_search(store, query, top_k * 2)
# 子串搜索
substring_results = await _substring_search(store, query, top_k * 2)
# 合并结果
combined = {}
for r in vector_results:
chunk_id = r.chunk.chunk_id
combined[chunk_id] = (alpha * r.score, r.chunk)
for r in bm25_results:
chunk_id = r.chunk.chunk_id
if chunk_id in combined:
old_score, chunk = combined[chunk_id]
combined[chunk_id] = (old_score + (1 - alpha) * r.score * 0.5, chunk)
else:
combined[chunk_id] = ((1 - alpha) * r.score * 0.5, r.chunk)
for r in substring_results:
chunk_id = r.chunk.chunk_id
if chunk_id in combined:
old_score, chunk = combined[chunk_id]
combined[chunk_id] = (old_score + (1 - alpha) * r.score * 0.3, chunk)
else:
combined[chunk_id] = ((1 - alpha) * r.score * 0.3, r.chunk)
sorted_results = sorted(combined.items(), key=lambda x: x[1][0], reverse=True)
from src.rag.base import SearchResult
return [SearchResult(chunk=chunk, score=score) for _, (score, chunk) in sorted_results[:top_k]]
async def _generate_response_async(query: str, sources: List[RAGSource], use_llm: bool = False) -> str:
"""生成回答 - 支持LLM总结(异步版本)
优化:对于"有哪些物种"类问题,直接查询物种API返回完整列表
"""
import re
# 检测是否是"有哪些物种"类查询
is_species_list_query = any(re.search(p, query.lower()) for p in [
r"有哪些物种", r"哪些物种", r"什么物种", r"哪些.*做过.*分析", r"哪些.*研究过", r".*物种.*列表"
])
# 如果是物种列表查询,直接返回物种API的结果
if is_species_list_query:
gene_family = _extract_gene_family(query)
species_list_data = await _fetch_species_list(gene_family)
if species_list_data and species_list_data.get("species"):
return _format_species_list_answer(species_list_data, query)
if not sources:
return "未找到相关文献。"
# Pfam/domain accession questions need a deterministic answer. When the
# LLM is unavailable, falling through to the generic literature-count
# summary hides the actual accession users asked for.
pfam_answer = _try_generate_pfam_answer(query, sources)
if pfam_answer:
return pfam_answer
# 如果使用LLM生成总结
if use_llm:
llm_mgr = get_llm_manager()
if llm_mgr:
# 构造上下文
context = "\n\n".join([
f"【文献{i+1}】{s.content[:500]}"
for i, s in enumerate(sources[:5])
])
prompt = f"""你是一个基因家族分析专家。请**严格**根据以下检索到的文献内容回答用户问题。
【重要规则】
1. **只使用文献中明确提到的信息**,禁止补充任何训练知识
2. 如果文献中没有相关信息,必须明确说明:"根据提供的文献,未找到相关信息"
3. 禁止推测、扩展或解释文献中未提及的内容
4. 每个关键事实必须标注来源(如【文献1】)
用户问题: {query}
文献内容:
{context}
请生成回答:
- 只总结文献中明确提到的内容
- 不添加背景知识、不解释概念、不做推测
- 如果文献内容不完整,如实说明"文献信息有限"
- 使用 Markdown 格式,关键信息加粗
如果文献内容与问题不相关,请直接说明:"提供的文献内容与问题不相关,无法回答"。
"""
try:
from src.llm.base import LLMMessage
messages = [LLMMessage(role="user", content=prompt)]
# 使用异步调用
response = await llm_mgr.acall(messages)
return response.content if hasattr(response, 'content') else str(response)
except Exception as e:
print(f"LLM生成失败: {e}")
import traceback
traceback.print_exc()
# 默认统计信息
species_list = set()
gf_list = set()
for s in sources:
if 'species' in s.metadata:
species_list.add(s.metadata['species'])
if 'gene_family' in s.metadata:
gf_list.add(s.metadata['gene_family'])
response = f"找到 {len(sources)} 条相关文献。"
if species_list:
response += f"\n涉及物种: {', '.join(species_list)}。"
if gf_list:
response += f"\n涉及基因家族: {', '.join(gf_list)}。"
return response
def _try_generate_pfam_answer(query: str, sources: List[RAGSource]) -> str:
"""Answer Pfam accession questions from retrieved evidence when possible."""
import re
query_lower = query.lower()
is_pfam_query = any(token in query_lower for token in ["pfam", "pfam号", "pfam编号", "pfam id", "结构域编号"])
if not is_pfam_query:
return ""
gene_family = _extract_gene_family(query).upper()
try:
fact = ensure_family_fact(gene_family)
pfam_items = fact.get("pfam") or []
if pfam_items:
pfam_ids = [item.get("pfam_id") for item in pfam_items if item.get("pfam_id")]
lines = [
f"{gene_family} 家族的 Pfam 号是:**{', '.join(pfam_ids)}**。",
"",
"证据:",
]
for item in pfam_items:
pfam_id = item.get("pfam_id", "")
for ev in (item.get("evidence") or [])[:3]:
source_label = ev.get("paper_id") or ev.get("source_path") or "family_fact"
if ev.get("pmid"):
source_label += f", PMID: {ev['pmid']}"
lines.append(f"- {pfam_id}: {ev.get('evidence', '')}(来源: {source_label})")
return "\n".join(lines)
except Exception as exc:
logger.warning(f"[RAG] family fact Pfam lookup failed: {exc}")
evidence_by_pfam: dict[str, dict] = {}
pfam_pattern = re.compile(r"\bPF\d{5}\b", re.I)
for source in sources:
text = source.content or ""
metadata_text = " ".join(str(v) for v in (source.metadata or {}).values())
combined = f"{text} {metadata_text}"
for match in pfam_pattern.finditer(combined):
pfam_id = match.group(0).upper()
if pfam_id in evidence_by_pfam:
continue
evidence = _extract_pfam_evidence_sentence(text, pfam_id)
evidence_by_pfam[pfam_id] = {
"evidence": evidence or text[:220],
"doc_id": source.doc_id,
"pmid": source.pmid or (source.metadata or {}).get("pmid", ""),
}
if not evidence_by_pfam:
return "根据检索到的文献片段,未找到明确的 Pfam 编号。"
pfam_ids = sorted(evidence_by_pfam)
lines = [
f"{gene_family} 家族的 Pfam 号是:**{', '.join(pfam_ids)}**。",
"",
"证据:",
]
for pfam_id in pfam_ids:
item = evidence_by_pfam[pfam_id]
source_label = item["doc_id"]
if item.get("pmid"):
source_label += f", PMID: {item['pmid']}"
lines.append(f"- {pfam_id}: {item['evidence']}(来源: {source_label})")
return "\n".join(lines)
def _extract_pfam_evidence_sentence(text: str, pfam_id: str) -> str:
"""Return a compact sentence-like evidence snippet containing a Pfam ID."""
import re
if not text:
return ""
normalized = re.sub(r"\s+", " ", text).strip()
sentences = re.split(r"(?<=[。!?.!?])\s+", normalized)
for sentence in sentences:
if pfam_id.upper() in sentence.upper():
return sentence[:260]
idx = normalized.upper().find(pfam_id.upper())
if idx < 0:
return ""
start = max(0, idx - 110)
end = min(len(normalized), idx + 150)
return normalized[start:end].strip()
def _generate_strict_response(query: str, sources: List[RAGSource]) -> str:
"""Strict mode: only return original document chunks without summarization.
Features:
1. Only display retrieved document chunks
2. No summarization, inference, or knowledge supplementation
3. Clearly label source of each chunk
4. Honestly report if content is irrelevant
"""
import re
if not sources:
return "【严格模式】未检索到相关文献片段。"
lines = [
f"【严格模式 - 仅基于检索到的文献片段】",
f"查询: {query}",
f"检索到 {len(sources)} 条相关片段",
"",
"=" * 70,
""
]
# 检查相关性
query_keywords = set(re.findall(r'[\u4e00-\u9fa5]{2,}|[a-zA-Z]{3,}', query.lower()))
relevant_count = 0
for i, source in enumerate(sources, 1):
content_lower = source.content.lower()
matched_keywords = [k for k in query_keywords if k in content_lower]
relevance = len(matched_keywords) / len(query_keywords) if query_keywords else 0
if relevance > 0.3:
relevant_count += 1
lines.extend([
f"【文献片段 {i}】",
f"来源ID: {source.doc_id}",
f"相关度: {source.score:.3f}",
f"匹配关键词: {', '.join(matched_keywords) if matched_keywords else '无直接匹配'}",
f"内容类型: {source.metadata.get('type', 'unknown')}",
"-" * 50,
source.content,
"",
"=" * 70,
""
])
# 添加相关性评估
lines.extend([
"",
"【相关性评估】",
f"- 总检索片段: {len(sources)}",
f"- 高度相关片段: {relevant_count}",
f"- 相关度比例: {relevant_count/len(sources)*100:.1f}%" if sources else "N/A",
])
if relevant_count == 0:
lines.extend([
"",
"⚠️ 警告:检索到的片段与查询问题相关性较低,",
" 可能无法准确回答问题。"
])
return "\n".join(lines)
def _extract_gene_family(query: str) -> str:
"""从查询中提取基因家族名称"""
import re
gf_patterns = [
r"(SPL|NAC|WRKY|MYB|bHLH|AP2|ERF|bZIP|Dof|SBP)[^a-zA-Z]",
r"(\w+).*基因家族",
r"(\w+).*gene family",
]
query_upper = query.upper()
for pattern in gf_patterns:
match = re.search(pattern, query_upper)
if match:
return match.group(1)
return "SPL"
async def _fetch_species_list(gene_family: str) -> dict:
"""从物种索引API获取物种列表"""
try:
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(
f"http://localhost:8000/api/v1/species/list?gene_family={gene_family}"
) as response:
if response.status == 200:
return await response.json()
except Exception as e:
logger.warning(f"[RAG] 获取物种列表失败: {e}")
return None
def _format_species_list_answer(species_data: dict, query: str) -> str:
"""格式化物种列表回答"""
if not species_data or "species" not in species_data:
return "未找到相关物种信息。"
species_list = species_data["species"]
gene_family = species_data.get("gene_family", "SPL")
total_species = species_data.get("total_species", len(species_list))
lines = [
f"## {gene_family}基因家族已分析的物种(共{total_species}种)",
"",
"根据基因家族-物种索引,已对以下物种开展全基因组鉴定与分析:",
"",
"| 物种 | 学名 | 比较物种 | 关系数 | 实体数 |",
"|:---|:---|:---|:---:|:---:|"
]
for sp in species_list:
name = sp.get('common_name_cn', 'N/A')
scientific = sp.get('scientific_name', 'N/A')
# 显示完整的比较物种列表
comparison = ', '.join(sp.get('comparison_species', []))
relations = sp.get('relations_count', 0)
entities = sp.get('entities_count', 0)
lines.append(f"| {name} | {scientific} | {comparison} | {relations} | {entities} |")
lines.extend([
"",
"### 说明",
"",
f"- **{gene_family}基因家族**:SQUAMOSA启动子结合蛋白样基因家族",
"- **研究覆盖**:木本经济作物、油料作物、蔬菜作物、药用植物及模式植物",
"- **比较分析**:所有研究均以拟南芥和水稻作为核心参照物种",
"",
"如需了解某个物种的详细分析结果,请直接提问,如'苦荞SPL基因家族功能'。"
])
return '\n'.join(lines)
@router.get("/rag/status")
async def rag_status():
"""RAG系统状态"""
try:
store = get_vector_store()
return {
"status": "ready",
"total_chunks": len(store._chunks),
"total_documents": len(set(c.doc_id for c in store._chunks.values())),
"agent_enabled": True
}
except Exception as e:
return {
"status": "error",
"error": str(e)
}
# ===== 用户反馈API =====
from datetime import datetime
import json
import sqlite3
# 反馈数据库路径
FEEDBACK_DB = PROJECT_ROOT / "data" / "feedback.db"
def init_feedback_db():
"""初始化反馈数据库"""
FEEDBACK_DB.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(FEEDBACK_DB)
conn.execute("""
CREATE TABLE IF NOT EXISTS feedback (
id INTEGER PRIMARY KEY AUTOINCREMENT,
query TEXT NOT NULL,
answer TEXT,
rating INTEGER, -- 1-5 评分
feedback_type TEXT, -- thumbs_up/thumbs_down/correction
feedback_text TEXT, -- 用户反馈内容
session_id TEXT,
timestamp TEXT NOT NULL
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS evaluation_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
question_id TEXT,
query TEXT NOT NULL,
query_type TEXT,
ndcg REAL,
answer_quality REAL,
timestamp TEXT NOT NULL
)
""")
conn.commit()
conn.close()
class FeedbackRequest(BaseModel):
query: str
answer: str = None
rating: int = None # 1-5
feedback_type: str = None # thumbs_up/thumbs_down/correction
feedback_text: str = None
session_id: str = None
class EvaluationRecordRequest(BaseModel):
question_id: str
query: str
query_type: str
ndcg: float
answer_quality: float
@router.post("/rag/feedback")
async def submit_feedback(request: FeedbackRequest):
"""提交用户反馈"""
try:
init_feedback_db()
conn = sqlite3.connect(FEEDBACK_DB)
cursor = conn.cursor()
cursor.execute("""
INSERT INTO feedback (query, answer, rating, feedback_type, feedback_text, session_id, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (
request.query,
request.answer,
request.rating,
request.feedback_type,
request.feedback_text,
request.session_id,
datetime.now().isoformat()
))
conn.commit()
conn.close()
return {"status": "success", "message": "反馈已保存"}
except Exception as e:
raise HTTPException(status_code=500, detail=f"保存反馈失败: {str(e)}")
@router.post("/rag/evaluation/record")
async def record_evaluation(request: EvaluationRecordRequest):
"""记录评估结果"""
try:
init_feedback_db()
conn = sqlite3.connect(FEEDBACK_DB)
cursor = conn.cursor()
cursor.execute("""
INSERT INTO evaluation_history (question_id, query, query_type, ndcg, answer_quality, timestamp)
VALUES (?, ?, ?, ?, ?, ?)
""", (
request.question_id,
request.query,
request.query_type,
request.ndcg,
request.answer_quality,
datetime.now().isoformat()
))
conn.commit()
conn.close()
return {"status": "success", "message": "评估记录已保存"}
except Exception as e:
raise HTTPException(status_code=500, detail=f"保存评估记录失败: {str(e)}")
@router.get("/rag/feedback/stats")
async def get_feedback_stats():
"""获取反馈统计"""
try:
init_feedback_db()
conn = sqlite3.connect(FEEDBACK_DB)
cursor = conn.cursor()
# 总反馈数
cursor.execute("SELECT COUNT(*) FROM feedback")
total = cursor.fetchone()[0]
# 评分分布
cursor.execute("""
SELECT rating, COUNT(*)
FROM feedback
WHERE rating IS NOT NULL
GROUP BY rating
""")
rating_dist = {row[0]: row[1] for row in cursor.fetchall()}
# 反馈类型分布
cursor.execute("""
SELECT feedback_type, COUNT(*)
FROM feedback
WHERE feedback_type IS NOT NULL
GROUP BY feedback_type
""")
feedback_type_dist = {row[0]: row[1] for row in cursor.fetchall()}
# 平均评分
cursor.execute("SELECT AVG(rating) FROM feedback WHERE rating IS NOT NULL")
avg_rating = cursor.fetchone()[0] or 0
conn.close()
return {
"total_feedback": total,
"rating_distribution": rating_dist,
"feedback_type_distribution": feedback_type_dist,
"average_rating": round(avg_rating, 2)
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取统计失败: {str(e)}")
@router.get("/rag/evaluation/history")
async def get_evaluation_history(limit: int = 50):
"""获取评估历史"""
try:
init_feedback_db()
conn = sqlite3.connect(FEEDBACK_DB)
cursor = conn.cursor()
cursor.execute("""
SELECT question_id, query, query_type, ndcg, answer_quality, timestamp
FROM evaluation_history
ORDER BY timestamp DESC
LIMIT ?
""", (limit,))
results = []
for row in cursor.fetchall():
results.append({
"question_id": row[0],
"query": row[1],
"query_type": row[2],
"ndcg": row[3],
"answer_quality": row[4],
"timestamp": row[5]
})
conn.close()
return {"history": results, "count": len(results)}
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取历史失败: {str(e)}")
# ===== 高级RAG功能 =====
from src.rag.advanced import (
MultiGranularChunker,
HierarchicalRetriever,
AdaptiveRetriever,
RetrievalConfig,
QueryDecomposer,
ContextCompressor
)
# 高级组件实例
_advanced_chunker = None
_advanced_retriever = None
_query_decomposer = None
_context_compressor = None
def get_advanced_components():
"""获取高级组件"""
global _advanced_chunker, _advanced_retriever, _query_decomposer, _context_compressor
if _advanced_chunker is None:
_advanced_chunker = MultiGranularChunker()
_query_decomposer = QueryDecomposer()
_context_compressor = ContextCompressor()
# retriever需要vector store,暂不初始化
_advanced_retriever = None
return {
"chunker": _advanced_chunker,
"retriever": _advanced_retriever,
"decomposer": _query_decomposer,
"compressor": _context_compressor
}
class AdvancedSearchRequest(BaseModel):
query: str
top_k: int = 10
use_hierarchical: bool = False # 使用分层检索
use_query_decompose: bool = False # 使用查询分解
use_compression: bool = True # 使用上下文压缩
query_type: str = "general" # 查询类型
# Self-RAG + MultiHopRAG 参数
use_agentic: bool = False # 使用Agent编排
enable_self_rag: bool = False # 启用Self-RAG反思
enable_multi_hop: bool = False # 启用MultiHopRAG多跳检索
max_iterations: int = 3 # Self-RAG最大迭代次数
class QueryDecomposeRequest(BaseModel):
query: str
use_llm: bool = False
@router.post("/rag/advanced/search")
async def advanced_search(request: AdvancedSearchRequest):
"""高级检索接口 - 支持Self-RAG和MultiHopRAG"""
try:
comps = get_advanced_components()
results = {
"query": request.query,
"features": {
"hierarchical": request.use_hierarchical,
"query_decompose": request.use_query_decompose,
"compression": request.use_compression,
"agentic": request.use_agentic,
"self_rag": request.enable_self_rag,
"multi_hop": request.enable_multi_hop
}
}
# 1. 多跳推理模式 (直接使用MultiHopRAG)
if request.enable_multi_hop:
# 直接调用multi_hop_rag
try:
from src.rag.multi_hop_rag import MultiHopRAG
from src.rag.hybrid_retriever import HybridRetriever, SearchConfig
store = get_vector_store()
llm_mgr = get_llm_manager()
# 创建HybridRetriever包装store(MultiHopRAG需要正确接口)
config = SearchConfig(top_k=5, enable_query_rewrite=True)
hybrid_retriever = HybridRetriever(
vector_store=store,
config=config,
embedding_model=_embedding_model
)
# 创建MultiHopRAG实例
multi_hop = MultiHopRAG(
retriever=hybrid_retriever, # 使用HybridRetriever
llm_client=llm_mgr,
max_hops=request.max_iterations,
enable_web_search=False
)
# 执行多跳推理
hop_result = await multi_hop.query(request.query)
results["multi_hop_result"] = {
"final_answer": hop_result.final_answer,
"total_hops": hop_result.total_hops,
"sources": hop_result.sources[:5]
}
return results
except Exception as e:
results["error"] = f"MultiHopRAG failed: {str(e)}"
# 回退到普通检索
# 2. Self-RAG模式(暂不支持,agentic已移除)
if request.enable_self_rag or request.use_agentic:
results["message"] = "Self-RAG/Agentic模式已移除,请使用enable_multi_hop"
return results
# 2. 查询分解
if request.use_query_decompose:
sub_queries = comps["decomposer"].decompose(request.query)
results["sub_queries"] = [
{
"query": sq.query,
"reason": sq.reason,
"expected_docs": sq.expected_docs
}
for sq in sub_queries
]
# 3. 分层检索(需要实际索引)
if request.use_hierarchical:
results["message"] = "分层检索需要先构建多粒度索引"
# 4. 上下文压缩
if request.use_compression:
results["compression"] = "已启用上下文压缩"
# 5. 普通检索结果:保持与 gfa_core.mas.rag.RAGRetriever.search 的契约一致。
store = get_vector_store()
search_results = await _hybrid_search(store, request.query, max(1, min(request.top_k, 50)))
results["results"] = [
{
"doc_id": item.chunk.doc_id,
"content": item.chunk.content[:1000],
"score": item.score,
"metadata": item.chunk.metadata,
"pmid": item.chunk.metadata.get("pmid"),
}
for item in search_results
]
results["count"] = len(results["results"])
return results
except Exception as e:
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"高级检索失败: {str(e)}")
@router.post("/rag/advanced/decompose")
async def decompose_query(request: QueryDecomposeRequest):
"""查询分解接口"""
try:
comps = get_advanced_components()
if request.use_llm:
sub_queries = await comps["decomposer"].decompose_with_llm(request.query)
else:
sub_queries = comps["decomposer"].decompose(request.query)
return {
"original_query": request.query,
"sub_queries": [
{
"query": sq.query,
"reason": sq.reason,
"expected_docs": sq.expected_docs,
"priority": sq.priority
}
for sq in sub_queries
],
"count": len(sub_queries)
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"查询分解失败: {str(e)}")
def _extract_knowledge_graph(sources: List[RAGSource], query: str) -> KnowledgeGraphData:
"""
从RAG检索结果动态提取知识图谱
提取基因名、物种、表型、过程等实体和关系
"""
import re
nodes = []
edges = []
node_ids = set()
# 基因名模式 (支持任意物种)
gene_pattern = r'\b([A-Z][a-z]?[A-Z]{1,4}\d{1,4}[a-z]?)\b'
# 数据集 ID 模式 (V15.1 新增)
dataset_pattern = r'\b(PRJNA\d+|SRP\d+|GSE\d+|GSM\d+|SRX\d+|SRR\d+)\b'
# 物种模式
species_pattern = r'\b(Arabidopsis thaliana|Oryza sativa|Zea mays|Glycine max|Solanum lycopersicum|Malus domestica|Carya illinoinensis|pecan|Arabidopsis|rice|maize|soybean|tomato|apple)\b'
# 过程/功能关键词
process_keywords = ['development', 'growth', 'regulation', 'signaling', 'biosynthesis',
'response', 'metabolism', 'expression', 'flowering', 'fruit development']
# 组织/器官关键词
tissue_keywords = ['root', 'shoot', 'leaf', 'flower', 'fruit', 'seed', 'embryo',
'endosperm', 'pollen', 'ovule', 'meristem', 'bud']
# 处理每个source
for source in sources:
content = source.content
doc_id = source.doc_id
# 提取基因
genes = re.findall(gene_pattern, content)
for gene in genes:
if gene not in node_ids:
nodes.append({
"id": gene,
"type": "gene"
})
node_ids.add(gene)
# 提取数据集 (V15.1 新增)
datasets = re.findall(dataset_pattern, content)
for ds_id in datasets:
if ds_id not in node_ids:
# 判断数据集类型
if ds_id.startswith('GSE') or ds_id.startswith('GSM'):
ds_type = 'geo_dataset'
else:
ds_type = 'sra_dataset'
nodes.append({
"id": ds_id,
"type": ds_type
})
node_ids.add(ds_id)
# 提取物种
species_list = re.findall(species_pattern, content, re.IGNORECASE)
for sp in species_list:
sp_key = sp.lower()
if sp_key not in node_ids:
nodes.append({
"id": sp,
"type": "species"
})
node_ids.add(sp_key)
# 提取过程
for proc in process_keywords:
if proc.lower() in content.lower():
proc_key = f"{proc}_{doc_id[:8]}"
if proc_key not in node_ids:
nodes.append({
"id": proc,
"type": "process"
})
node_ids.add(proc_key)
# 提取组织
for tissue in tissue_keywords:
if tissue.lower() in content.lower():
tissue_key = f"{tissue}_{doc_id[:8]}"
if tissue_key not in node_ids:
nodes.append({
"id": tissue,
"type": "tissue"
})
node_ids.add(tissue_key)
# 构建关系:基因-基因 (共现)
if len(genes) >= 2:
for i, g1 in enumerate(genes[:5]): # 限制避免过多边
for g2 in genes[i+1:6]:
edges.append({
"source": g1,
"target": g2,
"relation": "co_occurrence"
})
# 构建关系:基因-过程
for gene in genes[:3]:
for proc in process_keywords:
if proc.lower() in content.lower():
edges.append({
"source": gene,
"target": proc,
"relation": "involved_in"
})
# 构建关系:基因-组织 (表达)
for gene in genes[:3]:
for tissue in tissue_keywords:
if tissue.lower() in content.lower():
edges.append({
"source": gene,
"target": tissue,
"relation": "expressed_in"
})
# 去重边
unique_edges = []
edge_keys = set()
for edge in edges:
key = f"{edge['source']}->{edge['target']}"
if key not in edge_keys:
unique_edges.append(edge)
edge_keys.add(key)
# V15.1: 为检测到的数据集节点添加文章连接
try:
kg_path = PROJECT_ROOT / "frontend" / "knowledge_graph_with_omics.json"
if kg_path.exists():
with open(kg_path, 'r', encoding='utf-8') as f:
full_kg = json.load(f)
# 构建数据集到文章连接的映射
dataset_connections = {}
for edge in full_kg.get('edges', []):
if edge.get('relation') in ['from_article', 'from_paper', 'has_data_type', 'from_species']:
source = edge.get('source', '')
if 'PRJNA' in source or 'SRP' in source or 'GSE' in source:
if source not in dataset_connections:
dataset_connections[source] = []
dataset_connections[source].append(edge)
# 为当前知识图谱中的数据集节点添加连接
dataset_ids = {n['id'] for n in nodes if 'dataset' in n.get('type', '')}
for ds_id in dataset_ids:
if ds_id in dataset_connections:
for conn in dataset_connections[ds_id]:
target = conn.get('target', '')
target_type = conn.get('target_type', 'unknown')
# 添加目标节点(如果不存在)
if target and target not in node_ids:
nodes.append({
"id": target,
"type": target_type
})
node_ids.add(target)
# 添加连接边
edge_key = f"{ds_id}->{target}"
if edge_key not in edge_keys:
unique_edges.append({
"source": ds_id,
"target": target,
"relation": conn.get('relation', 'connected_to')
})
edge_keys.add(edge_key)
except Exception as e:
logger.warning(f"添加数据集文章连接失败: {e}")
return KnowledgeGraphData(nodes=nodes, edges=unique_edges)
def _query_omics_datasets(query: str) -> List[OmicsDataset]:
"""
检测用户查询意图,从知识图谱中查找多组学数据集
根据查询中的物种关键词过滤相关数据集
"""
import re
import json
# 数据集查询关键词(中英文)
omics_keywords = [
'转录组', 'rna-seq', 'rnaseq', 'rna seq', 'transcriptome',
'sra', 'geo', '数据集', 'dataset', '编号', 'accession',
'prjna', 'gse', 'gsm', 'srp', 'srr', 'srx'
]
# 检查查询是否包含数据集相关关键词
query_lower = query.lower()
is_omics_query = any(kw in query_lower for kw in omics_keywords)
if not is_omics_query:
return []
# 从查询中提取物种关键词
species_keywords = {
'pecan': 'Carya illinoinensis',
'山核桃': 'Carya illinoinensis',
'carya': 'Carya illinoinensis',
'谷子': 'Setaria italica',
'foxtail millet': 'Setaria italica',
'setaria': 'Setaria italica',
'向日葵': 'Helianthus annuus',
'sunflower': 'Helianthus annuus',
'柑橘': 'Citrus',
'citrus': 'Citrus',
}
# 确定查询的物种
target_species = None
for keyword, species in species_keywords.items():
if keyword in query_lower:
target_species = species
break
# 从知识图谱文件中查找数据集
try:
kg_path = PROJECT_ROOT / "frontend" / "knowledge_graph_with_omics.json"
if not kg_path.exists():
kg_path = PROJECT_ROOT / "frontend" / "knowledge_graph.json"
with open(kg_path, 'r', encoding='utf-8') as f:
kg_data = json.load(f)
datasets = []
for node in kg_data.get('nodes', []):
node_type = node.get('type', '')
node_id = node.get('id', '')
# 查找 sra_dataset 类型的节点
if node_type == 'sra_dataset':
# 推断物种(从节点ID或名称)
species = 'Unknown'
if 'PRJNA' in node_id:
species = 'Carya illinoinensis' # PRJNA435846 是 pecan
elif any(x in node_id for x in ['SRP028', 'SRP051']):
species = 'Setaria italica' # 谷子
elif 'GSE221055' in node_id:
species = 'Helianthus annuus' # 向日葵
elif any(x in node_id for x in ['SRP162', 'SRP355', 'SRP357']):
species = 'Citrus' # 柑橘
# 如果指定了目标物种,只返回匹配的数据集
if target_species and species != target_species:
continue
datasets.append(OmicsDataset(
dataset_id=node_id,
dataset_type='transcriptome',
species=species,
source_paper='V15 Knowledge Graph'
))
return datasets
except Exception as e:
logger.warning(f"查询多组学数据集失败: {e}")
return []
def _add_omics_info_to_response(response: str, datasets: List[OmicsDataset]) -> str:
"""
在回答中添加多组学数据集信息
"""
if not datasets:
return response
# 构建数据集信息文本
omics_info = "\n\n📊 **多组学数据集信息**:\n\n"
omics_info += "| 数据集编号 | 类型 | 物种 |\n"
omics_info += "|-----------|------|------|\n"
for ds in datasets:
omics_info += f"| {ds.dataset_id} | {ds.dataset_type} | {ds.species} |\n"
omics_info += "\n💡 您可以在 NCBI SRA/GEO 数据库中使用这些编号检索原始数据。"
# 在回答末尾添加(在参考文献之前)
if "📚 参考文献" in response:
response = response.replace("📚 参考文献", omics_info + "\n📚 参考文献")
else:
response += omics_info
return response
def _extract_kg_entities(sources: List[RAGSource]) -> List[KGEntityInfo]:
"""
从检索结果中提取可用于Neo4j可视化的实体
优先从metadata中提取gene_family和species
"""
entities = []
seen = set()
for source in sources:
metadata = source.metadata
# 提取基因家族
if 'gene_family' in metadata:
family = metadata['gene_family']
if family and family not in seen:
entities.append(KGEntityInfo(
name=family,
type='family',
display_name=family
))
seen.add(family)
# 提取物种
if 'species' in metadata:
species = metadata['species']
if species and species not in seen:
entities.append(KGEntityInfo(
name=species,
type='species',
display_name=species
))
seen.add(species)
# 如果metadata中没有,尝试从内容中提取(简化版)
if not entities:
import re
for source in sources[:3]: # 只看前3个source
content = source.content
# 尝试匹配基因家族名(大写字母组合)
family_match = re.search(r'\b([A-Z]{2,10})\s+gene family', content, re.IGNORECASE)
if family_match:
family = family_match.group(1).upper()
if family not in seen:
entities.append(KGEntityInfo(
name=family,
type='family',
display_name=family
))
seen.add(family)
break
return entities
@router.get("/rag/advanced/status")
async def advanced_status():
"""高级功能状态"""
comps = get_advanced_components()
return {
"status": "ready",
"features": {
"multi_granular_chunking": True,
"hierarchical_retrieval": comps["retriever"] is not None,
"query_decomposition": True,
"context_compression": True,
"citation_extraction": True,
"phylogeny_extraction": True # v5新增
},
"chunk_levels": ["summary", "paragraph", "sentence", "keyword"]
}
# ========== v5 系统发育提取接口 ==========
@router.post("/rag/phylogeny/extract")
async def extract_phylogeny(request: PhylogenyExtractionRequest):
"""
系统发育信息提取接口 (v5.2 - TAIR关键词库增强版)
自动从PDF提取系统发育分组信息,集成TAIR权威基因家族数据,自动消歧
包含4轮审查流程和关键词匹配详情
"""
try:
# 导入v5.2提取器 (TAIR关键词库增强版)
sys.path.insert(0, str(PROJECT_ROOT))
from extract_phylogeny_v5_universal_v3 import extract_phylogeny_v52
print(f"[Phylogeny Extract v5.2] 开始提取: {request.pdf_path}")
print(f"[Phylogeny Extract v5.2] DOI: {request.doi}")
print(f"[Phylogeny Extract v5.2] PMCID: {request.pmcid}")
# 读取PDF文本
import fitz
doc = fitz.open(request.pdf_path)
text = ""
for page in doc:
text += page.get_text()
doc.close()
# 执行提取(v5.2包含TAIR关键词库)
result = extract_phylogeny_v52(text)
# 返回结果(包含审查详情和TAIR关键词匹配)
ext = result.get("extraction", {})
return {
"status": "success",
"pdf_name": Path(request.pdf_path).name,
"gene_family": ext.get("gene_family"),
"species": ext.get("species"),
"total_genes": ext.get("total_genes"),
"total_groups": ext.get("total_groups"),
"confidence_score": result.get("confidence"),
"confidence_level": result.get("assessment", {}).get("level"),
"source": "TAIR+v5.2",
# v5.2新增:TAIR关键词匹配详情
"matched_keywords": ext.get("matched_keywords", {}),
# v5.2新增:审查详情
"scrutiny": result.get("scrutiny", {}),
"validation": result.get("validation", {}),
"assessment": result.get("assessment", {}),
"version": result.get("version", "v5.2-TAIR")
}
except Exception as e:
import traceback
print(f"[Phylogeny Extract v5.1] 错误: {e}")
print(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"提取失败: {str(e)}")
@router.get("/rag/phylogeny/status/{pdf_name}")
async def get_phylogeny_status(pdf_name: str):
"""
查询系统发育提取状态
"""
try:
result_path = PROJECT_ROOT / "evaluation" / "auto_results" / f"{pdf_name}_v5_report.json"
if not result_path.exists():
return {"status": "not_found", "message": "提取结果不存在"}
with open(result_path) as f:
result = json.load(f)
return {
"status": "success",
"pdf_name": pdf_name,
"confidence_score": result.get("confidence_score"),
"confidence_level": result.get("confidence_level"),
"total_groups": result.get("total_groups"),
"has_supplementary": result.get("has_supplementary")
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"查询失败: {str(e)}")