1100 lines
36 KiB
Python
Executable File
1100 lines
36 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
RAG搜索路由 - 支持新的索引格式
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import pickle
|
||
from pathlib import Path
|
||
from typing import Optional, List
|
||
from fastapi import APIRouter, HTTPException
|
||
from pydantic import BaseModel
|
||
|
||
# 添加项目路径
|
||
PROJECT_ROOT = Path(os.environ.get("PLANTAI_PROJECT_ROOT", "") or Path(__file__).resolve().parents[1])
|
||
sys.path.insert(0, str(PROJECT_ROOT))
|
||
extra_site_packages = os.environ.get("GF_PYTHON_SITE_PACKAGES", "").strip()
|
||
if extra_site_packages:
|
||
sys.path.insert(0, extra_site_packages)
|
||
|
||
from src.rag.base import SearchResult
|
||
from src.rag.vector_store import InMemoryStore
|
||
from src.rag.embeddings import BGEEmbedding
|
||
from src.rag.agentic.query_analyzer import QueryAnalyzer, QueryType
|
||
from src.rag.agentic.retrieval_strategy import RetrievalStrategyAgent, RetrievalMode
|
||
|
||
router = APIRouter()
|
||
|
||
# 初始化Agent
|
||
query_analyzer = QueryAnalyzer()
|
||
strategy_agent = RetrievalStrategyAgent()
|
||
|
||
# 初始化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:
|
||
index_path = PROJECT_ROOT / "data" / "rag_index_final" / "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']
|
||
_vector_store._embeddings = data['embeddings']
|
||
|
||
print(f"RAG索引加载完成: {len(_vector_store._chunks)} chunks")
|
||
|
||
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 = False # 是否使用LLM生成总结
|
||
|
||
|
||
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
|
||
|
||
|
||
class KnowledgeGraphData(BaseModel):
|
||
nodes: List[dict] = []
|
||
edges: List[dict] = []
|
||
|
||
|
||
class RAGQueryResponse(BaseModel):
|
||
response: str
|
||
sources: List[RAGSource] = []
|
||
mode: str
|
||
total_chunks: int
|
||
knowledge_graph: Optional[KnowledgeGraphData] = None # 动态生成的知识图谱数据
|
||
|
||
|
||
@router.post("/chat")
|
||
async def rag_chat(request: RAGQueryRequest):
|
||
"""RAG搜索端点"""
|
||
try:
|
||
store = get_vector_store()
|
||
|
||
# 使用Agent编排进行智能检索
|
||
if request.mode == "agentic":
|
||
# Agent编排模式
|
||
results = await _agentic_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)
|
||
|
||
# 格式化响应
|
||
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
|
||
))
|
||
|
||
# 生成回答(支持LLM总结)
|
||
response_text = await _generate_response_async(request.query, sources, request.use_llm)
|
||
|
||
# 从检索结果动态提取知识图谱
|
||
kg_data = _extract_knowledge_graph(sources, request.query)
|
||
|
||
return RAGQueryResponse(
|
||
response=response_text,
|
||
sources=sources if request.include_sources else [],
|
||
mode=request.mode,
|
||
total_chunks=len(store._chunks),
|
||
knowledge_graph=kg_data
|
||
)
|
||
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"搜索失败: {str(e)}")
|
||
|
||
|
||
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]
|
||
|
||
|
||
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 []
|
||
|
||
results = []
|
||
|
||
for chunk_id, chunk in store._chunks.items():
|
||
content_lower = chunk.content.lower()
|
||
|
||
total_score = 0
|
||
matched_words = 0
|
||
|
||
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
|
||
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
|
||
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总结(异步版本)"""
|
||
if not sources:
|
||
return "未找到相关文献。"
|
||
|
||
# 如果使用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"""你是一个基因家族分析专家。请根据以下检索到的文献内容,回答用户的问题。
|
||
|
||
用户问题: {query}
|
||
|
||
文献内容:
|
||
{context}
|
||
|
||
请生成一个专业、详细的回答,包括:
|
||
1. **核心概念解释** - 清晰解释相关概念
|
||
2. **主要研究发现** - 总结文献中的关键发现
|
||
3. **生物学意义** - 说明其在生物学/育种中的应用价值
|
||
|
||
格式要求:
|
||
- 使用 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
|
||
|
||
|
||
async def _agentic_search(store, query: str, top_k: int):
|
||
"""
|
||
Agent编排搜索 - 使用新的Orchestrator启用Self-RAG和MultiHopRAG
|
||
"""
|
||
# 使用新的 RAGAgentOrchestrator
|
||
try:
|
||
from src.rag.agentic.orchestrator import RAGAgentOrchestrator
|
||
|
||
# 创建orchestrator实例
|
||
orchestrator = RAGAgentOrchestrator(
|
||
vector_store=store,
|
||
retriever=None, # 使用store直接检索
|
||
max_iterations=3,
|
||
enable_self_rag=True,
|
||
enable_multi_hop=True,
|
||
multi_hop_threshold=0.3
|
||
)
|
||
|
||
# 运行编排
|
||
result = await orchestrator.run(query, store_memory=False)
|
||
|
||
# 转换结果为SearchResult列表
|
||
from src.rag.base import Chunk, SearchResult
|
||
results = []
|
||
for r_data in result.get("results", []):
|
||
chunk = Chunk(
|
||
chunk_id=r_data.get("chunk_id", "unknown"),
|
||
content=r_data.get("content", ""),
|
||
doc_id=r_data.get("doc_id", "unknown"),
|
||
metadata=r_data.get("metadata", {})
|
||
)
|
||
results.append(SearchResult(chunk=chunk, score=r_data.get("score", 0.5)))
|
||
|
||
return results[:top_k]
|
||
|
||
except Exception as e:
|
||
print(f"Orchestrator failed: {e}, falling back to hybrid search")
|
||
# 回退到hybrid search
|
||
return await _hybrid_search(store, query, top_k)
|
||
|
||
|
||
@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)
|
||
}
|
||
|
||
|
||
@router.post("/rag/analyze")
|
||
async def rag_analyze(request: RAGQueryRequest):
|
||
"""查询分析端点 - 测试Agent编排"""
|
||
try:
|
||
# 查询分析
|
||
analysis = query_analyzer.analyze(request.query)
|
||
|
||
# 确定检索策略
|
||
strategy = strategy_agent.determine_strategy(analysis)
|
||
params = strategy_agent.get_search_params(strategy)
|
||
|
||
return {
|
||
"query": request.query,
|
||
"analysis": {
|
||
"type": analysis.query_type.value,
|
||
"intent": analysis.intent,
|
||
"entities": analysis.entities,
|
||
"keywords": analysis.keywords[:10],
|
||
"required_info": analysis.required_info
|
||
},
|
||
"strategy": {
|
||
"primary_mode": strategy.primary_mode.value,
|
||
"secondary_modes": [m.value for m in strategy.secondary_modes],
|
||
"top_k": strategy.top_k,
|
||
"threshold": strategy.threshold,
|
||
"rerank": strategy.rerank
|
||
},
|
||
"search_params": params
|
||
}
|
||
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"分析失败: {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
|
||
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
|
||
|
||
|
||
# Orchestrator 实例缓存
|
||
_orchestrator = None
|
||
_orchestrator_config = None
|
||
|
||
def get_orchestrator(enable_self_rag: bool = False, enable_multi_hop: bool = False, multi_hop_threshold: float = 0.3):
|
||
"""获取Agent编排器实例"""
|
||
global _orchestrator, _orchestrator_config
|
||
|
||
# 检查配置是否变化
|
||
current_config = (enable_self_rag, enable_multi_hop, multi_hop_threshold)
|
||
|
||
if _orchestrator is None or _orchestrator_config != current_config:
|
||
from src.rag.agentic.orchestrator import RAGAgentOrchestrator
|
||
|
||
llm_mgr = get_llm_manager()
|
||
store = get_vector_store()
|
||
|
||
_orchestrator = RAGAgentOrchestrator(
|
||
retriever=store,
|
||
llm_client=llm_mgr,
|
||
enable_self_rag=enable_self_rag,
|
||
enable_multi_hop=enable_multi_hop,
|
||
multi_hop_threshold=multi_hop_threshold
|
||
)
|
||
_orchestrator_config = current_config
|
||
|
||
return _orchestrator
|
||
|
||
|
||
@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. Agent编排模式 (Self-RAG + MultiHopRAG)
|
||
if request.use_agentic or request.enable_self_rag or request.enable_multi_hop:
|
||
orchestrator = get_orchestrator(
|
||
enable_self_rag=request.enable_self_rag,
|
||
enable_multi_hop=request.enable_multi_hop,
|
||
multi_hop_threshold=0.3 # 降低阈值以更容易触发多跳
|
||
)
|
||
|
||
# 执行Agent编排检索
|
||
# 注意: max_iterations 在 orchestrator 初始化时设置
|
||
agent_result = await orchestrator.run(query=request.query)
|
||
|
||
results["agent_result"] = {
|
||
"final_answer": agent_result.get("answer", agent_result.get("final_answer", "")),
|
||
"iteration": agent_result.get("iterations", agent_result.get("iteration", 0)),
|
||
"query_analysis": agent_result.get("query_analysis", {}),
|
||
"evaluation": agent_result.get("evaluation", {}),
|
||
"reflection": agent_result.get("reflection", {}),
|
||
"multi_hop_used": agent_result.get("multi_hop_used", False),
|
||
"multi_hop_hops": agent_result.get("multi_hop_hops", 0),
|
||
"multi_hop_sources": agent_result.get("multi_hop_sources", [])
|
||
}
|
||
|
||
# 添加检索来源
|
||
sources = agent_result.get("results", [])
|
||
results["sources"] = [
|
||
{
|
||
"doc_id": s.get("doc_id", "unknown"),
|
||
"content": s.get("content", "")[:300],
|
||
"score": s.get("score", 0),
|
||
"metadata": s.get("metadata", {})
|
||
}
|
||
for s in sources[:5]
|
||
]
|
||
|
||
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"] = "已启用上下文压缩"
|
||
|
||
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'
|
||
|
||
# 物种模式
|
||
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)
|
||
|
||
# 提取物种
|
||
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)
|
||
|
||
return KnowledgeGraphData(nodes=nodes, edges=unique_edges)
|
||
|
||
|
||
@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_root = Path(
|
||
os.environ.get("PHYLOGENY_AUTO_RESULTS_DIR", "")
|
||
or PROJECT_ROOT / "evaluation" / "auto_results"
|
||
)
|
||
result_path = result_root / 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)}")
|