| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933 |
- """
- PaperAssistant - 智能论文助手 Gradio Web 界面
- 提供文献检索、论文总结、引用生成、论文润色、大纲生成、PDF 提取等功能。
- 所有操作自动记录到对话历史,可回溯查看。
- """
- import os
- import sys
- import json
- from datetime import datetime
- import gradio as gr
- from dotenv import load_dotenv
- load_dotenv()
- # Windows UTF-8 兼容
- sys.stdout.reconfigure(encoding='utf-8')
- from hello_agents import (
- HelloAgentsLLM, SimpleAgent, ToolRegistry, Config
- )
- from src.arxiv_tool import ArxivSearchTool
- from src.pdf_tool import PDFExtractTool
- from src.citation_tool import CitationTool
- from src.literature_tool import LiteratureSearchTool
- from src.pubmed_tool import PubMedSearchTool
- from src.crossref_tool import CrossRefSearchTool
- from src.openalex_tool import OpenAlexSearchTool
- from src.aminer_tool import AminerSearchTool
- # ========================================
- # 对话日志系统
- # ========================================
- HISTORY_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "outputs", "conversations")
- class ConversationLogger:
- """对话日志管理器:记录、持久化、检索所有交互"""
- def __init__(self, save_dir=HISTORY_DIR):
- self.save_dir = save_dir
- os.makedirs(save_dir, exist_ok=True)
- self.records = self._load_all()
- def _filepath(self):
- """当前会话的日志文件"""
- today = datetime.now().strftime("%Y-%m-%d")
- return os.path.join(self.save_dir, f"session_{today}.json")
- def _load_all(self):
- """加载所有历史记录"""
- records = []
- if os.path.exists(self.save_dir):
- for fname in sorted(os.listdir(self.save_dir), reverse=True):
- if fname.endswith(".json"):
- fpath = os.path.join(self.save_dir, fname)
- try:
- with open(fpath, "r", encoding="utf-8") as f:
- records.extend(json.load(f))
- except Exception:
- pass
- return records
- def add(self, tab, action, user_input, output):
- """添加一条对话记录并持久化"""
- record = {
- "id": len(self.records) + 1,
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
- "tab": tab,
- "action": action,
- "user_input": user_input[:200] + ("..." if len(user_input) > 200 else ""),
- "output_preview": output[:200] + ("..." if len(output) > 200 else ""),
- "output_full": output
- }
- self.records.insert(0, record) # 最新的在前
- # 追加到今日文件
- today_file = self._filepath()
- try:
- existing = []
- if os.path.exists(today_file):
- with open(today_file, "r", encoding="utf-8") as f:
- existing = json.load(f)
- existing.insert(0, record)
- with open(today_file, "w", encoding="utf-8") as f:
- json.dump(existing, f, ensure_ascii=False, indent=2)
- except Exception:
- pass
- return record
- def format_history_html(self):
- """格式化为 HTML 展示,每条记录带独立删除按钮"""
- if not self.records:
- return "<p><i>暂无对话记录,开始使用后会自动保存。</i></p>"
- lines = [f'<p style="color:#888;">共 {len(self.records)} 条记录</p>']
- for r in self.records[:50]:
- escaped_output = (r['output_full']
- .replace("&", "&")
- .replace("<", "<")
- .replace(">", ">")
- .replace("\n", "<br>")
- .replace("`", "`"))
- rid = r["id"]
- lines.append(f'''
- <div style="border:1px solid #e0e0e0; border-radius:8px; padding:12px; margin:10px 0; position:relative;">
- <div style="position:absolute; top:8px; right:8px;">
- <button onclick="document.getElementById('del_trigger').querySelector('textarea,input').value='{rid}';
- document.getElementById('del_trigger').querySelector('textarea,input').dispatchEvent(new Event('input',{{bubbles:true}}));
- document.getElementById('del_trigger').querySelector('textarea,input').dispatchEvent(new Event('change',{{bubbles:true}}));"
- style="background:#e74c3c; color:#fff; border:none; border-radius:4px; cursor:pointer; padding:4px 12px; font-size:12px;">
- ✕ 删除
- </button>
- </div>
- <div style="margin-right:70px;">
- <strong>[#{r['id']}] {r['timestamp']}</strong>
- <span style="color:#666;"> | {r['tab']} | {r['action']}</span>
- <p style="margin:6px 0 2px 0; color:#555; font-size:13px;"><b>输入:</b> {r['user_input']}</p>
- <details style="margin-top:6px;">
- <summary style="cursor:pointer; color:#2980b9;">查看完整输出</summary>
- <div style="background:#f8f9fa; padding:10px; border-radius:4px; margin-top:4px; max-height:300px; overflow-y:auto; font-size:13px; white-space:pre-wrap;">{escaped_output}</div>
- </details>
- </div>
- </div>''')
- return "\n".join(lines)
- def delete_record(self, record_id: int) -> str:
- """删除单条记录"""
- for i, r in enumerate(self.records):
- if r.get("id") == record_id:
- del self.records[i]
- # 重新持久化当天文件
- today_file = self._filepath()
- try:
- with open(today_file, "w", encoding="utf-8") as f:
- json.dump(self.records, f, ensure_ascii=False, indent=2)
- except Exception:
- pass
- return f"已删除记录 #{record_id}"
- return f"未找到记录 #{record_id}"
- def clear(self):
- """清空记录"""
- self.records = []
- for fname in os.listdir(self.save_dir):
- if fname.endswith(".json"):
- os.remove(os.path.join(self.save_dir, fname))
- return "对话记录已清空。"
- # 全局日志实例
- logger = ConversationLogger()
- # ========================================
- # 会话管理器(润色 & 大纲的对话历史)
- # ========================================
- class ChatSessionManager:
- """管理润色和大纲的多轮对话会话"""
- def __init__(self, save_dir: str):
- self.save_dir = save_dir
- os.makedirs(save_dir, exist_ok=True)
- def _filepath(self, session_id: str) -> str:
- return os.path.join(self.save_dir, f"{session_id}.json")
- def save(self, session_id: str, messages: list, title: str = ""):
- """保存会话"""
- data = {
- "id": session_id,
- "title": title or f"会话 {session_id[:8]}",
- "updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
- "messages": messages
- }
- with open(self._filepath(session_id), "w", encoding="utf-8") as f:
- json.dump(data, f, ensure_ascii=False, indent=2)
- def load(self, session_id: str) -> list:
- """加载会话,返回 messages 列表"""
- with open(self._filepath(session_id), "r", encoding="utf-8") as f:
- data = json.load(f)
- return data.get("messages", [])
- def list_sessions(self):
- """列出所有会话 [(id, title, updated), ...]"""
- sessions = []
- if os.path.exists(self.save_dir):
- for fname in sorted(os.listdir(self.save_dir), reverse=True):
- if fname.endswith(".json"):
- try:
- with open(os.path.join(self.save_dir, fname), "r", encoding="utf-8") as f:
- data = json.load(f)
- sessions.append((
- data.get("id", fname[:-5]),
- data.get("title", fname[:-5]),
- data.get("updated", "")
- ))
- except Exception:
- pass
- return sessions
- def delete(self, session_id: str):
- """删除会话"""
- path = self._filepath(session_id)
- if os.path.exists(path):
- os.remove(path)
- # 为润色和大纲各创建一个会话管理器
- polish_sessions = ChatSessionManager(os.path.join(os.path.dirname(os.path.abspath(__file__)), "outputs", "polish_sessions"))
- outline_sessions = ChatSessionManager(os.path.join(os.path.dirname(os.path.abspath(__file__)), "outputs", "outline_sessions"))
- paper_sessions = ChatSessionManager(os.path.join(os.path.dirname(os.path.abspath(__file__)), "outputs", "paper_sessions"))
- # ========================================
- # 初始化:LLM + 工具 + 智能体
- # ========================================
- config = Config(trace_enabled=False)
- llm = HelloAgentsLLM()
- tool_registry = ToolRegistry()
- tool_registry.register_tool(LiteratureSearchTool())
- tool_registry.register_tool(ArxivSearchTool())
- tool_registry.register_tool(PubMedSearchTool())
- tool_registry.register_tool(CrossRefSearchTool())
- tool_registry.register_tool(OpenAlexSearchTool())
- tool_registry.register_tool(AminerSearchTool())
- tool_registry.register_tool(PDFExtractTool())
- tool_registry.register_tool(CitationTool())
- # ---- 文献检索智能体 ----
- search_agent = SimpleAgent(
- name="文献检索助手", llm=llm, config=config,
- system_prompt="""你是一位学术文献检索专家。你有 6 个检索工具可用,请严格按用户指定的工具名称调用:
- - literature_search: Semantic Scholar,全学科覆盖(推荐)
- - aminer_search: AMiner,中文学术论文
- - openalex_search: OpenAlex,开放获取论文
- - pubmed_search: PubMed,生物医学领域
- - crossref_search: CrossRef,期刊论文元数据
- - arxiv_search: arXiv,CS/数学/物理预印本
- 规则:
- 1. 必须使用用户指定的工具搜索论文,不要用其他工具替代
- 2. 基于工具返回的真实结果进行分析和推荐
- 3. 绝对禁止在工具调用失败时凭空编造论文信息
- 4. 如果工具返回错误,直接向用户报告错误"""
- )
- # 注册全部 5 个检索工具
- search_agent.add_tool(tool_registry.get_tool("literature_search"))
- search_agent.add_tool(tool_registry.get_tool("openalex_search"))
- search_agent.add_tool(tool_registry.get_tool("pubmed_search"))
- search_agent.add_tool(tool_registry.get_tool("crossref_search"))
- search_agent.add_tool(tool_registry.get_tool("arxiv_search"))
- search_agent.add_tool(tool_registry.get_tool("aminer_search"))
- # ---- 论文总结智能体 ----
- summary_agent = SimpleAgent(
- name="论文总结助手", llm=llm, config=config,
- system_prompt="""你是一位学术论文审稿专家。请按以下结构生成总结报告:
- ## 论文信息
- ## 研究问题
- ## 方法与创新点
- ## 贡献与局限
- ## 启发与延伸
- 请使用中文输出报告,专业术语保留英文。"""
- )
- # ---- 对话智能体工厂(每次新对话创建独立实例,保持上下文记忆) ----
- def create_polish_agent():
- """创建论文润色对话智能体"""
- return SimpleAgent(
- name="论文润色助手", llm=llm, config=config,
- system_prompt="""你是资深学术论文语言编辑。以对话方式帮助用户润色论文。
- 润色原则:
- 1. 保持原意不变,仅优化表达
- 2. 改善句式结构,消除冗余
- 3. 确保逻辑连贯,统一术语
- 4. 对修改处简要说明原因
- 对话方式:用户可能多次提出修改要求(如"更正式一些"、"缩短第三段"),
- 你需要记住之前的内容和修改历史,在此基础上继续优化。"""
- )
- def create_outline_agent():
- """创建论文大纲对话智能体"""
- return SimpleAgent(
- name="大纲生成助手", llm=llm, config=config,
- system_prompt="""你是经验丰富的学术导师。以对话方式帮助用户构建论文大纲。
- 你需要:
- 1. 根据主题拆解核心章节和子主题
- 2. 为每个章节规划核心内容要点
- 3. 推荐研究方法和参考文献方向
- 对话方式:用户可能多次要求调整(如"在第三章加入实验对比"、"细化文献综述部分"),
- 你需要记住已生成的大纲内容,在此基础上修改,而不是每次重新开始。"""
- )
- def create_paper_writer_agent():
- """创建论文写作对话智能体(带文献检索能力)"""
- agent = SimpleAgent(
- name="论文写作助手", llm=llm, config=config,
- system_prompt="""你是一位学术论文写作专家。你有 6 个文献检索工具可用:
- - literature_search: Semantic Scholar,全学科文献检索(推荐优先使用)
- - aminer_search: AMiner,中文学术论文
- - openalex_search: OpenAlex,开放获取论文
- - pubmed_search: PubMed,生物医学文献
- - crossref_search: CrossRef,期刊论文
- - arxiv_search: arXiv,预印本
- 写作规则(必须严格遵守):
- 1. 根据用户提供的大纲,逐章节撰写论文
- 2. 学术化语言风格,逻辑严谨,段落清晰
- 3. **引用文献时,必须先使用检索工具搜索真实论文,只引用工具返回的真实文献**
- 4. 每引用一篇论文,必须在参考文献处标注真实信息(作者、标题、年份、期刊)
- 5. **绝对禁止**编造不存在的论文标题、作者或期刊名
- 6. 如果工具检索失败,明确告知用户"该领域文献检索失败,建议稍后重试",而不是编造文献"""
- )
- # 注册全部检索工具,确保文献来源真实
- for name in ["literature_search", "openalex_search", "pubmed_search",
- "crossref_search", "arxiv_search", "aminer_search"]:
- agent.add_tool(tool_registry.get_tool(name))
- return agent
- # ========================================
- # Gradio 回调函数(所有操作自动记录日志)
- # ========================================
- def search_papers(query, source, max_results, field, year_from, year_to):
- """文献检索 — 支持 5 大数据源"""
- if not query.strip():
- return "请输入搜索关键词。"
- # 数据源 → 工具名映射
- SOURCE_MAP = {
- "Semantic Scholar": "literature_search",
- "AMiner": "aminer_search",
- "OpenAlex": "openalex_search",
- "PubMed": "pubmed_search",
- "CrossRef": "crossref_search",
- "arXiv": "arxiv_search",
- }
- tool_name = next((v for k, v in SOURCE_MAP.items() if source.startswith(k)), "literature_search")
- source_name = next((k for k in SOURCE_MAP if source.startswith(k)), "Semantic Scholar")
- # 构建参数(高级筛选仅 Semantic Scholar 和 OpenAlex 支持)
- params_str = f"max_results={int(max_results)}"
- supports_advanced = source_name in ("Semantic Scholar", "OpenAlex", "PubMed", "CrossRef")
- if supports_advanced and field and field != "全部领域":
- params_str += f", field='{field}'"
- if supports_advanced and year_from and year_from.strip():
- params_str += f", year_from='{year_from.strip()}'"
- if supports_advanced and year_to and year_to.strip():
- params_str += f", year_to='{year_to.strip()}'"
- try:
- result = search_agent.run(
- f"请使用 {tool_name} 工具搜索以下主题的论文,然后分析结果:{query}\n"
- f"参数设置: {params_str}"
- )
- logger.add("文献检索", f"{source_name} 论文搜索", query, result)
- return result
- except Exception as e:
- err = f"检索出错: {str(e)}"
- logger.add("文献检索", f"{source_name} 搜索失败", query, err)
- return err
- def summarize_paper(content):
- """论文总结"""
- if not content.strip():
- return "请输入论文内容。"
- try:
- result = summary_agent.run(f"请对以下论文内容进行结构化总结:\n\n{content}")
- logger.add("论文总结", "结构化总结", content, result)
- return result
- except Exception as e:
- err = f"总结出错: {str(e)}"
- logger.add("论文总结", "总结失败", content, err)
- return err
- def generate_citation(title, authors, journal, year, volume, pages, doi, fmt):
- """引用生成"""
- if not title.strip() or not authors.strip():
- return "请至少填写论文标题和作者。"
- user_input = f"{title} | {authors} | {journal} | {year} | 格式: {fmt}"
- try:
- params = {
- "title": title, "authors": authors,
- "journal": journal, "year": year,
- "volume": volume, "pages": pages, "doi": doi,
- "format": fmt
- }
- resp = tool_registry.execute_tool("citation_generator", json.dumps(params))
- logger.add("引用生成", f"{fmt} 格式引用", user_input, resp.text)
- return resp.text
- except Exception as e:
- err = f"生成出错: {str(e)}"
- logger.add("引用生成", "生成失败", user_input, err)
- return err
- def polish_chat(message, history, session_id):
- """论文润色对话 — 多轮交互,自动保存会话"""
- if not message.strip():
- return "", history, session_id, _polish_sessions_dropdown()
- # 新会话自动生成 ID
- if not session_id:
- session_id = f"polish_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
- try:
- context = ""
- for msg in history:
- role = "用户" if msg["role"] == "user" else "助手"
- context += f"{role}: {msg['content']}\n"
- context += f"用户: {message}\n助手: "
- agent = create_polish_agent()
- result = agent.run(context)
- history.append({"role": "user", "content": message})
- history.append({"role": "assistant", "content": result})
- # 自动保存(用第一条用户消息做标题)
- title = history[0]["content"][:50] if history else "新对话"
- polish_sessions.save(session_id, history, title)
- logger.add("论文润色(对话)", "多轮润色", message, result)
- return "", history, session_id, _polish_sessions_dropdown()
- except Exception as e:
- err = f"润色出错: {str(e)}"
- history.append({"role": "user", "content": message})
- history.append({"role": "assistant", "content": err})
- return "", history, session_id, _polish_sessions_dropdown()
- def outline_chat(message, history, session_id):
- """大纲生成对话 — 多轮交互,自动保存会话"""
- if not message.strip():
- return "", history, session_id, _outline_sessions_dropdown()
- if not session_id:
- session_id = f"outline_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
- try:
- context = ""
- for msg in history:
- role = "用户" if msg["role"] == "user" else "助手"
- context += f"{role}: {msg['content']}\n"
- context += f"用户: {message}\n助手: "
- agent = create_outline_agent()
- result = agent.run(context)
- history.append({"role": "user", "content": message})
- history.append({"role": "assistant", "content": result})
- title = history[0]["content"][:50] if history else "新对话"
- outline_sessions.save(session_id, history, title)
- logger.add("大纲生成(对话)", "多轮大纲调整", message, result)
- return "", history, session_id, _outline_sessions_dropdown()
- except Exception as e:
- err = f"生成出错: {str(e)}"
- history.append({"role": "user", "content": message})
- history.append({"role": "assistant", "content": err})
- return "", history, session_id, _outline_sessions_dropdown()
- def _polish_choices():
- sessions = polish_sessions.list_sessions()
- return [(f"{t} ({u})", sid) for sid, t, u in sessions]
- def _outline_choices():
- sessions = outline_sessions.list_sessions()
- return [(f"{t} ({u})", sid) for sid, t, u in sessions]
- def _paper_choices():
- sessions = paper_sessions.list_sessions()
- return [(f"{t} ({u})", sid) for sid, t, u in sessions]
- def _polish_sessions_dropdown():
- """润色会话列表 → gr.update"""
- choices = _polish_choices()
- return gr.update(choices=choices, value=None if not choices else choices[0][1])
- def _outline_sessions_dropdown():
- """大纲会话列表 → gr.update"""
- choices = _outline_choices()
- return gr.update(choices=choices, value=None if not choices else choices[0][1])
- def _paper_sessions_dropdown():
- """论文写作会话列表 → gr.update"""
- choices = _paper_choices()
- return gr.update(choices=choices, value=None if not choices else choices[0][1])
- def clear_polish_chat():
- """重置润色对话"""
- return "", [], "", _polish_sessions_dropdown()
- def clear_outline_chat():
- """重置大纲对话"""
- return "", [], "", _outline_sessions_dropdown()
- def load_polish_session(session_id):
- """加载润色历史会话到 chatbot"""
- if not session_id:
- return [], session_id, _polish_sessions_dropdown()
- try:
- messages = polish_sessions.load(session_id)
- return messages, session_id, _polish_sessions_dropdown()
- except Exception:
- return [], "", _polish_sessions_dropdown()
- def load_outline_session(session_id):
- """加载大纲历史会话到 chatbot"""
- if not session_id:
- return [], session_id, _outline_sessions_dropdown()
- try:
- messages = outline_sessions.load(session_id)
- return messages, session_id, _outline_sessions_dropdown()
- except Exception:
- return [], "", _outline_sessions_dropdown()
- def delete_polish_session(session_id):
- """删除润色历史会话"""
- if session_id:
- polish_sessions.delete(session_id)
- return [], "", _polish_sessions_dropdown()
- def delete_outline_session(session_id):
- """删除大纲历史会话"""
- if session_id:
- outline_sessions.delete(session_id)
- return [], "", _outline_sessions_dropdown()
- # ========================================
- # 论文写作回调(对话模式 + DOCX 下载)
- def paper_write_chat(message, history, session_id):
- """论文写作对话 — 多轮交互,自动保存"""
- if not message.strip():
- return "", history, session_id, _paper_sessions_dropdown()
- if not session_id:
- session_id = f"paper_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
- try:
- context = ""
- for msg in history:
- role = "用户" if msg["role"] == "user" else "助手"
- context += f"{role}: {msg['content']}\n"
- context += f"用户: {message}\n助手: "
- agent = create_paper_writer_agent()
- result = agent.run(context)
- history.append({"role": "user", "content": message})
- history.append({"role": "assistant", "content": result})
- title = history[0]["content"][:50] if history else "新对话"
- paper_sessions.save(session_id, history, title)
- logger.add("论文写作(对话)", "多轮写作", message, result)
- return "", history, session_id, _paper_sessions_dropdown()
- except Exception as e:
- err = f"写作出错: {str(e)}"
- history.append({"role": "user", "content": message})
- history.append({"role": "assistant", "content": err})
- return "", history, session_id, _paper_sessions_dropdown()
- def load_paper_session(session_id):
- """加载论文写作历史会话"""
- if not session_id:
- return [], session_id, _paper_sessions_dropdown()
- try:
- messages = paper_sessions.load(session_id)
- return messages, session_id, _paper_sessions_dropdown()
- except Exception:
- return [], "", _paper_sessions_dropdown()
- def delete_paper_session(session_id):
- """删除论文写作历史会话"""
- if session_id:
- paper_sessions.delete(session_id)
- return [], "", _paper_sessions_dropdown()
- def clear_paper_chat():
- """重置论文写作对话"""
- return "", [], "", _paper_sessions_dropdown()
- def extract_pdf(pdf_file, max_chars):
- """PDF 文本提取"""
- if pdf_file is None:
- return "请上传一个 PDF 文件。"
- try:
- resp = tool_registry.execute_tool("pdf_extract", json.dumps({
- "file_path": pdf_file.name,
- "max_chars": int(max_chars)
- }))
- logger.add("PDF 提取", "PDF 文本提取", f"文件: {pdf_file.name}", resp.text)
- return resp.text
- except Exception as e:
- err = f"提取出错: {str(e)}"
- logger.add("PDF 提取", "提取失败", f"文件: {pdf_file.name}", err)
- return err
- def refresh_history():
- """刷新对话记录显示"""
- return logger.format_history_html()
- def delete_history_record(record_id):
- """删除指定编号的记录(由 HTML 按钮触发)"""
- if not record_id:
- return logger.format_history_html()
- try:
- rid = int(record_id)
- logger.delete_record(rid)
- return logger.format_history_html()
- except (ValueError, TypeError):
- return logger.format_history_html()
- def clear_history():
- """清空对话记录"""
- msg = logger.clear()
- return msg
- # ========================================
- # Gradio UI 布局
- # ========================================
- THEME = gr.themes.Soft(primary_hue="blue", secondary_hue="slate")
- with gr.Blocks(title="PaperAssistant - 智能论文助手") as demo:
- gr.Markdown("""
- # 🎓 PaperAssistant - 智能论文助手
- ### 基于 HelloAgents 框架 + DeepSeek 的多智能体论文学术辅助工具
- """)
- with gr.Tab("📚 文献检索"):
- with gr.Row():
- with gr.Column(scale=3):
- search_input = gr.Textbox(
- label="研究主题",
- placeholder="支持中英文关键词,如:气候变化对农业的影响、cancer immunotherapy...",
- lines=2
- )
- with gr.Row():
- search_source = gr.Dropdown(
- choices=[
- "Semantic Scholar(全学科推荐)",
- "AMiner(中文论文强项)",
- "OpenAlex(开放获取综合)",
- "PubMed(生物医学)",
- "CrossRef(期刊论文)",
- "arXiv(CS/数学/物理)"
- ],
- value="Semantic Scholar(全学科推荐)",
- label="数据源"
- )
- max_results = gr.Slider(1, 10, value=5, step=1, label="返回论文数")
- # 高级筛选(仅 Semantic Scholar 支持)
- with gr.Accordion("高级筛选", open=False):
- search_field = gr.Dropdown(
- choices=["全部领域"] + [
- "计算机科学", "人工智能", "医学", "生物学", "物理学", "化学",
- "数学", "经济学", "心理学", "社会学", "语言学", "哲学",
- "工程", "环境科学", "材料科学", "教育学", "法学", "商学"
- ],
- value="全部领域",
- label="学科领域"
- )
- with gr.Row():
- year_from = gr.Textbox(label="起始年份", placeholder="2020", scale=1)
- year_to = gr.Textbox(label="截止年份", placeholder="2025", scale=1)
- search_btn = gr.Button("🔍 开始检索", variant="primary")
- with gr.Column(scale=7):
- search_output = gr.Markdown(label="检索结果", value="*等待搜索...*")
- search_btn.click(
- fn=search_papers,
- inputs=[search_input, search_source, max_results, search_field, year_from, year_to],
- outputs=search_output
- )
- with gr.Tab("📝 论文总结"):
- with gr.Row():
- with gr.Column(scale=4):
- summary_input = gr.Textbox(
- label="论文内容(粘贴标题、作者、摘要等信息)",
- placeholder="粘贴论文信息,包括标题、作者、摘要、方法描述...",
- lines=15
- )
- summary_btn = gr.Button("📝 生成总结", variant="primary")
- with gr.Column(scale=6):
- summary_output = gr.Markdown(label="总结报告", value="*等待输入...*")
- summary_btn.click(
- fn=summarize_paper,
- inputs=[summary_input],
- outputs=summary_output
- )
- with gr.Tab("📎 引用生成"):
- with gr.Row():
- with gr.Column(scale=4):
- cite_title = gr.Textbox(label="论文标题 *", placeholder="Attention Is All You Need")
- cite_authors = gr.Textbox(label="作者 *", placeholder="Vaswani, A., Shazeer, N., Parmar, N., et al.")
- with gr.Row():
- cite_journal = gr.Textbox(label="期刊/会议", placeholder="NeurIPS")
- cite_year = gr.Textbox(label="年份", placeholder="2017")
- with gr.Row():
- cite_volume = gr.Textbox(label="卷号", placeholder="30")
- cite_pages = gr.Textbox(label="页码", placeholder="5998-6008")
- cite_doi = gr.Textbox(label="DOI(可选)")
- cite_format = gr.Radio(
- choices=["gbt7714", "apa", "mla"],
- value="gbt7714",
- label="引用格式"
- )
- cite_btn = gr.Button("📎 生成引用", variant="primary")
- with gr.Column(scale=6):
- cite_output = gr.Textbox(label="生成的引用", lines=8)
- cite_btn.click(
- fn=generate_citation,
- inputs=[cite_title, cite_authors, cite_journal, cite_year,
- cite_volume, cite_pages, cite_doi, cite_format],
- outputs=cite_output
- )
- with gr.Tab("✍️ 论文润色"):
- # 当前会话 ID(隐藏)
- polish_session_id = gr.State(value="")
- # 历史会话面板
- with gr.Accordion("📋 历史会话", open=False):
- with gr.Row():
- polish_history_list = gr.Dropdown(
- label="历史对话", choices=_polish_choices(), scale=6,
- info="选择一条历史会话后点击加载,可继续对话"
- )
- polish_load_btn = gr.Button("📂 加载", variant="primary", size="sm", scale=1)
- polish_del_btn = gr.Button("🗑️ 删除", variant="stop", size="sm", scale=1)
- # 对话区
- gr.Markdown("粘贴文本后可以持续对话: 说'更正式一些'、'缩短第三段'等,我会记住上下文。")
- polish_chatbot = gr.Chatbot(label="润色对话", height=450)
- with gr.Row():
- polish_msg = gr.Textbox(
- label="输入修改要求",
- placeholder="例如:请润色这段文字... / 把第二段改得更学术化...",
- scale=7
- )
- polish_send = gr.Button("发送", variant="primary", scale=1)
- polish_clear = gr.Button("🗑️ 开始新对话", size="sm", variant="stop")
- # 事件绑定
- polish_send.click(
- fn=polish_chat,
- inputs=[polish_msg, polish_chatbot, polish_session_id],
- outputs=[polish_msg, polish_chatbot, polish_session_id, polish_history_list]
- )
- polish_msg.submit(
- fn=polish_chat,
- inputs=[polish_msg, polish_chatbot, polish_session_id],
- outputs=[polish_msg, polish_chatbot, polish_session_id, polish_history_list]
- )
- polish_clear.click(
- fn=clear_polish_chat,
- outputs=[polish_msg, polish_chatbot, polish_session_id, polish_history_list]
- )
- polish_load_btn.click(
- fn=load_polish_session,
- inputs=[polish_history_list],
- outputs=[polish_chatbot, polish_session_id, polish_history_list]
- )
- polish_del_btn.click(
- fn=delete_polish_session,
- inputs=[polish_history_list],
- outputs=[polish_chatbot, polish_session_id, polish_history_list]
- )
- with gr.Tab("📊 大纲生成"):
- # 当前会话 ID(隐藏)
- outline_session_id = gr.State(value="")
- # 历史会话面板
- with gr.Accordion("📋 历史会话", open=False):
- with gr.Row():
- outline_history_list = gr.Dropdown(
- label="历史对话", choices=_outline_choices(), scale=6,
- info="选择一条历史会话后点击加载,可继续对话"
- )
- outline_load_btn = gr.Button("📂 加载", variant="primary", size="sm", scale=1)
- outline_del_btn = gr.Button("🗑️ 删除", variant="stop", size="sm", scale=1)
- # 对话区
- gr.Markdown("输入论文主题后,可以持续对话优化: 说'细化第三章'、'增加实验对比章节'等,我会记住已有大纲并在此基础上修改。")
- outline_chatbot = gr.Chatbot(label="大纲对话", height=450)
- with gr.Row():
- outline_msg = gr.Textbox(
- label="输入要求",
- placeholder="例如:我想写一篇关于XX的毕业论文,帮我生成大纲...",
- scale=7
- )
- outline_send = gr.Button("发送", variant="primary", scale=1)
- outline_clear = gr.Button("🗑️ 开始新对话", size="sm", variant="stop")
- # 事件绑定
- outline_send.click(
- fn=outline_chat,
- inputs=[outline_msg, outline_chatbot, outline_session_id],
- outputs=[outline_msg, outline_chatbot, outline_session_id, outline_history_list]
- )
- outline_msg.submit(
- fn=outline_chat,
- inputs=[outline_msg, outline_chatbot, outline_session_id],
- outputs=[outline_msg, outline_chatbot, outline_session_id, outline_history_list]
- )
- outline_clear.click(
- fn=clear_outline_chat,
- outputs=[outline_msg, outline_chatbot, outline_session_id, outline_history_list]
- )
- outline_load_btn.click(
- fn=load_outline_session,
- inputs=[outline_history_list],
- outputs=[outline_chatbot, outline_session_id, outline_history_list]
- )
- outline_del_btn.click(
- fn=delete_outline_session,
- inputs=[outline_history_list],
- outputs=[outline_chatbot, outline_session_id, outline_history_list]
- )
- with gr.Tab("📝 论文写作"):
- paper_session_id = gr.State(value="")
- with gr.Accordion("📋 历史会话", open=False):
- with gr.Row():
- paper_history_list = gr.Dropdown(
- label="历史对话", choices=_paper_choices(), scale=6,
- info="选择历史会话后加载,可继续写作"
- )
- paper_load_btn = gr.Button("📂 加载", variant="primary", size="sm", scale=1)
- paper_del_btn = gr.Button("🗑️ 删除", variant="stop", size="sm", scale=1)
- gr.Markdown("根据大纲逐章撰写论文。粘贴大纲后说'开始写第一章',可持续对话调整内容。")
- paper_chatbot = gr.Chatbot(label="论文写作对话", height=450)
- with gr.Row():
- paper_msg = gr.Textbox(
- label="输入写作要求",
- placeholder="例如:以下是论文大纲...请从摘要开始撰写 / 写第三章实验部分 / 这部分再详细一些...",
- scale=7
- )
- paper_send = gr.Button("发送", variant="primary", scale=1)
- with gr.Row():
- paper_clear = gr.Button("🗑️ 开始新对话", size="sm", variant="stop")
- paper_send.click(
- fn=paper_write_chat,
- inputs=[paper_msg, paper_chatbot, paper_session_id],
- outputs=[paper_msg, paper_chatbot, paper_session_id, paper_history_list]
- )
- paper_msg.submit(
- fn=paper_write_chat,
- inputs=[paper_msg, paper_chatbot, paper_session_id],
- outputs=[paper_msg, paper_chatbot, paper_session_id, paper_history_list]
- )
- paper_clear.click(
- fn=clear_paper_chat,
- outputs=[paper_msg, paper_chatbot, paper_session_id, paper_history_list]
- )
- paper_load_btn.click(
- fn=load_paper_session,
- inputs=[paper_history_list],
- outputs=[paper_chatbot, paper_session_id, paper_history_list]
- )
- paper_del_btn.click(
- fn=delete_paper_session,
- inputs=[paper_history_list],
- outputs=[paper_chatbot, paper_session_id, paper_history_list]
- )
- with gr.Tab("📄 PDF → Markdown"):
- gr.Markdown("上传 PDF 论文,自动识别标题、章节、段落,输出为格式化的 **Markdown** 文本。")
- with gr.Row():
- with gr.Column(scale=4):
- pdf_input = gr.File(label="上传 PDF 文件", file_types=[".pdf"])
- pdf_max_chars = gr.Slider(0, 100000, value=0, step=1000,
- label="字符上限(0=不限制)")
- pdf_btn = gr.Button("📄 转换为 Markdown", variant="primary")
- with gr.Column(scale=6):
- pdf_output = gr.Code(label="Markdown 输出", language="markdown", lines=20)
- pdf_btn.click(
- fn=extract_pdf,
- inputs=[pdf_input, pdf_max_chars],
- outputs=pdf_output
- )
- with gr.Tab("💬 对话记录"):
- # 操作按钮(页面顶部)
- with gr.Row():
- refresh_btn = gr.Button("🔄 刷新", size="sm")
- clear_btn = gr.Button("🗑️ 清空全部", size="sm", variant="stop")
- # 隐藏触发组件:删除按钮通过 JS 填充此字段
- delete_trigger = gr.Textbox(visible=False, elem_id="del_trigger")
- # 历史展示(HTML 格式,每条带删除按钮)
- history_display = gr.HTML(value=logger.format_history_html())
- refresh_btn.click(fn=refresh_history, outputs=history_display)
- clear_btn.click(fn=clear_history, outputs=history_display)
- delete_trigger.change(
- fn=delete_history_record,
- inputs=[delete_trigger],
- outputs=[history_display]
- )
- gr.Markdown("""
- ---
- ### 👤 作者: [@chengH425](https://github.com/chengH425) | 🙏 感谢 Datawhale 社区和 Hello-Agents 项目
- """)
- if __name__ == "__main__":
- demo.launch(server_name="127.0.0.1", server_port=7860, share=False, theme=THEME)
|