Spaces:
Runtime error
Runtime error
import os | |
import gradio as gr | |
import logging | |
from typing import List, Dict, Any | |
# 配置日志 | |
logging.basicConfig( | |
level=logging.INFO, | |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' | |
) | |
logger = logging.getLogger(__name__) | |
# 从环境变量中获取 API Key | |
def get_api_key() -> str: | |
api_key = os.environ.get("OPENAI_API_KEY") | |
if not api_key: | |
logger.error("环境变量中未找到 OPENAI_API_KEY") | |
raise ValueError("环境变量中未找到 OPENAI_API_KEY,请先设置。") | |
return api_key | |
# 初始化 LLM | |
def initialize_llm(api_key: str): | |
try: | |
from langchain_openai.chat_models import ChatOpenAI | |
logger.info("初始化 LLM...") | |
return ChatOpenAI( | |
temperature=0, | |
model="gpt-4o-mini", | |
api_key=api_key | |
) | |
except Exception as e: | |
logger.error(f"初始化 LLM 失败: {str(e)}") | |
raise | |
# 定义 Prompt 模板 | |
def initialize_prompt_template(): | |
from langchain_core.prompts import PromptTemplate | |
template_text = """请根据以下 context 回答问题,答案请使用 Markdown 格式输出。 | |
如果 context 存在latex表达式,请正确书写。 | |
如果 context 中包含图表链接,请在回答中原封不动地加入图表,并在每个包含"/images"的链接前添加前缀 "https://huggingface.co/spaces/zliang/palynogeology/resolve/main"。 | |
Question: {question} | |
Context: {context}""" | |
return PromptTemplate( | |
input_variables=["question", "context"], | |
template=template_text | |
) | |
# 将检索到的文档内容格式化为字符串 | |
def format_docs(docs) -> str: | |
return "\n\n".join(doc.page_content for doc in docs) | |
# 加载本地 FAISS 向量库 | |
def initialize_vectorstore(api_key: str): | |
try: | |
from langchain_openai import OpenAIEmbeddings | |
from langchain_community.vectorstores import FAISS | |
logger.info("初始化向量存储...") | |
embed = OpenAIEmbeddings( | |
model="text-embedding-3-small", | |
api_key=api_key | |
) | |
if not os.path.exists("faiss_db"): | |
logger.error("未找到 'faiss_db' 目录") | |
raise FileNotFoundError("未找到 'faiss_db' 目录,请先构建向量库。") | |
return FAISS.load_local("faiss_db", embed, allow_dangerous_deserialization=True) | |
except Exception as e: | |
logger.error(f"初始化向量存储失败: {str(e)}") | |
raise | |
# 构建检索器 | |
def initialize_retriever(db): | |
return db.as_retriever( | |
search_type="mmr", | |
search_kwargs={"score_threshold": 0.5, "k": 3} | |
) | |
# 构造问答链 | |
def initialize_qa_chain(llm, prompt, retriever): | |
from langchain_core.runnables import RunnablePassthrough | |
from langchain_core.output_parsers import StrOutputParser | |
qa_chain = ( | |
{ | |
"context": retriever | format_docs, | |
"question": RunnablePassthrough(), | |
} | |
| prompt | |
| llm | |
| StrOutputParser() | |
) | |
return qa_chain | |
# 根据用户输入的问题调用问答链 | |
def answer_question(qa_chain, question: str) -> str: | |
try: | |
logger.info(f"处理问题: {question}") | |
answer = qa_chain.invoke(question) | |
return answer | |
except Exception as e: | |
logger.error(f"调用问答链时出错: {str(e)}", exc_info=True) | |
return "⚠️ 出错了,请稍后重试。如果问题持续存在,请联系管理员。" | |
# 返回《孢粉地质学》书籍简介 | |
def get_book_introduction() -> str: | |
introduction = """ | |
# 《孢粉地质学》 | |
 | |
## 简介 | |
《孢粉地质学》是一本系统介绍孢粉与孢子在地质学中应用的重要专著。本书由国内顶尖专家编撰,融合了最新的研究成果和实践经验。 | |
## 内容亮点 | |
- **孢粉与孢子的形成与保存** | |
阐述孢粉在沉积环境中的生成过程及保存机制。 | |
- **鉴定与分类方法** | |
详细介绍如何通过形态学、化学特征对孢粉进行鉴定与分类。 | |
- **地层对比与古环境重建** | |
探讨孢粉在地层划分、沉积环境重建、古气候研究等方面的应用。 | |
- **案例分析与实践应用** | |
配合丰富的实例解析,为读者提供理论与实践相结合的指导。 | |
## 适读人群 | |
本书适合地质学、古生物学及相关领域的研究人员和学生参考,不仅为学术研究提供了坚实的理论基础,同时也为野外勘查和实际应用提供了实用工具。 | |
👉 欢迎在"书籍问答"标签页中提问,获取更多关于书中内容的详细解读! | |
""" | |
return introduction | |
# 定义常见问题列表 | |
def get_faq_list() -> List[Dict[str, str]]: | |
return [ | |
{"question": "孢粉是什么?", "category": "基础概念"}, | |
{"question": "如何采集孢粉样本?", "category": "实验方法"}, | |
{"question": "孢粉分析的主要步骤有哪些?", "category": "实验方法"}, | |
{"question": "如何鉴定孢粉的年代?", "category": "年代学"}, | |
{"question": "孢粉数据如何应用于地层对比?", "category": "地层学"}, | |
{"question": "常见的孢粉类型有哪些?", "category": "分类学"}, | |
{"question": "孢粉如何指示古气候变化?", "category": "古环境"}, | |
{"question": "孢粉研究在石油勘探中的应用", "category": "应用领域"}, | |
{"question": "孢粉与孢子有什么区别?", "category": "基础概念"}, | |
{"question": "电子显微镜在孢粉研究中的应用", "category": "技术方法"} | |
] | |
# 构建自定义 Gradio 界面 | |
def create_custom_ui(qa_chain): | |
# 主题配置 | |
theme = gr.Theme( | |
primary_hue="green", | |
secondary_hue="emerald", | |
neutral_hue="gray", | |
font=[gr.themes.GoogleFont("Source Sans Pro"), "system-ui", "sans-serif"], | |
) | |
# 自定义CSS | |
custom_css = """ | |
.container {max-width: 1000px; margin: auto;} | |
.header-logo {text-align: center; padding: 20px 0; margin-bottom: 20px;} | |
.header-title {font-size: 2.5rem; font-weight: 700; margin: 0.5rem 0;} | |
.header-subtitle {font-size: 1.25rem; color: #555; margin-bottom: 1rem;} | |
.footer {text-align: center; margin-top: 40px; padding: 20px 0; font-size: 0.9rem; color: #666;} | |
.card {border-radius: 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); padding: 20px; margin-bottom: 20px; background-color: white;} | |
.faq-item {cursor: pointer; padding: 10px; border-radius: 5px; margin: 5px 0; transition: all 0.2s;} | |
.faq-item:hover {background-color: #f0f9f0;} | |
.category-tag {display: inline-block; font-size: 0.8rem; padding: 2px 8px; border-radius: 12px; background-color: #e0f2e0; color: #2e7d32; margin-left: 10px;} | |
.loading-indicator {display: flex; justify-content: center; align-items: center; height: 100px;} | |
.sample-questions-header {font-weight: 600; margin: 15px 0 10px 0;} | |
.search-box {margin-bottom: 15px;} | |
/* 自定义滚动条 */ | |
::-webkit-scrollbar {width: 8px; height: 8px;} | |
::-webkit-scrollbar-track {background: #f1f1f1; border-radius: 10px;} | |
::-webkit-scrollbar-thumb {background: #c1e0c1; border-radius: 10px;} | |
::-webkit-scrollbar-thumb:hover {background: #8bc48b;} | |
/* 响应式调整 */ | |
@media (max-width: 768px) { | |
.header-title {font-size: 2rem;} | |
.header-subtitle {font-size: 1rem;} | |
} | |
""" | |
with gr.Blocks(theme=theme, css=custom_css) as demo: | |
# 注入 KaTeX 样式表以支持LaTeX公式渲染 | |
gr.HTML( | |
'<link rel="stylesheet" ' | |
'href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css" ' | |
'integrity="sha384-GvrOXuhMATgEsSwCs4smul74iXGOixntILdUW9XmUC6+HX0sLNAK3q71HotJqlAn" ' | |
'crossorigin="anonymous">' | |
) | |
# 应用标题和介绍 | |
with gr.Row(elem_classes="header-logo"): | |
gr.HTML(""" | |
<div style="text-align: center;"> | |
<h1 class="header-title">《孢粉地质学》数字化知识库</h1> | |
<p class="header-subtitle">探索孢粉地质学的奥秘,获取专业知识解答</p> | |
</div> | |
""") | |
# 主内容区域 | |
with gr.Tabs() as tabs: | |
# 书籍简介标签页 | |
with gr.TabItem("📚 书籍简介", id="intro"): | |
with gr.Box(elem_classes="card"): | |
gr.Markdown(get_book_introduction()) | |
# 书籍问答标签页 | |
with gr.TabItem("❓ 知识问答", id="qa"): | |
with gr.Row(): | |
# 左侧:问答区域 | |
with gr.Column(scale=7): | |
with gr.Box(elem_classes="card"): | |
gr.Markdown("### 📝 提问区") | |
with gr.Row(): | |
question_input = gr.Textbox( | |
lines=3, | |
placeholder="请输入您关于孢粉地质学的问题...", | |
label="问题", | |
elem_classes="search-box" | |
) | |
with gr.Row(): | |
submit_button = gr.Button("提交问题", variant="primary") | |
clear_button = gr.Button("清空", variant="secondary") | |
with gr.Box(): | |
with gr.Row(): | |
status_indicator = gr.Markdown("准备就绪,等待提问...") | |
with gr.Box(elem_classes="card", visible=False) as answer_card: | |
gr.Markdown("### 🔍 回答结果") | |
answer_output = gr.Markdown( | |
label="回答", | |
elem_id="answer-output", | |
latex_delimiters=[ | |
{"left": "$", "right": "$", "display": False}, | |
{"left": "$$", "right": "$$", "display": True} | |
] | |
) | |
# 右侧:常见问题 | |
with gr.Column(scale=3): | |
with gr.Box(elem_classes="card"): | |
gr.Markdown("### 📋 常见问题") | |
# 分类筛选下拉框 | |
category_filter = gr.Dropdown( | |
["全部", "基础概念", "实验方法", "分类学", "年代学", "地层学", "古环境", "应用领域", "技术方法"], | |
value="全部", | |
label="按类别筛选" | |
) | |
faq_container = gr.HTML() # 用于显示FAQ的容器 | |
# 更新FAQ显示的函数 | |
def update_faq_display(category): | |
faq_list = get_faq_list() | |
html = "<div class='faq-list'>" | |
for item in faq_list: | |
if category == "全部" or item["category"] == category: | |
html += f""" | |
<div class='faq-item' onclick='document.querySelector("[data-testid=textbox]").value = this.getAttribute("data-question"); document.querySelector("[data-testid=button]").click()' data-question='{item["question"]}'> | |
{item["question"]} | |
<span class='category-tag'>{item["category"]}</span> | |
</div> | |
""" | |
html += "</div>" | |
return html | |
# 绑定更新事件 | |
category_filter.change(update_faq_display, inputs=[category_filter], outputs=[faq_container]) | |
# 使用指南标签页 | |
with gr.TabItem("📖 使用指南", id="guide"): | |
with gr.Box(elem_classes="card"): | |
gr.Markdown(""" | |
# 使用指南 | |
## 🔍 如何有效提问 | |
为了获得最准确的回答,建议您: | |
1. **使用专业术语** - 尽量使用孢粉学和地质学的专业术语 | |
2. **具体明确** - 问题越具体,回答越精准 | |
3. **一次一问** - 每次提交一个问题,而不是多个问题组合 | |
4. **参考示例** - 可以参考右侧的常见问题示例 | |
## 📊 功能介绍 | |
本平台提供以下功能: | |
- **书籍内容检索** - 快速获取《孢粉地质学》中的知识点 | |
- **专业问题解答** - 解答孢粉学相关的各类专业问题 | |
- **图例与公式** - 支持显示专业图例和数学公式 | |
- **常见问题库** - 提供常见问题的快速访问 | |
## ⚠️ 注意事项 | |
- 本平台不替代专业人士的建议 | |
- 复杂图表可能需要等待较长时间加载 | |
- 若遇到技术问题,请刷新页面或稍后再试 | |
""") | |
# 页脚 | |
with gr.Row(elem_classes="footer"): | |
gr.HTML(""" | |
<div> | |
<p>© 2025 孢粉地质学数字平台 | 由 GPT-4o 提供支持</p> | |
<p>如有问题或建议,请联系我们</p> | |
</div> | |
""") | |
# 函数:处理问题提交 | |
def process_question(question): | |
if not question or question.strip() == "": | |
return ("请输入有效的问题!", gr.update(visible=False)) | |
status = "🔍 正在检索相关知识..." | |
yield (status, gr.update(visible=False)) | |
try: | |
# 调用问答链获取答案 | |
answer = answer_question(qa_chain, question) | |
status = "✅ 回答已生成" | |
# 显示答案卡片 | |
return (status, gr.update(visible=True, value=answer)) | |
except Exception as e: | |
logger.error(f"处理问题时出错: {str(e)}") | |
status = "❌ 出错了:" + str(e) | |
return (status, gr.update(visible=False)) | |
# 函数:清空输入和结果 | |
def clear_input(): | |
return "", "准备就绪,等待提问...", gr.update(visible=False) | |
# 事件绑定 | |
submit_button.click( | |
process_question, | |
inputs=[question_input], | |
outputs=[status_indicator, answer_card] | |
) | |
clear_button.click( | |
clear_input, | |
inputs=[], | |
outputs=[question_input, status_indicator, answer_card] | |
) | |
# 初始化FAQ显示 | |
demo.load( | |
update_faq_display, | |
inputs=[category_filter], | |
outputs=[faq_container] | |
) | |
return demo | |
def main(): | |
try: | |
logger.info("启动应用...") | |
api_key = get_api_key() | |
llm = initialize_llm(api_key) | |
prompt = initialize_prompt_template() | |
db = initialize_vectorstore(api_key) | |
retriever = initialize_retriever(db) | |
qa_chain = initialize_qa_chain(llm, prompt, retriever) | |
app = create_custom_ui(qa_chain) | |
app.launch(share=False) | |
except Exception as e: | |
logger.critical(f"应用启动失败: {str(e)}", exc_info=True) | |
print(f"错误: {str(e)}") | |
print("请检查日志以获取详细信息。") | |
if __name__ == "__main__": | |
main() |