agentbio / app.py
a96560575's picture
Update app.py
10287da verified
Raw
History Blame Contribute Delete
28.9 kB
import os
import time
import json
import gradio as gr
from neo4j import GraphDatabase
from langchain_community.graphs import Neo4jGraph
from langchain.chains import GraphCypherQAChain
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_community.utilities import WikipediaAPIWrapper
from langchain_community.tools import WikipediaQueryRun
from pyvis.network import Network
import html
import tempfile
BIOCHAR_QUERY_TEMPLATES = """
Biochar remediation query patterns:
1. Pollutant <- treats - Biochar
MATCH (b:Biochar)-[r:TREATS]->(p:Pollutant)
WHERE toLower(p.name) CONTAINS toLower($keyword) OR $keyword IN coalesce(p.aliases, [])
RETURN b.name, type(r), p.name
2. Biochar -> prepared_by -> PreparationMethod
MATCH (b:Biochar)-[:PREPARED_BY]->(m:PreparationMethod)
WHERE toLower(b.name) CONTAINS toLower($keyword) OR $keyword IN coalesce(b.aliases, [])
RETURN b.name, m.name
3. Biochar -> has_property -> Property
MATCH (b:Biochar)-[:HAS_PROPERTY]->(prop:Property)
WHERE toLower(b.name) CONTAINS toLower($keyword) OR $keyword IN coalesce(b.aliases, [])
RETURN b.name, prop.name
4. Biochar / Pollutant -> removes_via -> Mechanism
MATCH (n)-[:REMOVES_VIA]->(m:Mechanism)
WHERE (n:Biochar OR n:Pollutant)
RETURN n.name, labels(n), m.name
5. Biochar -> derived_from -> Feedstock
MATCH (b:Biochar)-[:DERIVED_FROM]->(f:Feedstock)
RETURN b.name, f.name
6. Biochar / ApplicationScenario -> applied_in -> EnvironmentMedium
MATCH (n)-[:APPLIED_IN]->(env:EnvironmentMedium)
WHERE n:Biochar OR n:ApplicationScenario
RETURN n.name, env.name
7. Biochar / PreparationMethod / ApplicationScenario -> performs_under -> Condition
MATCH (n)-[:PERFORMS_UNDER]->(c:Condition)
RETURN n.name, labels(n), c.name
8. Biochar / Property -> characterized_by -> CharacterizationMethod
MATCH (n)-[:CHARACTERIZED_BY]->(cm:CharacterizationMethod)
WHERE n:Biochar OR n:Property
RETURN n.name, cm.name
"""
BIOCHAR_CYPHER_TEMPLATE = """You are an expert Neo4j Cypher generator for a biochar environmental remediation knowledge graph.
Task:
- Generate a read-only Cypher query that answers the user's question.
- Use only the relationship types and node labels present in the provided schema.
- Prefer precise, compact queries over broad graph expansion.
Domain priorities:
- Most important paths:
Biochar-[:TREATS]->Pollutant
Biochar-[:PREPARED_BY]->PreparationMethod
Biochar-[:HAS_PROPERTY]->Property
Biochar-[:REMOVES_VIA]->Mechanism
Biochar-[:DERIVED_FROM]->Feedstock
Biochar-[:APPLIED_IN]->EnvironmentMedium
Biochar-[:PERFORMS_UNDER]->Condition
Biochar|Property-[:CHARACTERIZED_BY]->CharacterizationMethod
- `Chunk` nodes are evidence containers. Use them only when the question asks for evidence, source text, or supporting context.
- Prefer matching on `name`; when helpful, also consider `aliases`.
- When the user asks "which", "what", "list", or "show", return distinct names and a small number of relevant supporting fields.
- When the user asks about mechanisms, prioritize `REMOVES_VIA`.
- When the user asks about synthesis or modification, prioritize `PREPARED_BY` and `DERIVED_FROM`.
- When the user asks about material characteristics, prioritize `HAS_PROPERTY`.
- When the user asks about environmental application, prioritize `APPLIED_IN` and `PERFORMS_UNDER`.
Query rules:
- Return at most 15 rows unless the user explicitly asks for more.
- Use `DISTINCT` whenever duplicates are likely.
- Do not write, delete, merge, call procedures, or use APOC.
- Do not invent labels or relationships.
- If multiple hop paths are needed, keep them biologically and chemically meaningful.
- If the question names a specific pollutant or biochar, filter with `toLower(n.name) CONTAINS toLower("...")`
and optionally `ANY(alias IN coalesce(n.aliases, []) WHERE toLower(alias) CONTAINS toLower("..."))`.
Helpful query patterns:
{query_templates}
Schema:
{schema}
Question:
{question}
Return only the Cypher query text.
"""
def extract_visualization_keywords(message, api_key, base_url):
llm_kwargs = {"model": "gpt-4o-mini", "temperature": 0, "openai_api_key": api_key}
if base_url and base_url.strip():
llm_kwargs["base_url"] = base_url.strip()
llm = ChatOpenAI(**llm_kwargs)
prompt = f"""你要为知识图谱可视化提取检索关键词。
用户问题可能是中文、英文或中英混合。请尽量返回图谱中最可能存在的实体关键词候选,优先返回英文学术名称;如果原问题里有中文术语,请同时保留中文候选。
候选可以来自这些类别:
- 污染物
- 生物炭材料
- 原料
- 制备方法
- 性质
- 机理
要求:
1. 返回 1 到 3 个候选短语。
2. 优先返回文献和知识图谱中最可能使用的标准英文名称。
3. 如果问题是中文,尽量给出对应英文术语。
4. 只返回 JSON,不要解释。
输出格式:
{{"keywords": ["candidate 1", "candidate 2"]}}
问题:{message}
"""
content = llm.invoke(prompt).content.strip()
try:
payload = json.loads(content)
keywords = payload.get("keywords") or []
keywords = [str(k).strip() for k in keywords if str(k).strip()]
except Exception:
keywords = [content]
keywords.append(message.strip())
normalized = []
seen = set()
for keyword in keywords:
if keyword and keyword not in seen:
seen.add(keyword)
normalized.append(keyword)
return normalized[:4]
def rewrite_question_for_graph_search(message, api_key, base_url):
llm_kwargs = {"model": "gpt-4o-mini", "temperature": 0, "openai_api_key": api_key}
if base_url and base_url.strip():
llm_kwargs["base_url"] = base_url.strip()
llm = ChatOpenAI(**llm_kwargs)
prompt = f"""你要把用户问题改写成更适合英文知识图谱检索的问句。
要求:
1. 保留原问题语义,不要扩展没提到的事实。
2. 如果原问题是中文,请优先改写为简洁、标准的英文科研问句。
3. 把中文术语改成更可能出现在知识图谱节点里的英文表达。
4. 保留核心实体,例如污染物、生物炭材料、原料、制备方法、性质、机理。
5. 只返回改写后的单句问句,不要解释。
示例:
- 哪些生物炭可以处理镉、铅或砷等污染物?
-> Which biochars can treat pollutants such as cadmium, lead, or arsenic?
- 稻壳生物炭常见的制备方法和关键条件有哪些?
-> What are the common preparation methods and key conditions for rice husk biochar?
原问题:{message}
"""
return llm.invoke(prompt).content.strip()
def extract_wikipedia_keyword(message, api_key, base_url):
llm_kwargs = {"model": "gpt-4o-mini", "temperature": 0, "openai_api_key": api_key}
if base_url and base_url.strip():
llm_kwargs["base_url"] = base_url.strip()
llm = ChatOpenAI(**llm_kwargs)
prompt = f"""请为 Wikipedia 检索提取一个最具体的实体关键词。
要求:
1. 如果原问题是中文,尽量输出对应的标准英文术语。
2. 只输出一个词或一个短语。
3. 不要输出宽泛类别词,如 Pollutant、Property、Material。
4. 不要解释。
问题:{message}
"""
return llm.invoke(prompt).content.strip()
# ==========================================
# 1. Read Database Credentials
# ==========================================
NEO4J_URI = os.environ.get("NEO4J_URI", "")
NEO4J_USERNAME = os.environ.get("NEO4J_USERNAME", "")
NEO4J_PASSWORD = os.environ.get("NEO4J_PASSWORD", "")
DEFAULT_BASE_URL = ""
NEO4J_DATABASE_NAME = "f8c5f809"
# ==========================================
# 2. Database Initialization (LangChain RAG)
# ==========================================
try:
graph = Neo4jGraph(
url=NEO4J_URI,
username=NEO4J_USERNAME,
password=NEO4J_PASSWORD,
database=NEO4J_DATABASE_NAME
)
print("✅ LangChain Graph RAG connected successfully!")
except Exception as e:
print(f"❌ LangChain Graph RAG connection failed: {e}")
graph = None
# Native driver for visualization
neo4j_driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USERNAME, NEO4J_PASSWORD))
# ==========================================
# 3. Dynamic Graph Chain Initialization
# ==========================================
def get_graph_chain(api_key: str, base_url: str):
# 动态组装大模型参数,支持自定义 Base URL
llm_kwargs = {
"model": "gpt-4o-mini",
"temperature": 0,
"openai_api_key": api_key
}
if base_url and base_url.strip():
llm_kwargs["base_url"] = base_url.strip()
llm = ChatOpenAI(**llm_kwargs)
cypher_prompt = PromptTemplate(
template=BIOCHAR_CYPHER_TEMPLATE,
input_variables=["schema", "question"],
partial_variables={"query_templates": BIOCHAR_QUERY_TEMPLATES}
)
qa_template = """你是一名严谨的生物炭环境修复、环境工程与材料学专家。
请严格基于图数据库返回的 [Context] 作答。
[关键规则]:
1. 尽最大努力使用给定 Context。即使上下文稍微凌乱,也要尽量提取其中与生物炭、污染物、原料、制备方法、性质、机理、环境介质相关的信息。
2. 回答要围绕生物炭环境修复展开,尽量区分处理对象、制备方式、材料性质和修复机理。
3. 除非上下文明确把某设备当作表征方法,否则不要把纯实验辅助设备写进答案。
4. 只有当 Context 完全为空时,才允许输出完全等于 "Not found"。只要有任何数据,就必须组织出有用答案。
5. 最终请用专业、自然、清晰的中文回答。
Context: {context}
Question: {question}
中文专业回答:"""
qa_prompt = PromptTemplate(template=qa_template, input_variables=["context", "question"])
kg_rag_chain = GraphCypherQAChain.from_llm(
llm=llm,
graph=graph,
verbose=True,
qa_prompt=qa_prompt,
cypher_prompt=cypher_prompt,
top_k=15,
allow_dangerous_requests=True
)
return kg_rag_chain
# ==========================================
# 4. 🕸️ Core Visualization Function: Generate Pyvis HTML
# ==========================================
def generate_vis_subgraph_html(message, api_key, base_url):
if not neo4j_driver:
return "⚠️ 图数据库未连接"
if not api_key or not api_key.startswith("sk-"):
return "<h3>⚠️ 未填写 API Key,无法生成可视化结果</h3>"
# Extract keyword candidates using LLM
try:
keywords = extract_visualization_keywords(message, api_key, base_url)
except Exception as e:
keywords = [message]
print(f"Keyword extraction failed: {e}")
if not keywords:
keywords = [message]
print(f"🔍 Extracted graph keywords for visualization: {keywords}")
vis_cypher = """
MATCH (core_entity)
WHERE any(keyword IN $keywords WHERE
toLower(coalesce(core_entity.name, "")) CONTAINS toLower(keyword)
OR toLower(coalesce(core_entity.id, "")) CONTAINS toLower(keyword)
OR toLower(coalesce(core_entity.canonical_name, "")) CONTAINS toLower(keyword)
OR any(alias IN coalesce(core_entity.aliases, []) WHERE toLower(alias) CONTAINS toLower(keyword))
)
MATCH (core_entity)-[r]-(neighbor)
RETURN core_entity, r, neighbor
LIMIT 60
"""
color_map = {
'Biochar': '#7C5C3B',
'Pollutant': '#E76F51',
'Feedstock': '#8AB17D',
'PreparationMethod': '#E9C46A',
'Property': '#4EA8DE',
'Mechanism': '#9D4EDD',
'EnvironmentMedium': '#2A9D8F',
'Condition': '#F4A261',
'CharacterizationMethod': '#7B8CDE',
'ApplicationScenario': '#B56576',
'Chunk': '#E5E7E9'
}
nodes = {}
edges = []
try:
with neo4j_driver.session(database=NEO4J_DATABASE_NAME) as session:
result = session.run(vis_cypher, keywords=keywords)
for record in result:
core_node = record['core_entity']
rel = record['r']
neighbor_node = record['neighbor']
core_id = core_node.element_id
if core_id not in nodes:
core_label = list(core_node.labels)[0]
core_name = core_node.get('name', core_node.get('id', 'Unknown'))
nodes[core_id] = {
'id': core_id,
'label': core_name,
'title': f"Label: {core_label}\n{json.dumps(dict(core_node), indent=2, ensure_ascii=False)}",
'color': color_map.get(core_label, '#D2E5FF')
}
neighbor_id = neighbor_node.element_id
if neighbor_id not in nodes:
neighbor_label = list(neighbor_node.labels)[0]
if neighbor_label == 'Chunk':
raw_name = neighbor_node.get('text', '')[:12] + '...'
else:
raw_name = neighbor_node.get('name', neighbor_node.get('id', 'Unknown'))
nodes[neighbor_id] = {
'id': neighbor_id,
'label': raw_name,
'title': f"Label: {neighbor_label}\n{json.dumps(dict(neighbor_node), indent=2, ensure_ascii=False)}",
'color': color_map.get(neighbor_label, '#D2E5FF')
}
rel_type = rel.type
edges.append((rel.start_node.element_id, rel.end_node.element_id, rel_type))
except Exception as e:
print(f"❌ 可视化查询失败: {str(e)}")
return f"<h3>❌ 可视化关系查询失败:{str(e)}</h3>"
if not nodes:
return f"<h3>⚠️ 检索完成,但没有找到可视化关系网络。</h3><p>💡 当前尝试匹配的关键词为:<b>{', '.join(keywords)}</b>。这可能意味着这些关键词未命中图谱中的节点名称或别名,或相关节点尚未建立一阶关系。</p>"
net = Network(height='600px', width='100%', bgcolor='#ffffff', font_color='#333333', notebook=False)
for node_id, node_data in nodes.items():
net.add_node(node_data['id'], label=node_data['label'], title=node_data['title'], color=node_data['color'])
for edge in edges:
net.add_edge(edge[0], edge[1], label=edge[2], width=1, color='#DDDDDD')
net.toggle_physics(True)
net.set_options("""
var options = {
"interaction": {
"hover": true,
"hoverConnectedEdges": true
}
}
""")
path = tempfile.mktemp(suffix='.html')
net.save_graph(path)
with open(path, 'r', encoding='utf-8') as f:
html_content = f.read()
escaped_html = html.escape(html_content)
return f'<iframe style="width: 100%; height: 600px; border: none;" srcdoc="{escaped_html}"></iframe>'
# ==========================================
# 5. Q&A Function (Graph Priority + Wiki Fallback)
# ==========================================
def answer_question(message, history, api_key, base_url):
history.append({"role": "user", "content": message})
if not api_key or not api_key.startswith("sk-"):
history.append({"role": "assistant", "content": "⚠️ 请先在上方填写有效的 LLM API Key。"})
yield history
return
if graph is None:
history.append({"role": "assistant", "content": "⚠️ 图数据库连接失败,请检查环境变量配置。"})
yield history
return
try:
chain = get_graph_chain(api_key, base_url)
graph_query = rewrite_question_for_graph_search(message, api_key, base_url)
history.append({"role": "assistant", "content": "🧠 正在优先检索生物炭修复知识图谱,请稍候..."})
yield history
response = chain.invoke({"query": graph_query})
final_answer = response["result"]
if "Not found" in final_answer:
history[-1]["content"] = "🧠 图谱中暂未命中,正在补充检索 Wikipedia..."
yield history
keyword = extract_wikipedia_keyword(message, api_key, base_url)
wiki_wrapper = WikipediaAPIWrapper(lang="en", top_k_results=1, doc_content_chars_max=800)
wiki_tool = WikipediaQueryRun(api_wrapper=wiki_wrapper)
wiki_result = wiki_tool.run(keyword)
if "No good Wikipedia Search Result" in wiki_result or not wiki_result.strip():
final_answer = f"图谱中没有找到相关信息,Wikipedia 中也没有检索到 [{keyword}] 的合适结果。"
else:
final_answer = f"**图谱中未找到该实体。以下是 Wikipedia 中关于 [{keyword}] 的基础说明:**\n\n{wiki_result}"
streamed_text = ""
for char in final_answer:
streamed_text += char
history[-1]["content"] = streamed_text
yield history
time.sleep(0.005)
except Exception as e:
history[-1]["content"] = f"处理过程中出现系统错误:{str(e)}"
yield history
def get_schema():
if graph is None:
return "⚠️ 图数据库未连接"
live_schema = str(graph.schema or "").strip()
if not live_schema:
return "⚠️ 当前未读取到实时 schema。可能原因包括:图数据库中尚无数据、连接已建立但 schema introspection 暂未返回结果,或当前库尚未完成导入。"
return live_schema
# ==========================================
# 6. UI Construction (Professional Sans-serif Theme)
# ==========================================
custom_theme = gr.themes.Soft(
font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
text_size=gr.themes.sizes.text_md
)
custom_css = """
:root {
--paper: #f6f2e8;
--ink: #1e2a22;
--muted: #5d665e;
--sand: #e9dfc8;
--clay: #7c5c3b;
--leaf: #365f48;
--water: #d9e9e8;
--panel: rgba(255, 252, 246, 0.92);
}
.gradio-container {
background:
radial-gradient(circle at top right, rgba(54, 95, 72, 0.12), transparent 26%),
radial-gradient(circle at left center, rgba(124, 92, 59, 0.10), transparent 24%),
linear-gradient(180deg, #f7f4ee 0%, #efe5d4 100%);
}
.app-shell {
max-width: 1440px;
margin: 0 auto;
}
.hero-banner {
background: linear-gradient(135deg, rgba(250, 248, 243, 0.96), rgba(232, 223, 203, 0.92));
border: 1px solid rgba(124, 92, 59, 0.18);
border-radius: 28px;
padding: 28px 30px;
box-shadow: 0 18px 40px rgba(67, 49, 28, 0.10);
}
.hero-kicker {
display: inline-block;
padding: 6px 12px;
border-radius: 999px;
background: rgba(54, 95, 72, 0.10);
color: var(--leaf);
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 700;
}
.hero-title {
margin: 14px 0 10px;
font-size: 42px;
line-height: 1.08;
color: var(--ink);
font-weight: 800;
}
.hero-subtitle {
margin: 0;
max-width: 860px;
color: var(--muted);
font-size: 16px;
line-height: 1.75;
}
.metric-strip {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-top: 18px;
}
.metric-card {
border-radius: 18px;
padding: 16px 18px;
background: rgba(255,255,255,0.55);
border: 1px solid rgba(54, 95, 72, 0.12);
}
.metric-label {
font-size: 12px;
color: #6a7067;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.metric-value {
margin-top: 8px;
font-size: 20px;
font-weight: 700;
color: #223228;
}
.workspace-row {
gap: 20px;
}
.side-panel, .main-panel {
background: var(--panel);
border: 1px solid rgba(54, 95, 72, 0.14);
border-radius: 26px;
box-shadow: 0 16px 36px rgba(44, 34, 20, 0.08);
}
.side-panel {
padding: 20px;
}
.main-panel {
padding: 22px;
}
.panel-title {
font-size: 14px;
color: #617064;
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 8px;
}
.panel-headline {
font-size: 26px;
color: var(--ink);
font-weight: 800;
margin-bottom: 8px;
}
.panel-copy {
color: var(--muted);
line-height: 1.7;
font-size: 14px;
margin-bottom: 16px;
}
.gradio-container .gr-button-primary {
background: linear-gradient(135deg, #355f49, #4e7b60);
border: none !important;
}
.gradio-container .gr-button-secondary {
border-color: rgba(54, 95, 72, 0.18) !important;
}
.viz-intro {
padding: 14px 16px;
border-radius: 16px;
background: rgba(233, 223, 200, 0.55);
color: #58452f;
font-size: 13px;
line-height: 1.7;
margin-bottom: 10px;
}
.path-card {
margin-top: 18px;
padding: 18px;
border-radius: 22px;
background:
radial-gradient(circle at top right, rgba(54, 95, 72, 0.10), transparent 38%),
linear-gradient(160deg, rgba(241, 235, 221, 0.95), rgba(232, 220, 196, 0.92));
border: 1px solid rgba(124, 92, 59, 0.14);
}
.path-title {
font-size: 12px;
color: #72685a;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 8px;
}
.path-headline {
font-size: 22px;
font-weight: 800;
color: #26342a;
margin-bottom: 10px;
}
.path-copy {
font-size: 14px;
line-height: 1.75;
color: #57584f;
margin-bottom: 14px;
}
.path-steps {
display: grid;
gap: 10px;
}
.path-step {
padding: 12px 14px;
border-radius: 16px;
background: rgba(255,255,255,0.52);
border: 1px solid rgba(54, 95, 72, 0.10);
}
.path-step strong {
display: block;
color: #304036;
margin-bottom: 4px;
font-size: 14px;
}
.path-step span {
color: #61655b;
font-size: 13px;
line-height: 1.65;
}
@media (max-width: 900px) {
.hero-title {
font-size: 34px;
}
.metric-strip {
grid-template-columns: 1fr;
}
}
"""
with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
with gr.Column(elem_classes=["app-shell"]):
gr.HTML("""
<div class="hero-banner">
<div class="hero-kicker">Designed by Zhou's Group</div>
<div class="hero-title">生物炭环境修复动态资源库</div>
<p class="hero-subtitle">
支持用户围绕污染物、生物炭、制备方法、基础性质与修复机理做连续探索。
</p>
</div>
""")
with gr.Row(elem_classes=["workspace-row"]):
with gr.Column(scale=4, elem_classes=["side-panel"]):
gr.HTML("""
<div class="panel-title">控制台</div>
<div class="panel-headline">连接、提示与图谱总览</div>
<div class="panel-copy">
请先填写凭证,再从示例问题或自定义问题开始。
</div>
""")
api_key_input = gr.Textbox(
label="🔑 API Key",
placeholder="sk-...",
type="password"
)
base_url_input = gr.Textbox(
label="🌐 Base URL",
placeholder="https://api.openai.com/v1",
value=DEFAULT_BASE_URL
)
gr.Markdown("""
**提问模板**
1. 哪些生物炭可以处理镉、铅或砷?
2. 稻壳生物炭常见的制备方法和条件有哪些?
3. 某类改性生物炭通常有哪些基础性质?
4. 生物炭去除磷酸盐可能涉及哪些机理?
5. 哪些表征方法常用于分析生物炭结构?
""")
gr.HTML("""
<div class="path-card">
<div class="path-title">研究路径</div>
<div class="path-headline">建议的探索顺序</div>
<div class="path-copy">
如果你想连续追问并快速得到更稳定的图谱结果,可以按下面这条路径逐步展开。
</div>
<div class="path-steps">
<div class="path-step">
<strong>先问污染物</strong>
<span>先锁定镉、砷、磷酸盐、抗生素等目标污染物,找到相关生物炭材料。</span>
</div>
<div class="path-step">
<strong>再问材料与制备</strong>
<span>继续追问原料、热解方式、改性手段和关键条件,补全材料侧信息。</span>
</div>
<div class="path-step">
<strong>最后问性质与机理</strong>
<span>查看表面性质、作用机理和子图关系,确认知识连接是否完整。</span>
</div>
</div>
</div>
""")
with gr.Column(scale=8, elem_classes=["main-panel"]):
gr.HTML("""
<div class="panel-title">研究工作区</div>
<div class="panel-headline">问答与关系导航</div>
<div class="panel-copy">
专注于连续提问、结果阅读和关系网络浏览。每次提交问题后,系统会先回答,再自动生成相关实体的一阶子图。
</div>
""")
chatbot_ui = gr.Chatbot(
avatar_images=("user.png", "ai.png"),
height=430,
render=True
)
msg_input = gr.Textbox(
label="输入问题",
placeholder="例如:哪些生物炭可以处理镉、铅或砷等污染物?",
lines=2,
max_lines=4
)
with gr.Row():
submit_btn = gr.Button("开始分析", variant="primary")
clear_btn = gr.Button("清空当前会话", variant="secondary")
with gr.Tabs():
with gr.Tab("关系子图"):
gr.HTML("""
<div class="viz-intro">
当前问题最相关的关系网络。支持拖拽、缩放和查看节点属性,适合快速确认某一污染物、生物炭或机理在图谱中的连接方式。
</div>
""")
html_vis_output = gr.HTML(
value="<h3>💬 请输入凭证并提交问题...</h3><p>系统会在这里绘制与问题关联最紧密的动态子图。</p>",
height="640px",
render=True
)
with gr.Tab("提问建议"):
gr.Markdown(
"\n".join(
[
"1. 问污染物:哪些生物炭可以处理镉、砷、磷酸盐?",
"2. 问材料:某种改性生物炭的制备方法、原料和性质是什么?",
"3. 问机理:某类污染物去除可能涉及哪些机制?",
"4. 问条件:某种处理效果在什么 pH、温度或投加量下表现更好?",
"5. 问表征:哪些方法常用于分析生物炭结构与表面性质?",
]
)
)
# ==========================================
# Wiring logic (现在包含了 base_url_input)
# ==========================================
answer_event = msg_input.submit(
fn=answer_question,
inputs=[msg_input, chatbot_ui, api_key_input, base_url_input],
outputs=[chatbot_ui]
)
answer_event.then(
fn=generate_vis_subgraph_html,
inputs=[msg_input, api_key_input, base_url_input],
outputs=[html_vis_output]
)
answer_event.then(lambda: "", outputs=[msg_input])
btn_event = submit_btn.click(
fn=answer_question,
inputs=[msg_input, chatbot_ui, api_key_input, base_url_input],
outputs=[chatbot_ui]
)
btn_event.then(
fn=generate_vis_subgraph_html,
inputs=[msg_input, api_key_input, base_url_input],
outputs=[html_vis_output]
)
btn_event.then(lambda: "", outputs=[msg_input])
clear_btn.click(lambda: [], outputs=chatbot_ui, queue=False)
clear_btn.click(lambda: "<h3>💬 请输入一个生物炭修复相关问题...</h3>", outputs=html_vis_output)
if __name__ == "__main__":
demo.launch(theme=custom_theme)