文献批量上传,api调用改为异步,优化显示

This commit is contained in:
2025-01-13 11:02:40 +00:00
parent eeaeff04d0
commit 738fa19b9d
2 changed files with 533 additions and 356 deletions
+272 -181
View File
@@ -31,6 +31,23 @@
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
transition: transform 0.2s, box-shadow 0.2s;
margin-bottom: 24px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.card-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
}
.card-text {
font-size: 14px;
color: #64748B;
line-height: 1.6;
}
.text-muted {
font-size: 12px;
}
.card:hover {
@@ -320,10 +337,10 @@
<nav class="navbar">
<div class="navbar-brand">Lab</div>
<div class="navbar-menu">
<div class="navbar-menu-item" onclick="window.open('lab.html', '_blank')">Project</div>
<div class="navbar-menu-item" onclick="window.open('device.html', '_blank')">Device</div>
<div class="navbar-menu-item" onclick="window.open('reports.html', '_blank')">Reports</div>
<div class="navbar-menu-item active" onclick="window.open('literature.html', '_blank')">Literature</div>
<div class="navbar-menu-item" onclick="window.location.href='lab.html'">Project</div>
<div class="navbar-menu-item" onclick="window.location.href='device.html'">Device</div>
<div class="navbar-menu-item" onclick="window.location.href='reports.html'">Reports</div>
<div class="navbar-menu-item active" onclick="window.location.href='literature.html'">Literature</div>
</div>
<div class="navbar-right">
<a href="https://beta.obscura.work/lab/device.html" target="_blank" class="btn btn-primary">
@@ -553,47 +570,23 @@
}
// Literature card rendering function
function renderReferenceCard(reference, projectId, analysisResult = null) {
let basicInfo = {
title: reference.reference_title,
author: 'Unknown Author',
publication_date: 'Unknown Date',
journal_publisher: 'Unknown Publisher'
};
if (analysisResult && analysisResult['1. Basic Information']) {
const analyzeBasicInfo = analysisResult['1. Basic Information'];
basicInfo = {
title: analyzeBasicInfo.title || reference.reference_title,
author: analyzeBasicInfo.author || 'Unknown Author',
publication_date: analyzeBasicInfo.publication_date || 'Unknown Date',
journal_publisher: analyzeBasicInfo.journal_publisher || 'Unknown Publisher',
document_type: analyzeBasicInfo.document_type || 'Unknown Type'
};
}
function renderReferenceCard(reference, projectId) {
const uploadTime = reference.upload_time;
const cardId = `reference-${reference._id}`;
const encodedAnalysis = analysisResult ?
encodeURIComponent(JSON.stringify(analysisResult)) : '';
setTimeout(() => {
const card = document.getElementById(cardId);
if (card) {
card.setAttribute('data-analysis', encodedAnalysis);
}
}, 0);
return `
<div class="card mb-3" id="${cardId}">
<div class="card-body d-flex position-relative">
<div class="flex-grow-1" style="cursor: pointer;" onclick="viewReferenceDetailFromCard(this.closest('.card'))">
<h5 class="card-title">${basicInfo.title}</h5>
<p class="card-text">
Author: ${basicInfo.author}<br>
Publication Date: ${basicInfo.publication_date}<br>
Journal/Publisher: ${basicInfo.journal_publisher}
</p>
<h5 class="card-title">${reference.reference_title}</h5>
<div class="card-text analysis-content-${reference._id}">
<div class="d-flex align-items-center">
<div class="spinner-border spinner-border-sm text-primary me-2" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<span>Analyzing literature content...</span>
</div>
</div>
<div class="btn-group">
<button class="btn btn-warning btn-sm"
onclick="event.stopPropagation(); viewReferenceDetailFromCard(this.closest('.card'))">
@@ -608,13 +601,14 @@
<div class="position-absolute" style="right: 1rem; top: 50%; transform: translateY(-50%);">
<button class="btn rounded-circle d-flex align-items-center justify-content-center"
style="width: 48px; height: 48px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); background: var(--primary-color); color: white;"
onclick="event.stopPropagation(); openChatDialog('${reference._id}', '${basicInfo.title}')">
onclick="event.stopPropagation(); openChatDialog('${reference._id}', '${reference.reference_title}')">
<i class="bi bi-chat-dots" style="font-size: 1.2rem;"></i>
</button>
</div>
</div>
<div class="card-footer text-muted">
Upload Time: ${uploadTime}
<span class="ms-2 analysis-status-${reference._id}">Analysis in progress...</span>
</div>
</div>
`;
@@ -625,6 +619,7 @@
const referencesList = document.getElementById('referenceAnalysis');
try {
// Get literature list
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/references`, {
headers: {
'Authorization': `Bearer ${token}`
@@ -632,7 +627,6 @@
});
if (response.status === 401) {
// Token 无效或过期
window.location.href = 'https://beta.obscura.work/lab/login.html';
return;
}
@@ -652,27 +646,93 @@
referencesList.innerHTML = '<div class="alert alert-info">No literature data, please upload literature</div>';
return;
}
const referencesWithAnalysis = await Promise.all(references.map(async (ref) => {
// Render literature cards
const referenceCards = references.map(ref => renderReferenceCard(ref, projectId)).join('');
referencesList.innerHTML = referenceCards;
// Get analysis status and results for each literature
references.forEach(async (ref) => {
try {
const analysisResponse = await fetch(`${API_BASE_URL}/references/${ref._id}/report`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (analysisResponse.ok) {
const analysis = await analysisResponse.json();
return renderReferenceCard(ref, projectId, analysis);
if (!analysisResponse.ok) {
throw new Error('Failed to fetch analysis');
}
const analysisResult = await analysisResponse.json();
const contentElement = document.querySelector(`.analysis-content-${ref._id}`);
const statusElement = document.querySelector(`.analysis-status-${ref._id}`);
if (analysisResult.status === 'processing') {
// Processing status
contentElement.innerHTML = `
<div class="d-flex align-items-center">
<div class="spinner-border spinner-border-sm text-primary me-2" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<span>Analyzing literature content...</span>
</div>
`;
statusElement.innerHTML = '<span class="text-primary">Analysis in progress...</span>';
} else if (analysisResult.status === 'completed') {
// Completed status
let author = 'N/A';
let publicationDate = 'N/A';
let journal = 'N/A';
try {
if (analysisResult['1. Basic Information']) {
const basicInfo = analysisResult['1. Basic Information'];
author = basicInfo.author || 'N/A';
publicationDate = basicInfo.publication_date || 'N/A';
journal = basicInfo.journal_publisher || 'N/A';
// 更新卡片标题为分析结果中的标题
const titleElement = document.querySelector(`#reference-${ref._id} .card-title`);
if (titleElement && basicInfo.title) {
titleElement.textContent = basicInfo.title;
}
}
contentElement.innerHTML = `
<p class="mb-1">Author: ${author}</p>
<p class="mb-1">Publication Date: ${publicationDate}</p>
<p class="mb-1">Journal/Publisher: ${journal}</p>
`;
statusElement.innerHTML = '<span class="text-success">Analysis completed</span>';
} catch (parseError) {
console.error('Failed to parse analysis result:', parseError);
}
} else if (analysisResult.status === 'failed') {
// Failed status
contentElement.innerHTML = `
<div class="text-danger">
<i class="bi bi-exclamation-triangle me-2"></i>
Analysis failed
</div>
`;
statusElement.innerHTML = `<span class="text-danger">Analysis failed: ${analysisResult.message || 'Unknown error'}</span>`;
}
} catch (error) {
console.error(`Failed to get analysis report for literature ${ref._id}:`, error);
console.error(`Failed to fetch analysis for reference ${ref._id}:`, error);
const contentElement = document.querySelector(`.analysis-content-${ref._id}`);
const statusElement = document.querySelector(`.analysis-status-${ref._id}`);
contentElement.innerHTML = `
<div class="text-danger">
<i class="bi bi-exclamation-triangle me-2"></i>
Failed to get analysis status
</div>
`;
statusElement.innerHTML = '<span class="text-danger">Failed to get analysis status</span>';
}
return renderReferenceCard(ref, projectId);
}));
referencesList.innerHTML = referencesWithAnalysis.join('');
});
} catch (error) {
console.error('Failed to load literature list:', error);
referencesList.innerHTML = `
@@ -686,7 +746,6 @@
}
}
// Upload literature function
async function uploadLiterature() {
if (!currentProjectId) {
alert('Please select a project first');
@@ -702,12 +761,10 @@
const files = Array.from(e.target.files);
if (!files.length) return;
// 创建进度条卡片
// 创建进度条
const progressCard = document.createElement('div');
progressCard.className = 'card w-100 mb-4';
progressCard.style.cssText = `
transition: all 0.3s ease;
`;
progressCard.style.cssText = `transition: all 0.3s ease;`;
const progressCardBody = document.createElement('div');
progressCardBody.className = 'card-body';
@@ -746,80 +803,56 @@
const titleElement = document.getElementById('referenceAnalysisTitle');
titleElement.parentNode.parentNode.insertBefore(progressCard, titleElement.parentNode);
const updateProgress = (current, total, status) => {
const progress = (current / total) * 100;
progressFill.style.width = `${progress}%`;
progressText.textContent = `${status} (${current}/${total})`;
};
const removeProgressBar = () => {
progressCard.style.opacity = '0';
progressCard.style.transform = 'translateY(-10px)';
setTimeout(() => {
progressCard.remove();
}, 300);
};
try {
for (let i = 0; i < files.length; i++) {
const file = files[i];
const formData = new FormData();
formData.append('file', file);
formData.append('reference_title', file.name);
const formData = new FormData();
files.forEach(file => {
formData.append('files', file);
});
try {
updateProgress(i + 1, files.length, `Uploading: ${file.name}`);
const uploadResponse = await fetch(`${API_BASE_URL}/projects/${currentProjectId}/references`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
});
if (!uploadResponse.ok) {
throw new Error((await uploadResponse.json()).detail || 'Upload failed');
}
const uploadResult = await uploadResponse.json();
const referenceId = uploadResult.reference_id;
updateProgress(i + 1, files.length, `Analyzing: ${file.name}`);
const analyzeResponse = await fetch(`${API_BASE_URL}/references/${referenceId}/analyze`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!analyzeResponse.ok) {
throw new Error((await analyzeResponse.json()).detail || 'Analysis failed');
}
const reportResponse = await fetch(`${API_BASE_URL}/references/${referenceId}/report`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!reportResponse.ok) {
throw new Error('Failed to get analysis report');
}
} catch (error) {
console.error(`Error processing ${file.name}:`, error);
// 使用 XMLHttpRequest 来获取上传进度
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
progressFill.style.width = percentComplete + '%';
progressText.textContent = `Uploading: ${Math.round(percentComplete)}%`;
}
}
};
const uploadPromise = new Promise((resolve, reject) => {
xhr.onload = function() {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.response));
} else {
reject(new Error('Upload failed'));
}
};
xhr.onerror = () => reject(new Error('Upload failed'));
});
xhr.open('POST', `${API_BASE_URL}/projects/${currentProjectId}/references/batch`);
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
xhr.send(formData);
await uploadPromise;
// 上传完成后移除进度条
setTimeout(() => {
removeProgressBar();
}, 1000);
progressCard.style.opacity = '0';
progressCard.style.transform = 'translateY(-10px)';
setTimeout(() => {
progressCard.remove();
}, 300);
}, 500);
// 刷新文献列表
await loadReferences(currentProjectId);
} catch (error) {
console.error('Upload process failed:', error);
console.error('Upload failed:', error);
progressCard.remove();
progressText.remove();
alert(`Upload failed: ${error.message}`);
}
};
@@ -827,79 +860,104 @@
}
// View literature details function
async function viewReferenceDetail(referenceId, encodedAnalysis = '') {
async function viewReferenceDetail(referenceId) {
try {
let analysisResult;
if (encodedAnalysis) {
try {
analysisResult = JSON.parse(decodeURIComponent(encodedAnalysis));
} catch (e) {
console.error('Failed to parse encoded analysis:', e);
analysisResult = null;
const response = await fetch(`${API_BASE_URL}/references/${referenceId}/report`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch analysis report');
}
if (!analysisResult) {
const response = await fetch(`${API_BASE_URL}/references/${referenceId}/report`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const analysisResult = await response.json();
let modalContent;
if (analysisResult.status === 'processing') {
modalContent = `
<div class="d-flex align-items-center justify-content-center p-5">
<div class="spinner-border text-primary me-3" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<h5 class="mb-0">Analyzing literature content, please wait...</h5>
</div>
`;
} else if (analysisResult.status === 'completed') {
const formattedReport = JSON.stringify(analysisResult, null, 2)
.replace(/[{}"]/g, '')
.replace(/},/g, '')
.replace(/{ },/g, '')
.replace(/^\s*,/gm, '')
.replace(/Literature Analysis Report:/, '<div style="text-align: center"><strong>Literature Analysis Report</strong></div>')
.replace(/^(\s*\d+\.[^:\n]+:)/gm, '<strong>$1</strong>')
.split('\n')
.map(line => line.trimEnd())
.join('\n');
if (!response.ok) {
throw new Error('Failed to get analysis report');
}
analysisResult = await response.json();
modalContent = `
<pre class="report-content" style="white-space: pre-wrap; word-wrap: break-word;">${formattedReport}</pre>
`;
} else if (analysisResult.status === 'failed') {
modalContent = `
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle me-2"></i>
Analysis failed: ${analysisResult.message || 'Unknown error'}
</div>
`;
}
const formattedReport = JSON.stringify(analysisResult, null, 2)
.replace(/[{}"]/g, '')
.replace(/},/g, '')
.replace(/{ },/g, '')
.replace(/^\s*,/gm, '')
.replace(/Literature Analysis Report:/, '<div style="text-align: center"><strong>Literature Analysis Report</strong></div>')
.replace(/^(\s*\d+\.[^:\n]+:)/gm, '<strong>$1</strong>')
.split('\n')
.map(line => line.trimEnd())
.join('\n');
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.id = 'referenceDetailModal';
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-labelledby', 'referenceDetailModalTitle');
modal.innerHTML = `
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-dialog modal-lg" style="max-width: 80%; margin: 1.75rem auto;">
<div class="modal-content" style="min-height: 80vh;">
<div class="modal-header">
<h5 class="modal-title">Literature Analysis Report</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<h5 class="modal-title" id="referenceDetailModalTitle">Literature Analysis Report</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<pre class="report-content" style="white-space: pre-wrap; word-wrap: break-word;">${formattedReport}</pre>
<div class="modal-body" style="max-height: calc(80vh - 60px); overflow-y: auto;">
${modalContent}
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const modalInstance = new bootstrap.Modal(modal);
const modalInstance = new bootstrap.Modal(modal, {
keyboard: true,
focus: true
});
modalInstance.show();
modal.addEventListener('hidden.bs.modal', () => {
document.body.removeChild(modal);
modal.addEventListener('hide.bs.modal', () => {
const focusedElement = document.activeElement;
if (modal.contains(focusedElement)) {
focusedElement.blur();
}
});
modal.addEventListener('hidden.bs.modal', () => {
modal.remove();
});
} catch (error) {
console.error('Failed to view details:', error);
alert('Failed to get analysis report, please try again');
alert('Failed to get literature analysis report, please try again later');
}
}
// View literature details from card
function viewReferenceDetailFromCard(card) {
const referenceId = card.id.replace('reference-', '');
const encodedAnalysis = card.getAttribute('data-analysis') || '';
viewReferenceDetail(referenceId, encodedAnalysis);
viewReferenceDetail(referenceId);
}
// Download literature analysis report function
@@ -1015,25 +1073,29 @@
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.id = 'chatModal';
modal.setAttribute('data-bs-backdrop', 'static');
modal.setAttribute('data-bs-keyboard', 'false');
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-labelledby', 'chatModalTitle');
modal.innerHTML = `
<div class="modal-dialog modal-lg modal-dialog-centered" style="max-width: 800px;">
<div class="modal-content" style="min-height: 60vh;">
<div class="modal-header">
<h5 class="modal-title">Chat with Literature: ${title}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<h5 class="modal-title" id="chatModalTitle">Chat with Literature: ${title}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body d-flex flex-column" style="height: calc(60vh - 76px);">
<div id="chatHistory" class="flex-grow-1" style="overflow-y: auto; margin-bottom: 15px; padding: 10px;">
<div id="chatHistory" class="flex-grow-1 custom-scrollbar" style="overflow-y: auto; margin-bottom: 15px; padding: 10px;">
<!-- Chat history will be loaded here -->
</div>
<div class="input-group">
<input type="text" id="questionInput" class="form-control"
placeholder="Type your question here...">
placeholder="Type your question here..."
aria-label="Question input">
<button class="btn"
style="background: var(--primary-color); color: white;"
onclick="sendQuestion('${referenceId}')">
onclick="sendQuestion('${referenceId}')"
aria-label="Send question">
Send
</button>
</div>
@@ -1043,23 +1105,45 @@
`;
document.body.appendChild(modal);
const modalInstance = new bootstrap.Modal(modal);
// 使用更多的 Modal 选项来控制行为
const modalInstance = new bootstrap.Modal(modal, {
backdrop: 'static', // 静态背景
keyboard: false, // 禁用键盘关闭
focus: true // 自动聚焦
});
// 在显示模态框之前添加事件监听器
modal.addEventListener('show.bs.modal', () => {
// 确保模态框可以正确接收焦点
modal.removeAttribute('aria-hidden');
});
// 在隐藏模态框之前清除焦点
modal.addEventListener('hide.bs.modal', () => {
const focusedElement = document.activeElement;
if (modal.contains(focusedElement)) {
focusedElement.blur();
}
});
// 在模态框完全隐藏后移除它
modal.addEventListener('hidden.bs.modal', () => {
modal.remove();
});
modalInstance.show();
// Load chat history
// 加载聊天历史
await loadChatHistory(referenceId);
// Add event listener for Enter key
// 添加回车键事件监听器
const input = document.getElementById('questionInput');
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendQuestion(referenceId);
}
});
modal.addEventListener('hidden.bs.modal', () => {
document.body.removeChild(modal);
});
}
// Load chat history function
@@ -1122,11 +1206,13 @@
}
const chatHistory = document.getElementById('chatHistory');
const sendButton = input.nextElementSibling;
try {
// Clear input and disable it
// 清空输入并禁用输入框和发送按钮
input.value = '';
input.disabled = true;
sendButton.disabled = true;
// First, add the user's question to chat history
const questionDiv = document.createElement('div');
@@ -1236,8 +1322,13 @@
chatHistory.appendChild(errorDiv);
chatHistory.scrollTop = chatHistory.scrollHeight;
} finally {
// 重新启用输入框和发送按钮
input.disabled = false;
input.focus();
sendButton.disabled = false;
// 将焦点返回到输入框,但不强制聚焦
if (document.activeElement === document.body) {
input.focus();
}
}
}
</script>
+261 -175
View File
@@ -24,6 +24,9 @@ import PyPDF2
import numpy as np
import zipfile
from io import BytesIO, StringIO
import aiohttp
from concurrent.futures import ThreadPoolExecutor
from functools import partial
# Database Configuration
MONGODB_URL = "mongodb://lab:[email protected]:27017/lab"
@@ -1603,7 +1606,7 @@ async def analyze_project_data(project_data):
print(f"Error calling DeepSeek API: {e}")
return None
async def analyze_reference_document(content: str):
async def analyze_reference_document_async(content: str):
"""分析文献的基本信息和内容"""
system_prompt = """
You are an AI assistant tasked with analyzing academic literature.
@@ -1635,23 +1638,12 @@ async def analyze_reference_document(content: str):
}}
"""
messages = [
return await call_deepseek_api_async([
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
try:
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
response_format={'type': 'json_object'}
)
return json.loads(response.choices[0].message.content)
except Exception as e:
print(f"Error calling DeepSeek API for document analysis: {e}")
return None
])
async def analyze_reference_value(content_analysis: dict):
async def analyze_reference_value_async(content_analysis: dict):
"""基于内容分析结果评估文献的价值"""
system_prompt = """
You are an AI assistant tasked with evaluating the value of academic literature based on its content analysis.
@@ -1674,21 +1666,10 @@ async def analyze_reference_value(content_analysis: dict):
}}
"""
messages = [
return await call_deepseek_api_async([
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
try:
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
response_format={'type': 'json_object'}
)
return json.loads(response.choices[0].message.content)
except Exception as e:
print(f"Error calling DeepSeek API for value evaluation: {e}")
return None
])
# 修改函数定义为异步函数
async def analyze_reference_summary(reference_data):
@@ -2059,70 +2040,6 @@ class ReferenceModel(BaseModel):
arbitrary_types_allowed = True
json_encoders = {ObjectId: str}
# 添加文献上传路由
@app.post("/lab/projects/{project_id}/references")
async def upload_reference(
project_id: str,
file: UploadFile = File(...),
reference_title: str = Form(...),
current_user: UserModel = Depends(get_current_user)
):
"""上传项目相关文献"""
db = await get_database()
try:
# 验证项目是否存在且属于当前用户
project = await db.projects.find_one({
"_id": ObjectId(project_id),
"user_id": current_user.id
})
if not project:
raise HTTPException(status_code=404, detail="Project not found or unauthorized access")
# 验证文件类型(可选)
allowed_types = ["application/pdf", "application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"]
if file.content_type not in allowed_types:
raise HTTPException(status_code=400, detail="Unsupported file type")
# 确保上传目录存在
os.makedirs(upload_path, exist_ok=True)
# 生成安全的文件名
file_extension = os.path.splitext(file.filename)[1]
safe_filename = f"{project_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}{file_extension}"
file_path = os.path.join(upload_path, safe_filename)
# 保存文件
with open(file_path, "wb") as buffer:
content = await file.read()
buffer.write(content)
# 创建引用记录
reference = {
"project_id": ObjectId(project_id),
"reference_link": file_path,
"reference_title": reference_title or file.filename, # 使用提供的标题或文件名
"upload_time": datetime.now(timezone.utc)
}
result = await db.references.insert_one(reference)
return {
"message": "Reference uploaded successfully",
"reference_id": str(result.inserted_id),
"file_path": file_path,
"reference_title": reference["reference_title"]
}
except Exception as e:
print(f"Detailed error: {str(e)}") # 添加详细错误日志
if 'file_path' in locals() and os.path.exists(file_path):
os.remove(file_path)
raise HTTPException(
status_code=400,
detail=f"Failed to upload reference: {str(e)}"
)
# 删除文献
@app.delete("/lab/projects/{project_id}/references/{reference_id}")
@@ -2221,89 +2138,6 @@ async def get_reference_report(
except Exception as e:
print(f"Error closing Redis connection: {e}")
@app.get("/lab/references/{reference_id}/analyze")
async def analyze_reference_data(
reference_id: str,
current_user: UserModel = Depends(get_current_user)
):
"""分析文献数据"""
db = await get_database()
redis = await get_redis()
try:
# 从数据库获取文献信息
reference = await db.references.find_one({"_id": ObjectId(reference_id)})
if not reference:
raise HTTPException(status_code=404, detail="Reference record not found")
file_path = reference.get("reference_link")
if not file_path or not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="Reference file not found")
# 读取PDF文件内容
pdf_content = ""
try:
with open(file_path, 'rb') as file:
pdf_reader = PyPDF2.PdfReader(file)
for page in pdf_reader.pages:
content = page.extract_text()
pdf_content += content
except Exception as e:
print(f"Error reading PDF file: {str(e)}")
raise HTTPException(status_code=500, detail="Failed to read PDF file")
# 限制内容长度
MAX_CHARS = 180000
pdf_content = pdf_content[:MAX_CHARS]
# 分析文档内容
document_analysis = await analyze_reference_document(pdf_content)
if not document_analysis:
raise HTTPException(status_code=500, detail="Failed to analyze document")
# 分析文献价值
value_evaluation = await analyze_reference_value(document_analysis)
if not value_evaluation:
raise HTTPException(status_code=500, detail="Failed to evaluate value")
# 合并所有分析结果
analysis_result = {
**document_analysis,
**value_evaluation
}
# 将分析结果保存到db203
await redis.select(203)
report_key = f"reference_report:{reference_id}"
await redis.set(report_key, json.dumps(analysis_result))
# 更新数据库中的分析状态
await db.references.update_one(
{"_id": ObjectId(reference_id)},
{"$set": {
"analysis_status": "completed"
}}
)
return analysis_result
except Exception as e:
print(f"Error analyzing reference data: {str(e)}")
# 更新数据库中的分析状态为失败
if 'reference' in locals():
await db.references.update_one(
{"_id": ObjectId(reference_id)},
{"$set": {
"analysis_status": "failed",
"analysis_error": str(e),
}}
)
raise HTTPException(status_code=500, detail=str(e))
finally:
try:
await redis.aclose()
except Exception as e:
print(f"Error closing Redis connection: {e}")
@app.get("/lab/references/{project_id}/analyze_report")
async def analyze_reference_summary_report(
project_id: str,
@@ -2834,6 +2668,258 @@ async def get_reference_qa_history(
except Exception as e:
print(f"Error closing Redis connection: {e}")
@app.post("/lab/projects/{project_id}/references/batch")
async def batch_upload_references(
project_id: str,
files: List[UploadFile] = File(...),
current_user: UserModel = Depends(get_current_user)
):
"""批量上传项目相关文献"""
db = await get_database()
try:
# 验证项目是否存在且属于当前用户
project = await db.projects.find_one({
"_id": ObjectId(project_id),
"user_id": current_user.id
})
if not project:
raise HTTPException(status_code=404, detail="Project not found or unauthorized access")
uploaded_references = []
# 批量上传文件
for file in files:
# 验证文件类型
allowed_types = ["application/pdf", "application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"]
if file.content_type not in allowed_types:
continue # 跳过不支持的文件类型
# 确保上传目录存在
os.makedirs(upload_path, exist_ok=True)
# 生成安全的文件名
file_extension = os.path.splitext(file.filename)[1]
safe_filename = f"{project_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}{file_extension}"
file_path = os.path.join(upload_path, safe_filename)
# 保存文件
with open(file_path, "wb") as buffer:
content = await file.read()
buffer.write(content)
# 创建引用记录
reference = {
"project_id": ObjectId(project_id),
"reference_link": file_path,
"reference_title": file.filename,
"upload_time": datetime.now(timezone.utc)
}
result = await db.references.insert_one(reference)
reference_info = {
"reference_id": str(result.inserted_id),
"file_path": file_path,
"reference_title": reference["reference_title"]
}
uploaded_references.append(reference_info)
# 为每个文献创建初始状态
redis = await get_redis()
try:
await redis.select(203)
report_key = f"reference_report:{str(result.inserted_id)}"
initial_status = {
"status": "processing",
"message": "Analysis in progress"
}
await redis.set(report_key, json.dumps(initial_status))
finally:
await redis.aclose()
# 在后台启动分析任务
if uploaded_references:
asyncio.create_task(process_batch_analysis(uploaded_references))
return {
"message": f"Successfully uploaded {len(uploaded_references)} files",
"uploaded_files": uploaded_references
}
except Exception as e:
print(f"Batch upload error: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
async def process_batch_analysis(references: List[dict]):
"""批量处理文献分析的后台任务"""
redis = await get_redis()
# 限制并发数量
semaphore = asyncio.Semaphore(3)
async def process_single_reference(ref: dict):
async with semaphore:
try:
reference_id = ref["reference_id"]
file_path = ref["file_path"]
if not os.path.exists(file_path):
print(f"Reference file not found: {file_path}")
return
# 异步读取PDF
pdf_content = await read_pdf_async(file_path)
if not pdf_content:
raise Exception("Failed to read PDF content")
# 异步分析文档
document_analysis = await analyze_reference_document_async(pdf_content)
if not document_analysis:
raise Exception("Failed to analyze document")
# 等待一小段时间避免API限制
await asyncio.sleep(1)
# 异步分析价值
value_evaluation = await analyze_reference_value_async(document_analysis)
if not value_evaluation:
raise Exception("Failed to evaluate value")
# 合并结果
analysis_result = {
**document_analysis,
**value_evaluation,
"status": "completed"
}
# 保存结果
await redis.select(203)
report_key = f"reference_report:{reference_id}"
await redis.set(report_key, json.dumps(analysis_result))
except Exception as e:
print(f"Error processing reference {ref['reference_id']}: {str(e)}")
try:
await redis.select(203)
report_key = f"reference_report:{ref['reference_id']}"
error_status = {
"status": "failed",
"message": str(e)
}
await redis.set(report_key, json.dumps(error_status))
except Exception as redis_error:
print(f"Error updating Redis status: {redis_error}")
try:
# 并发处理所有引用
await asyncio.gather(
*(process_single_reference(ref) for ref in references)
)
finally:
try:
await redis.aclose()
except Exception as e:
print(f"Error closing Redis connection: {e}")
# 在全局范围创建线程池
pdf_thread_pool = ThreadPoolExecutor(max_workers=3) # 限制并发PDF处理数量
# 创建异步HTTP客户端会话
async def get_aiohttp_session():
return aiohttp.ClientSession(
base_url="https://api.deepseek.com/v1/", # 添加了末尾的斜杠
headers={"Authorization": f"Bearer sk-3027fb3c810b4e17985fa397d41250b9"}
)
async def read_pdf_async(file_path: str) -> str:
"""在线程池中异步读取PDF"""
def read_pdf():
try:
pdf_content = ""
with open(file_path, 'rb') as file:
pdf_reader = PyPDF2.PdfReader(file)
for page in pdf_reader.pages:
content = page.extract_text()
pdf_content += content
return pdf_content[:180000] # 限制内容长度
except Exception as e:
print(f"Error reading PDF: {e}")
return ""
loop = asyncio.get_event_loop()
return await loop.run_in_executor(pdf_thread_pool, read_pdf)
async def call_deepseek_api_async(messages: list) -> dict:
"""异步调用DeepSeek API"""
async with await get_aiohttp_session() as session:
async with session.post("/chat/completions", json={
"model": "deepseek-chat",
"messages": messages,
"response_format": {"type": "json_object"}
}) as response:
if response.status == 200:
data = await response.json()
return json.loads(data["choices"][0]["message"]["content"])
else:
raise Exception(f"API调用失败: {await response.text()}")
async def process_reference_analysis(reference_id: str, reference: dict):
"""后台处理文献分析"""
redis = await get_redis()
try:
file_path = reference.get("reference_link")
if not file_path or not os.path.exists(file_path):
print(f"Reference file not found: {file_path}")
return
# 异步读取PDF
pdf_content = await read_pdf_async(file_path)
if not pdf_content:
raise Exception("Failed to read PDF content")
# 异步分析文档内容
document_analysis = await analyze_reference_document_async(pdf_content)
if not document_analysis:
print("Failed to analyze document")
return
# 异步分析文献价值
value_evaluation = await analyze_reference_value_async(document_analysis)
if not value_evaluation:
print("Failed to evaluate value")
return
# 合并分析结果
analysis_result = {
**document_analysis,
**value_evaluation
}
# 保存分析结果到db203
await redis.select(203)
report_key = f"reference_report:{reference_id}"
await redis.set(report_key, json.dumps(analysis_result))
except Exception as e:
print(f"Error analyzing reference data: {str(e)}")
try:
await redis.select(203)
report_key = f"reference_report:{reference_id}"
error_status = {
"status": "failed",
"message": str(e)
}
await redis.set(report_key, json.dumps(error_status))
except Exception as redis_error:
print(f"Error updating Redis status: {redis_error}")
finally:
try:
await redis.aclose()
except Exception as e:
print(f"Error closing Redis connection: {e}")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=6000)