|
|
""" |
|
|
审稿人推荐系统主应用 |
|
|
基于AgentReview框架风格实现的Gradio界面 |
|
|
""" |
|
|
|
|
|
import json |
|
|
import os |
|
|
import time |
|
|
from datetime import datetime |
|
|
from typing import List, Dict, Any |
|
|
|
|
|
import gradio as gr |
|
|
|
|
|
from reviewer_recommendation import ( |
|
|
PaperInfo, Reviewer, RecommendationRequest, RecommendationResponse, AppState, |
|
|
AcademicSearcher, DynamicAcademicSearcher, LLMRecommendationEngine, OpenAlexSearcher |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
openai_api_key = os.environ.get('OPENAI_API_KEY', 'sk-cnaekqCE8TTvhJrY8q4GljfzLiHHbgR5nlzwgc14FG8iH3uj') |
|
|
os.environ['OPENAI_API_KEY'] = openai_api_key |
|
|
|
|
|
|
|
|
css = """ |
|
|
/* 调整列间距 */ |
|
|
.gradio-container .row { |
|
|
gap: 40px !important; /* 可以调整这个数值来改变间距 */ |
|
|
} |
|
|
|
|
|
/* 或者更精确地控制特定行的间距 */ |
|
|
.gradio-container .row > div { |
|
|
margin-right: 20px !important; /* 右侧边距 */ |
|
|
} |
|
|
|
|
|
.gradio-container .row > div:last-child { |
|
|
margin-right: 0 !important; /* 最后一列不需要右边距 */ |
|
|
} |
|
|
|
|
|
/* 对齐推荐结果标题和内容 */ |
|
|
.gradio-container .row > div:last-child h2 { |
|
|
margin-top: 0 !important; /* 移除标题顶部边距 */ |
|
|
} |
|
|
|
|
|
.gradio-container .row > div:last-child .html-content { |
|
|
margin-top: 0 !important; /* 移除内容顶部边距 */ |
|
|
} |
|
|
|
|
|
/* 隐藏标题下方的默认边框 */ |
|
|
.gradio-container h1 { |
|
|
border-bottom: none !important; |
|
|
margin-bottom: 20px !important; |
|
|
} |
|
|
|
|
|
/* 隐藏可能的默认分隔线 */ |
|
|
.gradio-container hr { |
|
|
display: none !important; |
|
|
} |
|
|
""" |
|
|
|
|
|
|
|
|
app_state = AppState() |
|
|
|
|
|
|
|
|
def format_reviewers_output(reviewers: List[Dict[str, Any]]) -> str: |
|
|
"""格式化审稿人输出""" |
|
|
if not reviewers: |
|
|
return "<p style='font-size: 14px;'>未找到合适的审稿人推荐,请尝试调整关键词或查询条件。</p>" |
|
|
|
|
|
output_lines = [] |
|
|
for i, reviewer in enumerate(reviewers, 1): |
|
|
|
|
|
if isinstance(reviewer, dict): |
|
|
name = reviewer.get('name', 'N/A') |
|
|
affiliation = reviewer.get('affiliation', 'N/A') |
|
|
email = reviewer.get('email', 'N/A') |
|
|
expertise_areas = reviewer.get('expertise_areas', []) |
|
|
reason = reviewer.get('reason', 'N/A') |
|
|
relevance_score = reviewer.get('relevance_score', 0) |
|
|
else: |
|
|
|
|
|
name = getattr(reviewer, 'name', 'N/A') |
|
|
affiliation = getattr(reviewer, 'affiliation', 'N/A') |
|
|
email = getattr(reviewer, 'email', 'N/A') |
|
|
expertise_areas = getattr(reviewer, 'expertise_areas', []) |
|
|
reason = getattr(reviewer, 'reason', 'N/A') |
|
|
relevance_score = getattr(reviewer, 'relevance_score', 0) |
|
|
|
|
|
output_lines.append(f"<h3 style='font-size: 16px; margin: 20px 0 10px 0;'>审稿人 {i}</h3>") |
|
|
output_lines.append(f"<p style='font-size: 14px; margin: 5px 0;'><strong>姓名</strong>: {name}</p>") |
|
|
output_lines.append(f"<p style='font-size: 14px; margin: 5px 0;'><strong>单位</strong>: {affiliation}</p>") |
|
|
output_lines.append(f"<p style='font-size: 14px; margin: 5px 0;'><strong>邮箱</strong>: {email}</p>") |
|
|
if expertise_areas: |
|
|
output_lines.append(f"<p style='font-size: 14px; margin: 5px 0;'><strong>专业领域</strong>: {', '.join(expertise_areas)}</p>") |
|
|
output_lines.append(f"<p style='font-size: 14px; margin: 5px 0;'><strong>推荐理由</strong>: {reason}</p>") |
|
|
|
|
|
try: |
|
|
score_display = f"{float(relevance_score):.2f}" |
|
|
except (ValueError, TypeError): |
|
|
score_display = "0.00" |
|
|
output_lines.append(f"<p style='font-size: 14px; margin: 5px 0;'><strong>匹配度</strong>: {score_display}</p>") |
|
|
output_lines.append("<hr style='margin: 15px 0;'>") |
|
|
|
|
|
return "".join(output_lines) |
|
|
|
|
|
|
|
|
def recommend_reviewers(paper_title: str, paper_abstract: str, paper_authors: str, paper_affiliations: str, reviewer_count) -> str: |
|
|
"""推荐审稿人的主要函数""" |
|
|
try: |
|
|
|
|
|
app_state.is_processing = True |
|
|
app_state.last_error = None |
|
|
|
|
|
|
|
|
try: |
|
|
reviewer_count = int(reviewer_count) |
|
|
except (ValueError, TypeError): |
|
|
return "错误:推荐审稿人数量必须是有效的数字。" |
|
|
|
|
|
|
|
|
if not paper_title.strip() or not paper_abstract.strip(): |
|
|
return "错误:论文标题和摘要不能为空。" |
|
|
|
|
|
if reviewer_count < 1 or reviewer_count > 10: |
|
|
return "错误:推荐审稿人数量应在1-10之间。" |
|
|
|
|
|
|
|
|
authors = [author.strip() for author in paper_authors.split(',') if author.strip()] |
|
|
affiliations = [aff.strip() for aff in paper_affiliations.split(',') if aff.strip()] |
|
|
|
|
|
|
|
|
paper = PaperInfo( |
|
|
title=paper_title.strip(), |
|
|
abstract=paper_abstract.strip(), |
|
|
keywords=[], |
|
|
authors=authors, |
|
|
affiliations=affiliations |
|
|
) |
|
|
|
|
|
|
|
|
request = RecommendationRequest( |
|
|
paper=paper, |
|
|
reviewer_count=reviewer_count |
|
|
) |
|
|
|
|
|
app_state.current_request = request |
|
|
|
|
|
|
|
|
start_time = time.time() |
|
|
|
|
|
|
|
|
print("初始化推荐系统...") |
|
|
|
|
|
openalex_searcher = OpenAlexSearcher(limit=reviewer_count * 3) |
|
|
dynamic_searcher = DynamicAcademicSearcher(openalex_searcher=openalex_searcher) |
|
|
engine = LLMRecommendationEngine() |
|
|
|
|
|
|
|
|
print("开始双数据源检索候选文献...") |
|
|
|
|
|
years_after = 3 |
|
|
|
|
|
channel1_candidates, channel2_candidates, channel3_candidates = dynamic_searcher.search_with_dynamic_queries( |
|
|
paper=paper, |
|
|
num_reviewers=reviewer_count, |
|
|
years_after=years_after |
|
|
) |
|
|
|
|
|
if not channel1_candidates and not channel2_candidates and not channel3_candidates: |
|
|
return "未找到相关候选文献,请尝试调整关键词或查询条件。" |
|
|
|
|
|
print(f"找到候选文献:{len(channel1_candidates)} 篇,开始分析...") |
|
|
|
|
|
|
|
|
found_reviewers = engine.analyze_candidates(paper, channel1_candidates, reviewer_count) |
|
|
|
|
|
|
|
|
search_time = time.time() - start_time |
|
|
|
|
|
|
|
|
response = RecommendationResponse( |
|
|
reviewers=found_reviewers[:reviewer_count], |
|
|
search_time=search_time, |
|
|
total_candidates=len(channel1_candidates) + len(channel2_candidates) + len(channel3_candidates), |
|
|
success=True |
|
|
) |
|
|
|
|
|
app_state.current_response = response |
|
|
app_state.is_processing = False |
|
|
|
|
|
|
|
|
output = format_reviewers_output(response.reviewers) |
|
|
|
|
|
return output |
|
|
|
|
|
except Exception as e: |
|
|
app_state.is_processing = False |
|
|
app_state.last_error = str(e) |
|
|
error_msg = f"推荐过程中发生错误: {str(e)}" |
|
|
print(error_msg) |
|
|
return error_msg |
|
|
|
|
|
|
|
|
def clear_form(): |
|
|
"""清空表单""" |
|
|
return "", "", "", "", "3" |
|
|
|
|
|
|
|
|
|
|
|
def create_interface(): |
|
|
"""创建Gradio界面""" |
|
|
|
|
|
with gr.Blocks(css=css, title="审稿人推荐系统") as demo: |
|
|
|
|
|
|
|
|
gr.Markdown("# 审稿人推荐系统") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
|
|
|
gr.Markdown("## 论文信息") |
|
|
|
|
|
paper_title = gr.Textbox( |
|
|
lines=2, |
|
|
label="论文标题", |
|
|
placeholder="请输入论文标题..." |
|
|
) |
|
|
|
|
|
paper_abstract = gr.Textbox( |
|
|
lines=8, |
|
|
label="论文摘要", |
|
|
placeholder="请输入论文摘要..." |
|
|
) |
|
|
|
|
|
|
|
|
paper_authors = gr.Textbox( |
|
|
lines=2, |
|
|
label="作者姓名(用逗号分隔)", |
|
|
placeholder="必填" |
|
|
) |
|
|
|
|
|
paper_affiliations = gr.Textbox( |
|
|
lines=2, |
|
|
label="作者所属单位(用逗号分隔)", |
|
|
placeholder="必填" |
|
|
) |
|
|
|
|
|
reviewer_count = gr.Dropdown( |
|
|
choices=["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"], |
|
|
value="3", |
|
|
label="推荐审稿人数量" |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
recommend_btn = gr.Button("开始推荐", variant="primary") |
|
|
clear_btn = gr.Button("清空表单") |
|
|
|
|
|
with gr.Column(scale=1): |
|
|
|
|
|
gr.Markdown("## 推荐结果") |
|
|
|
|
|
output_text = gr.HTML( |
|
|
value="<p style='font-size: 14px; text-align: center; color: #666; margin-top: 0;'>请输入论文信息并点击'开始推荐'按钮</p>", |
|
|
label="推荐结果" |
|
|
) |
|
|
|
|
|
|
|
|
recommend_btn.click( |
|
|
fn=recommend_reviewers, |
|
|
inputs=[paper_title, paper_abstract, paper_authors, paper_affiliations, reviewer_count], |
|
|
outputs=[output_text] |
|
|
) |
|
|
|
|
|
clear_btn.click( |
|
|
fn=clear_form, |
|
|
outputs=[paper_title, paper_abstract, paper_authors, paper_affiliations, reviewer_count] |
|
|
) |
|
|
|
|
|
return demo |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
|
|
demo = create_interface() |
|
|
demo.launch( |
|
|
server_name="0.0.0.0", |
|
|
|
|
|
debug=False, |
|
|
show_error=True, |
|
|
quiet=False |
|
|
) |