Spaces:
Running
Running
Upload app.py
Browse files
app.py
CHANGED
|
@@ -107,73 +107,34 @@ def inject_custom_css():
|
|
| 107 |
}
|
| 108 |
/* 减少标题上方空白 */
|
| 109 |
.block-container { padding-top: 1.5rem !important; }
|
| 110 |
-
|
| 111 |
-
/* 固定标题区域 */
|
| 112 |
-
.main-title {
|
| 113 |
-
position: sticky !important;
|
| 114 |
-
top: 0 !important;
|
| 115 |
-
background: white !important;
|
| 116 |
-
z-index: 100 !important;
|
| 117 |
-
padding: 0.5rem 0 !important;
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
/* 固定搜索框在底部 */
|
| 121 |
-
[data-testid="stChatInput"] {
|
| 122 |
-
position: fixed !important;
|
| 123 |
-
bottom: 1rem !important;
|
| 124 |
-
left: 50% !important;
|
| 125 |
-
transform: translateX(-50%) !important;
|
| 126 |
-
width: 90% !important;
|
| 127 |
-
max-width: 800px !important;
|
| 128 |
-
z-index: 99 !important;
|
| 129 |
-
background: white !important;
|
| 130 |
-
padding: 0.5rem !important;
|
| 131 |
-
border-radius: 8px !important;
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
/* 给主内容区底部留出搜索框空间 */
|
| 135 |
-
.main .block-container {
|
| 136 |
-
padding-bottom: 5rem !important;
|
| 137 |
-
}
|
| 138 |
</style>
|
| 139 |
""", unsafe_allow_html=True)
|
| 140 |
|
| 141 |
|
| 142 |
inject_custom_css()
|
| 143 |
-
|
| 144 |
-
# =========================
|
| 145 |
-
# 标题和欢迎页一起渲染(优化加载体验)
|
| 146 |
-
# =========================
|
| 147 |
-
# 标题区域 - 使用 main-title class 固定
|
| 148 |
st.markdown(
|
| 149 |
"""
|
| 150 |
-
<
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
</h1>
|
| 154 |
-
</div>
|
| 155 |
""",
|
| 156 |
unsafe_allow_html=True,
|
| 157 |
)
|
| 158 |
|
| 159 |
# 欢迎页占位(登录后若有消息则清除)
|
| 160 |
_welcome_hero = st.empty()
|
| 161 |
-
|
| 162 |
-
#
|
| 163 |
-
if not st.session_state.get("current_user") and not st.session_state.get("messages"):
|
| 164 |
_welcome_hero.markdown(
|
| 165 |
"""
|
| 166 |
-
<div style="display:flex; align-items:center; justify-content:center; min-height:
|
| 167 |
<div style="text-align:center; color:#888;">
|
| 168 |
-
<div style="font-size:
|
| 169 |
-
<div style="font-size:
|
| 170 |
-
<div style="font-size:
|
| 171 |
🌐 <b>联网模式</b> —— 大模型 + 网络搜索,实时回答<br>
|
| 172 |
📚 <b>知识库模式</b> —— 上传文档,基于私有知识回答
|
| 173 |
</div>
|
| 174 |
-
<div style="margin-top:12px; font-size:13px; color:#999;">
|
| 175 |
-
🙋 当前为<b>游客模式</b>,可直接在下方提问体验
|
| 176 |
-
</div>
|
| 177 |
</div>
|
| 178 |
</div>
|
| 179 |
""",
|
|
@@ -374,7 +335,6 @@ with st.sidebar:
|
|
| 374 |
st.session_state.login_attempts = 0
|
| 375 |
st.session_state.current_user = input_username
|
| 376 |
st.session_state.current_role = role
|
| 377 |
-
st.toast(f"✅ 登录成功,欢迎 {input_username}!")
|
| 378 |
st.rerun()
|
| 379 |
|
| 380 |
else: # 注册
|
|
@@ -392,7 +352,6 @@ with st.sidebar:
|
|
| 392 |
st.session_state.current_user = reg_user
|
| 393 |
st.session_state.current_role = "user"
|
| 394 |
st.session_state.login_attempts = 0
|
| 395 |
-
st.toast(f"✅ 注册成功,欢迎 {reg_user}!")
|
| 396 |
st.rerun()
|
| 397 |
else:
|
| 398 |
st.error(f"❌ {msg}")
|
|
@@ -724,9 +683,10 @@ SYSTEM_PROMPT = (
|
|
| 724 |
)
|
| 725 |
|
| 726 |
SYSTEM_PROMPT_WEB = (
|
| 727 |
-
"你是一个
|
| 728 |
-
"
|
| 729 |
-
"如果搜索结果不足以回答问题,请
|
|
|
|
| 730 |
)
|
| 731 |
|
| 732 |
|
|
@@ -958,7 +918,8 @@ else:
|
|
| 958 |
|
| 959 |
|
| 960 |
with st.sidebar:
|
| 961 |
-
|
|
|
|
| 962 |
# --- 模型设置(游客和登录用户均可用) ---
|
| 963 |
with st.expander("⚙️ 模型设置"):
|
| 964 |
_model_list = list(_active_models.keys())
|
|
@@ -1165,13 +1126,10 @@ with st.sidebar:
|
|
| 1165 |
hist_tab_new, hist_tab_history = st.tabs(["当前对话", "历史记录"])
|
| 1166 |
|
| 1167 |
with hist_tab_new:
|
| 1168 |
-
st.caption("清空当前对话(
|
| 1169 |
if st.button("🧹 清空当前对话", use_container_width=True, type="secondary", key="btn_clear_chat"):
|
| 1170 |
st.session_state.messages = []
|
| 1171 |
-
st.toast("当前对话已清空")
|
| 1172 |
-
st.rerun()
|
| 1173 |
|
| 1174 |
-
st.divider()
|
| 1175 |
st.caption("清空所有历史记录(不可恢复)")
|
| 1176 |
if st.button("🗑️ 清空全部记录", use_container_width=True, type="secondary", key="btn_clear_all_hist"):
|
| 1177 |
_async_run(_clear_chat_history_db, CURRENT_USER)
|
|
@@ -1222,14 +1180,19 @@ def llm_answer(query, context_docs, selected_display_name, web_enabled):
|
|
| 1222 |
all_context = ""
|
| 1223 |
curr_time = datetime.now().strftime("%Y-%m-%d %H:%M")
|
| 1224 |
|
| 1225 |
-
if context_docs:
|
| 1226 |
-
all_context += "【知识库资料】:\n" + "\n".join(context_docs) + "\n"
|
| 1227 |
-
|
| 1228 |
if web_enabled:
|
|
|
|
| 1229 |
search_res = web_search(query)
|
| 1230 |
-
all_context
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1231 |
|
| 1232 |
-
prompt_content = f"当前时间:{curr_time}\n\n参考资料:\n{all_context[:6500]}\n\n用户问题:{query}"
|
| 1233 |
input_tokens = estimate_tokens(prompt_content)
|
| 1234 |
|
| 1235 |
or_client = get_or_client()
|
|
@@ -1258,10 +1221,8 @@ def llm_answer(query, context_docs, selected_display_name, web_enabled):
|
|
| 1258 |
if selected_id != p_id:
|
| 1259 |
retry_queue.append((p_client, p_id, f"💰 收费兜底-{p_label}"))
|
| 1260 |
|
| 1261 |
-
# 联网模式和知识库模式使用不同的 system prompt
|
| 1262 |
-
_system_prompt = SYSTEM_PROMPT_WEB if web_enabled else SYSTEM_PROMPT
|
| 1263 |
messages = [
|
| 1264 |
-
{"role": "system", "content":
|
| 1265 |
{"role": "user", "content": prompt_content},
|
| 1266 |
]
|
| 1267 |
|
|
@@ -1323,9 +1284,25 @@ if not st.session_state.get("messages"):
|
|
| 1323 |
else:
|
| 1324 |
st.session_state.messages = []
|
| 1325 |
|
| 1326 |
-
# 有消息
|
| 1327 |
if st.session_state.messages:
|
| 1328 |
_welcome_hero.empty()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1329 |
|
| 1330 |
|
| 1331 |
@st.fragment
|
|
@@ -1336,48 +1313,24 @@ def _chat_fragment():
|
|
| 1336 |
_top_k = st.session_state.get("sel_topk", 5)
|
| 1337 |
_threshold = st.session_state.get("sel_threshold", 0.25)
|
| 1338 |
|
| 1339 |
-
#
|
|
|
|
| 1340 |
_has_msgs = bool(st.session_state.messages)
|
| 1341 |
-
|
| 1342 |
-
# 消息区域 - 固定高度,可滚动
|
| 1343 |
-
if _has_msgs:
|
| 1344 |
-
# 固定高度的消息容器(可滚动)
|
| 1345 |
-
msg_container = st.container(height=480)
|
| 1346 |
-
with msg_container:
|
| 1347 |
-
for m in st.session_state.messages:
|
| 1348 |
-
with st.chat_message(m["role"]):
|
| 1349 |
-
st.markdown(m["content"])
|
| 1350 |
-
if "meta" in m:
|
| 1351 |
-
st.caption(m["meta"])
|
| 1352 |
-
|
| 1353 |
-
# 自动滚动到底部
|
| 1354 |
-
st.markdown("""
|
| 1355 |
-
u003cscriptu003e
|
| 1356 |
-
(function() {
|
| 1357 |
-
const containers = document.querySelectorAll('[data-testid="stVerticalBlock"]');
|
| 1358 |
-
containers.forEach(c => {
|
| 1359 |
-
if (c.scrollHeight u003e c.clientHeight) {
|
| 1360 |
-
c.scrollTop = c.scrollHeight;
|
| 1361 |
-
}
|
| 1362 |
-
});
|
| 1363 |
-
})();
|
| 1364 |
-
u003c/scriptu003e
|
| 1365 |
-
""", unsafe_allow_html=True)
|
| 1366 |
-
else:
|
| 1367 |
-
# 无消息时不占高度
|
| 1368 |
-
msg_container = st.container()
|
| 1369 |
-
|
| 1370 |
-
# 搜索框固定在底部
|
| 1371 |
q = st.chat_input("输入问题...", key="chat_input_v3")
|
| 1372 |
|
| 1373 |
-
|
| 1374 |
-
|
| 1375 |
-
|
| 1376 |
-
|
| 1377 |
-
|
| 1378 |
-
|
| 1379 |
-
|
| 1380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1381 |
with st.chat_message("user"):
|
| 1382 |
st.markdown(q)
|
| 1383 |
|
|
@@ -1385,6 +1338,7 @@ def _chat_fragment():
|
|
| 1385 |
container = st.empty()
|
| 1386 |
|
| 1387 |
# 联网模式:大模型 + 网络搜索,不查知识库
|
|
|
|
| 1388 |
if _web_on:
|
| 1389 |
container.markdown("*🌐 正在联网搜索...*")
|
| 1390 |
relevant_docs = []
|
|
|
|
| 107 |
}
|
| 108 |
/* 减少标题上方空白 */
|
| 109 |
.block-container { padding-top: 1.5rem !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
</style>
|
| 111 |
""", unsafe_allow_html=True)
|
| 112 |
|
| 113 |
|
| 114 |
inject_custom_css()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
st.markdown(
|
| 116 |
"""
|
| 117 |
+
<h1 style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-size:clamp(1.4rem, 5vw, 2.2rem); margin:0 0 0.5rem; text-align:center;">
|
| 118 |
+
🤖 智答 AI 助手
|
| 119 |
+
</h1>
|
|
|
|
|
|
|
| 120 |
""",
|
| 121 |
unsafe_allow_html=True,
|
| 122 |
)
|
| 123 |
|
| 124 |
# 欢迎页占位(登录后若有消息则清除)
|
| 125 |
_welcome_hero = st.empty()
|
| 126 |
+
if not st.session_state.get("current_user"):
|
| 127 |
+
# 未登录:欢迎页(不含游客模式提示,避免闪现)
|
|
|
|
| 128 |
_welcome_hero.markdown(
|
| 129 |
"""
|
| 130 |
+
<div style="display:flex; align-items:center; justify-content:center; min-height:50vh;">
|
| 131 |
<div style="text-align:center; color:#888;">
|
| 132 |
+
<div style="font-size:52px; margin-bottom:16px;">🤖</div>
|
| 133 |
+
<div style="font-size:20px; font-weight:600; margin-bottom:10px;">欢迎使用智答 AI 助手</div>
|
| 134 |
+
<div style="font-size:15px; line-height:2;">
|
| 135 |
🌐 <b>联网模式</b> —— 大模型 + 网络搜索,实时回答<br>
|
| 136 |
📚 <b>知识库模式</b> —— 上传文档,基于私有知识回答
|
| 137 |
</div>
|
|
|
|
|
|
|
|
|
|
| 138 |
</div>
|
| 139 |
</div>
|
| 140 |
""",
|
|
|
|
| 335 |
st.session_state.login_attempts = 0
|
| 336 |
st.session_state.current_user = input_username
|
| 337 |
st.session_state.current_role = role
|
|
|
|
| 338 |
st.rerun()
|
| 339 |
|
| 340 |
else: # 注册
|
|
|
|
| 352 |
st.session_state.current_user = reg_user
|
| 353 |
st.session_state.current_role = "user"
|
| 354 |
st.session_state.login_attempts = 0
|
|
|
|
| 355 |
st.rerun()
|
| 356 |
else:
|
| 357 |
st.error(f"❌ {msg}")
|
|
|
|
| 683 |
)
|
| 684 |
|
| 685 |
SYSTEM_PROMPT_WEB = (
|
| 686 |
+
"你是一个智能AI助手,擅长结合互联网搜索结果回答用户问题。"
|
| 687 |
+
"请根据搜索结果提供准确、有条理的回答。"
|
| 688 |
+
"如果搜索结果不足以回答问题,请结合你自身的知识进行补充。"
|
| 689 |
+
"回答要简洁实用,注明信息来源。"
|
| 690 |
)
|
| 691 |
|
| 692 |
|
|
|
|
| 918 |
|
| 919 |
|
| 920 |
with st.sidebar:
|
| 921 |
+
if not IS_GUEST:
|
| 922 |
+
st.caption("👇 展开下方面板管理知识库和设置")
|
| 923 |
# --- 模型设置(游客和登录用户均可用) ---
|
| 924 |
with st.expander("⚙️ 模型设置"):
|
| 925 |
_model_list = list(_active_models.keys())
|
|
|
|
| 1126 |
hist_tab_new, hist_tab_history = st.tabs(["当前对话", "历史记录"])
|
| 1127 |
|
| 1128 |
with hist_tab_new:
|
| 1129 |
+
st.caption("清空当前对话(数据库记录保留)")
|
| 1130 |
if st.button("🧹 清空当前对话", use_container_width=True, type="secondary", key="btn_clear_chat"):
|
| 1131 |
st.session_state.messages = []
|
|
|
|
|
|
|
| 1132 |
|
|
|
|
| 1133 |
st.caption("清空所有历史记录(不可恢复)")
|
| 1134 |
if st.button("🗑️ 清空全部记录", use_container_width=True, type="secondary", key="btn_clear_all_hist"):
|
| 1135 |
_async_run(_clear_chat_history_db, CURRENT_USER)
|
|
|
|
| 1180 |
all_context = ""
|
| 1181 |
curr_time = datetime.now().strftime("%Y-%m-%d %H:%M")
|
| 1182 |
|
|
|
|
|
|
|
|
|
|
| 1183 |
if web_enabled:
|
| 1184 |
+
# 联网模式:只用网络搜索,不用知识库
|
| 1185 |
search_res = web_search(query)
|
| 1186 |
+
all_context = f"【互联网搜索结果】:\n{search_res}"
|
| 1187 |
+
prompt_content = f"当前时间:{curr_time}\n\n{all_context[:6500]}\n\n用户问题:{query}"
|
| 1188 |
+
system_prompt = SYSTEM_PROMPT_WEB
|
| 1189 |
+
else:
|
| 1190 |
+
# 知识库模式:用知识库资料
|
| 1191 |
+
if context_docs:
|
| 1192 |
+
all_context = "【知识库资料】:\n" + "\n".join(context_docs)
|
| 1193 |
+
prompt_content = f"当前时间:{curr_time}\n\n参考资料:\n{all_context[:6500]}\n\n用户问题:{query}"
|
| 1194 |
+
system_prompt = SYSTEM_PROMPT
|
| 1195 |
|
|
|
|
| 1196 |
input_tokens = estimate_tokens(prompt_content)
|
| 1197 |
|
| 1198 |
or_client = get_or_client()
|
|
|
|
| 1221 |
if selected_id != p_id:
|
| 1222 |
retry_queue.append((p_client, p_id, f"💰 收费兜底-{p_label}"))
|
| 1223 |
|
|
|
|
|
|
|
| 1224 |
messages = [
|
| 1225 |
+
{"role": "system", "content": system_prompt},
|
| 1226 |
{"role": "user", "content": prompt_content},
|
| 1227 |
]
|
| 1228 |
|
|
|
|
| 1284 |
else:
|
| 1285 |
st.session_state.messages = []
|
| 1286 |
|
| 1287 |
+
# 登录后处理欢迎页:有消息则清除,无消息则显示(不含登录提示)
|
| 1288 |
if st.session_state.messages:
|
| 1289 |
_welcome_hero.empty()
|
| 1290 |
+
else:
|
| 1291 |
+
_welcome_hero.markdown(
|
| 1292 |
+
"""
|
| 1293 |
+
<div style="display:flex; align-items:center; justify-content:center; min-height:50vh;">
|
| 1294 |
+
<div style="text-align:center; color:#888;">
|
| 1295 |
+
<div style="font-size:52px; margin-bottom:16px;">🤖</div>
|
| 1296 |
+
<div style="font-size:20px; font-weight:600; margin-bottom:10px;">欢迎使用智答 AI 助手</div>
|
| 1297 |
+
<div style="font-size:15px; line-height:2;">
|
| 1298 |
+
🌐 <b>联网模式</b> —— 大模型 + 网络搜索,实时回答<br>
|
| 1299 |
+
📚 <b>知识库模式</b> —— 上传文档,基于私有知识回答
|
| 1300 |
+
</div>
|
| 1301 |
+
</div>
|
| 1302 |
+
</div>
|
| 1303 |
+
""",
|
| 1304 |
+
unsafe_allow_html=True,
|
| 1305 |
+
)
|
| 1306 |
|
| 1307 |
|
| 1308 |
@st.fragment
|
|
|
|
| 1313 |
_top_k = st.session_state.get("sel_topk", 5)
|
| 1314 |
_threshold = st.session_state.get("sel_threshold", 0.25)
|
| 1315 |
|
| 1316 |
+
# 消息容器(可滚动) + 输入框固定在下方
|
| 1317 |
+
# 有消息时使用固定高度实现滚动;无消息时不设高度避免空白
|
| 1318 |
_has_msgs = bool(st.session_state.messages)
|
| 1319 |
+
chat_box = st.container(height=520) if _has_msgs else st.container()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1320 |
q = st.chat_input("输入问题...", key="chat_input_v3")
|
| 1321 |
|
| 1322 |
+
with chat_box:
|
| 1323 |
+
for m in st.session_state.messages:
|
| 1324 |
+
with st.chat_message(m["role"]):
|
| 1325 |
+
st.markdown(m["content"])
|
| 1326 |
+
if "meta" in m:
|
| 1327 |
+
st.caption(m["meta"])
|
| 1328 |
+
|
| 1329 |
+
if q:
|
| 1330 |
+
_welcome_hero.empty() # 首次发消息时清除欢迎页
|
| 1331 |
+
st.session_state.messages.append({"role": "user", "content": q})
|
| 1332 |
+
if not IS_GUEST:
|
| 1333 |
+
_save_chat_message(CURRENT_USER, "user", q)
|
| 1334 |
with st.chat_message("user"):
|
| 1335 |
st.markdown(q)
|
| 1336 |
|
|
|
|
| 1338 |
container = st.empty()
|
| 1339 |
|
| 1340 |
# 联网模式:大模型 + 网络搜索,不查知识库
|
| 1341 |
+
# 知识库模式:RAG 检索本地知识库
|
| 1342 |
if _web_on:
|
| 1343 |
container.markdown("*🌐 正在联网搜索...*")
|
| 1344 |
relevant_docs = []
|