leezhuuu commited on
Commit
96aed6c
·
verified ·
1 Parent(s): 90f053a

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +65 -111
src/streamlit_app.py CHANGED
@@ -6,7 +6,6 @@ import requests
6
  import os
7
  import sys
8
  import subprocess
9
- import time
10
  from openai import OpenAI
11
  from rank_bm25 import BM25Okapi
12
  from sklearn.metrics.pairwise import cosine_similarity
@@ -28,10 +27,10 @@ st.set_page_config(
28
  initial_sidebar_state="expanded"
29
  )
30
 
31
- # --- 注入自定义 CSS (核心美化部分) ---
32
  st.markdown("""
33
  <style>
34
- /* 1. 整体背景设定 - 深空黑 */
35
  .stApp {
36
  background-color: #050505;
37
  background-image: radial-gradient(circle at 50% 0%, #1a1f35 0%, #050505 60%);
@@ -44,7 +43,7 @@ st.markdown("""
44
  footer {visibility: hidden;}
45
  header {visibility: hidden;}
46
 
47
- /* 3. 聊天气泡 - 毛玻璃特效 */
48
  [data-testid="stChatMessage"] {
49
  background: rgba(255, 255, 255, 0.03);
50
  border: 1px solid rgba(255, 255, 255, 0.08);
@@ -52,13 +51,9 @@ st.markdown("""
52
  backdrop-filter: blur(12px);
53
  box-shadow: 0 4px 20px rgba(0,0,0,0.2);
54
  padding: 1.2rem;
55
- transition: transform 0.2s;
56
- }
57
- [data-testid="stChatMessage"]:hover {
58
- border-color: rgba(41, 181, 232, 0.3);
59
  }
60
 
61
- /* 用户气泡特别样式 */
62
  [data-testid="stChatMessage"][data-testid="user"] {
63
  background: rgba(41, 181, 232, 0.1);
64
  border-color: rgba(41, 181, 232, 0.2);
@@ -82,7 +77,7 @@ st.markdown("""
82
  letter-spacing: -1px;
83
  }
84
 
85
- /* 5. 快捷按钮 (Starter Pills) */
86
  div.stButton > button {
87
  background: rgba(255,255,255,0.05);
88
  color: #aaa;
@@ -100,7 +95,7 @@ st.markdown("""
100
  transform: translateY(-2px);
101
  }
102
 
103
- /* 6. 输入框美化 */
104
  .stChatInputContainer textarea {
105
  background-color: #0f1115 !important;
106
  border: 1px solid #333 !important;
@@ -108,29 +103,23 @@ st.markdown("""
108
  border-radius: 12px !important;
109
  }
110
 
111
- /* 7. 证据栏 Expander 美化 */
112
  .streamlit-expanderHeader {
113
  background-color: rgba(255,255,255,0.02);
114
  border: 1px solid rgba(255,255,255,0.05);
115
  border-radius: 8px;
116
  color: #bbb;
117
  }
118
- .evidence-highlight {
119
- color: #29B5E8;
120
- font-weight: bold;
121
- font-family: monospace;
122
- }
123
  </style>
124
  """, unsafe_allow_html=True)
125
 
126
- # ================= 2. 核心逻辑(保留原本的鲁棒设计) =================
127
 
128
  if not API_KEY:
129
  st.error("⚠️ 未检测到 API Key。请在 Settings -> Secrets 中配置 `SILICONFLOW_API_KEY`。")
130
  st.stop()
131
 
132
  def download_with_curl(url, output_path):
133
- """使用系统 curl 命令下载,模拟浏览器行为绕过 403"""
134
  try:
135
  cmd = [
136
  "curl", "-L",
@@ -140,8 +129,7 @@ def download_with_curl(url, output_path):
140
  url
141
  ]
142
  result = subprocess.run(cmd, capture_output=True, text=True)
143
- if result.returncode != 0:
144
- raise Exception(f"Curl failed: {result.stderr}")
145
  return True
146
  except Exception as e:
147
  print(f"Curl download error: {e}")
@@ -155,15 +143,11 @@ def get_data_file_path():
155
  os.path.join("..", DATA_FILENAME), "/tmp/" + DATA_FILENAME
156
  ]
157
  for path in possible_paths:
158
- if os.path.exists(path):
159
- return path
160
 
161
- # 下载逻辑
162
  download_target = "/app/" + DATA_FILENAME
163
- try:
164
- os.makedirs(os.path.dirname(download_target), exist_ok=True)
165
- except:
166
- download_target = "/tmp/" + DATA_FILENAME # 回退到 tmp
167
 
168
  status_container = st.empty()
169
  status_container.info("📡 正在接入神经元网络... (下载核心数据中)")
@@ -172,84 +156,62 @@ def get_data_file_path():
172
  status_container.empty()
173
  return download_target
174
 
175
- # 备用下载
176
  try:
177
  headers = {'User-Agent': 'Mozilla/5.0'}
178
  r = requests.get(DATA_URL, headers=headers, stream=True)
179
  r.raise_for_status()
180
  with open(download_target, 'wb') as f:
181
- for chunk in r.iter_content(chunk_size=8192):
182
- f.write(chunk)
183
  status_container.empty()
184
  return download_target
185
  except Exception as e:
186
  st.error(f"❌ 数据链路中断。Error: {e}")
187
  st.stop()
188
 
189
- class RerankClient:
190
- def __init__(self, api_base, api_key, model):
191
- self.api_url = f"{api_base}/rerank"
192
- self.headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
193
- self.model = model
194
-
195
- def rerank(self, query: str, documents: list, top_n: int = 5):
196
- if not documents: return []
197
- payload = {"model": self.model, "query": query, "documents": documents, "top_n": top_n}
198
- try:
199
- response = requests.post(self.api_url, headers=self.headers, json=payload, timeout=10)
200
- if response.status_code == 200:
201
- return response.json().get('results', [])
202
- return [{"index": i, "relevance_score": 0.0} for i in range(len(documents))]
203
- except:
204
- return [{"index": i, "relevance_score": 0.0} for i in range(len(documents))]
205
-
206
  class FullRetriever:
207
  def __init__(self, parquet_path):
208
- try:
209
- self.df = pd.read_parquet(parquet_path)
210
- except Exception as e:
211
- st.error(f"Memory Matrix Load Failed: {e}")
212
- st.stop()
213
-
214
  self.documents = self.df['content'].tolist()
215
  self.embeddings = np.stack(self.df['embedding'].values)
216
  self.bm25 = BM25Okapi([jieba.lcut(str(d).lower()) for d in self.documents])
217
  self.client = OpenAI(base_url=API_BASE, api_key=API_KEY)
218
- self.reranker = RerankClient(API_BASE, API_KEY, RERANK_MODEL)
 
 
219
 
220
  def _get_emb(self, q):
221
- try:
222
- return self.client.embeddings.create(model=EMBEDDING_MODEL, input=[q]).data[0].embedding
223
- except:
224
- return [0.0] * 1024
225
 
226
  def hybrid_search(self, query: str, top_k=5):
227
  # 1. Vector
228
  q_emb = self._get_emb(query)
229
  vec_scores = cosine_similarity([q_emb], self.embeddings)[0]
230
  vec_idx = np.argsort(vec_scores)[-100:][::-1]
231
-
232
  # 2. Keyword
233
  kw_idx = np.argsort(self.bm25.get_scores(jieba.lcut(query.lower())))[-100:][::-1]
234
-
235
  # 3. RRF Fusion
236
  fused = {}
237
  for r, i in enumerate(vec_idx): fused[i] = fused.get(i, 0) + 1/(60+r+1)
238
  for r, i in enumerate(kw_idx): fused[i] = fused.get(i, 0) + 1/(60+r+1)
239
-
240
  c_idxs = [x[0] for x in sorted(fused.items(), key=lambda x:x[1], reverse=True)[:50]]
241
  c_docs = [self.documents[i] for i in c_idxs]
242
 
243
  # 4. Rerank
244
- results = self.reranker.rerank(query, c_docs, top_n=top_k)
245
-
 
 
 
 
 
246
  final_res = []
247
  context = ""
248
  for i, item in enumerate(results):
249
  orig_idx = c_idxs[item['index']]
250
  row = self.df.iloc[orig_idx]
251
  final_res.append({
252
- "rank": i+1,
253
  "score": item['relevance_score'],
254
  "filename": row['filename'],
255
  "content": row['content']
@@ -262,17 +224,16 @@ def load_engine():
262
  real_path = get_data_file_path()
263
  return FullRetriever(real_path)
264
 
265
- # ================= 3. UI 主逻辑 =================
266
 
267
  def main():
268
- # 自定义 Header 区域
269
  st.markdown("""
270
  <div class="custom-header">
271
  <div style="font-size: 3rem;">🌌</div>
272
  <div>
273
  <div class="glitch-text">COMSOL DARK EXPERT</div>
274
  <div style="color: #666; font-size: 0.9rem; letter-spacing: 1px;">
275
- NEURAL SIMULATION ASSISTANT <span style="color:#29B5E8">V4.0</span>
276
  </div>
277
  </div>
278
  </div>
@@ -280,78 +241,75 @@ def main():
280
 
281
  retriever = load_engine()
282
 
283
- # --- 侧边栏 ---
284
  with st.sidebar:
285
  st.markdown("### ⚙️ 控制台")
286
- top_k = st.slider("Retrieval Depth (检索深度)", 1, 10, 4)
287
- temp = st.slider("Temperature (发散度)", 0.0, 1.0, 0.3)
288
  st.markdown("---")
289
-
290
- if st.button("🗑️ 格式化内存 (Clear History)", use_container_width=True):
291
  st.session_state.messages = []
292
  st.session_state.current_refs = []
293
  st.rerun()
294
-
295
- st.markdown("""
296
- <div style="margin-top: 2rem; padding: 1rem; background: rgba(255,255,255,0.05); border-radius: 8px; font-size: 0.8rem; color: #888;">
297
- <strong>System Status:</strong><br>
298
- ✅ Vector Engine: Online<br>
299
- ✅ Reranker: Active<br>
300
- ✅ Neural Link: Stable
301
- </div>
302
- """, unsafe_allow_html=True)
303
 
304
- # --- 初始化 Session ---
305
  if "messages" not in st.session_state: st.session_state.messages = []
306
  if "current_refs" not in st.session_state: st.session_state.current_refs = []
307
 
308
- # --- 布局:左聊天,右证据 ---
309
  col_chat, col_evidence = st.columns([0.65, 0.35], gap="large")
310
 
 
 
 
 
311
  with col_chat:
312
- # 1. 如果没有历史消息,显示快捷提问胶囊 (Starter Pills)
313
  if not st.session_state.messages:
314
  st.markdown("##### 💡 初始化提问序列 (Starter Sequence)")
315
  c1, c2, c3 = st.columns(3)
 
316
  if c1.button("🌊 流固耦合接口设置"):
317
- st.session_state.messages.append({"role": "user", "content": "怎么设置流固耦合接口?"})
318
- st.rerun()
319
- if c2.button("⚡ 低频电磁场网格"):
320
- st.session_state.messages.append({"role": "user", "content": "低频电磁场网格划分有哪些技巧?"})
321
- st.rerun()
322
- if c3.button("📉 求解器不收敛"):
323
- st.session_state.messages.append({"role": "user", "content": "求解器不收敛通常怎么解决?"})
324
- st.rerun()
325
-
326
  # 2. 渲染历史消息
327
  for msg in st.session_state.messages:
328
  with st.chat_message(msg["role"]):
329
  st.markdown(msg["content"])
330
 
331
- # 3. 输入处理
332
- if prompt := st.chat_input("输入指令或物理参数问题..."):
333
- st.session_state.messages.append({"role": "user", "content": prompt})
334
- with st.chat_message("user"): st.markdown(prompt)
 
 
 
 
 
335
 
336
- # RAG 检索
 
 
 
 
 
 
 
337
  with st.spinner("🔍 正在扫描向量空间..."):
338
- refs, context = retriever.hybrid_search(prompt, top_k=top_k)
339
  st.session_state.current_refs = refs
340
 
341
- # 增强版 System Prompt
342
  system_prompt = f"""你是一个COMSOL高级仿真专家。请基于提供的文档回答问题。
343
-
344
  要求:
345
  1. 语气专业、客观,逻辑严密。
346
  2. 涉及物理公式时,**必须**使用 LaTeX 格式(例如 $E = mc^2$)。
347
  3. 涉及步骤或参数对比时,优先使用 Markdown 列表或表格。
348
- 4. 严禁编造文档中不存在的参数。
349
 
350
  参考文档:
351
  {context}
352
  """
353
 
354
- # 生成回答
355
  with st.chat_message("assistant"):
356
  resp_cont = st.empty()
357
  full_resp = ""
@@ -360,7 +318,7 @@ def main():
360
  try:
361
  stream = client.chat.completions.create(
362
  model=GEN_MODEL_NAME,
363
- messages=[{"role": "system", "content": system_prompt}] + st.session_state.messages[-6:],
364
  temperature=temp,
365
  stream=True
366
  )
@@ -368,16 +326,13 @@ def main():
368
  txt = chunk.choices[0].delta.content
369
  if txt:
370
  full_resp += txt
371
- # 打字机效果 + 光标
372
  resp_cont.markdown(full_resp + " ▌")
373
- resp_cont.markdown(full_resp) # 移除光标
374
  st.session_state.messages.append({"role": "assistant", "content": full_resp})
375
- # 强制刷新以更新右侧证据栏(虽然Streamlit通常会自动更新,但显式rerun更稳)
376
- # st.rerun() -> 这里不需要rerun,因为session_state已更新,下一轮循环会渲染
377
  except Exception as e:
378
  st.error(f"Neural Generation Failed: {e}")
379
 
380
- # --- 右侧证据栏美化 ---
381
  with col_evidence:
382
  st.markdown("### 📚 神经记忆 (Evidence)")
383
  if st.session_state.current_refs:
@@ -385,7 +340,6 @@ def main():
385
  score = ref['score']
386
  score_color = "#00ff41" if score > 0.6 else "#ffb700" if score > 0.4 else "#ff003c"
387
 
388
- # 使用 Expander 折叠详细内容
389
  with st.expander(f"📄 Doc {i+1}: {ref['filename'][:20]}...", expanded=(i==0)):
390
  st.markdown(f"""
391
  <div style="margin-bottom:5px;">
 
6
  import os
7
  import sys
8
  import subprocess
 
9
  from openai import OpenAI
10
  from rank_bm25 import BM25Okapi
11
  from sklearn.metrics.pairwise import cosine_similarity
 
27
  initial_sidebar_state="expanded"
28
  )
29
 
30
+ # --- 注入自定义 CSS (保持之前的审美) ---
31
  st.markdown("""
32
  <style>
33
+ /* 1. 整体背景 - 深空黑 */
34
  .stApp {
35
  background-color: #050505;
36
  background-image: radial-gradient(circle at 50% 0%, #1a1f35 0%, #050505 60%);
 
43
  footer {visibility: hidden;}
44
  header {visibility: hidden;}
45
 
46
+ /* 3. 聊天气泡 */
47
  [data-testid="stChatMessage"] {
48
  background: rgba(255, 255, 255, 0.03);
49
  border: 1px solid rgba(255, 255, 255, 0.08);
 
51
  backdrop-filter: blur(12px);
52
  box-shadow: 0 4px 20px rgba(0,0,0,0.2);
53
  padding: 1.2rem;
 
 
 
 
54
  }
55
 
56
+ /* 用户气泡 */
57
  [data-testid="stChatMessage"][data-testid="user"] {
58
  background: rgba(41, 181, 232, 0.1);
59
  border-color: rgba(41, 181, 232, 0.2);
 
77
  letter-spacing: -1px;
78
  }
79
 
80
+ /* 5. 快捷按钮 */
81
  div.stButton > button {
82
  background: rgba(255,255,255,0.05);
83
  color: #aaa;
 
95
  transform: translateY(-2px);
96
  }
97
 
98
+ /* 6. 输入框 */
99
  .stChatInputContainer textarea {
100
  background-color: #0f1115 !important;
101
  border: 1px solid #333 !important;
 
103
  border-radius: 12px !important;
104
  }
105
 
106
+ /* 7. Expander */
107
  .streamlit-expanderHeader {
108
  background-color: rgba(255,255,255,0.02);
109
  border: 1px solid rgba(255,255,255,0.05);
110
  border-radius: 8px;
111
  color: #bbb;
112
  }
 
 
 
 
 
113
  </style>
114
  """, unsafe_allow_html=True)
115
 
116
+ # ================= 2. 核心逻辑(数据与RAG) =================
117
 
118
  if not API_KEY:
119
  st.error("⚠️ 未检测到 API Key。请在 Settings -> Secrets 中配置 `SILICONFLOW_API_KEY`。")
120
  st.stop()
121
 
122
  def download_with_curl(url, output_path):
 
123
  try:
124
  cmd = [
125
  "curl", "-L",
 
129
  url
130
  ]
131
  result = subprocess.run(cmd, capture_output=True, text=True)
132
+ if result.returncode != 0: raise Exception(f"Curl failed: {result.stderr}")
 
133
  return True
134
  except Exception as e:
135
  print(f"Curl download error: {e}")
 
143
  os.path.join("..", DATA_FILENAME), "/tmp/" + DATA_FILENAME
144
  ]
145
  for path in possible_paths:
146
+ if os.path.exists(path): return path
 
147
 
 
148
  download_target = "/app/" + DATA_FILENAME
149
+ try: os.makedirs(os.path.dirname(download_target), exist_ok=True)
150
+ except: download_target = "/tmp/" + DATA_FILENAME
 
 
151
 
152
  status_container = st.empty()
153
  status_container.info("📡 正在接入神经元网络... (下载核心数据中)")
 
156
  status_container.empty()
157
  return download_target
158
 
 
159
  try:
160
  headers = {'User-Agent': 'Mozilla/5.0'}
161
  r = requests.get(DATA_URL, headers=headers, stream=True)
162
  r.raise_for_status()
163
  with open(download_target, 'wb') as f:
164
+ for chunk in r.iter_content(chunk_size=8192): f.write(chunk)
 
165
  status_container.empty()
166
  return download_target
167
  except Exception as e:
168
  st.error(f"❌ 数据链路中断。Error: {e}")
169
  st.stop()
170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  class FullRetriever:
172
  def __init__(self, parquet_path):
173
+ try: self.df = pd.read_parquet(parquet_path)
174
+ except Exception as e: st.error(f"Memory Matrix Load Failed: {e}"); st.stop()
 
 
 
 
175
  self.documents = self.df['content'].tolist()
176
  self.embeddings = np.stack(self.df['embedding'].values)
177
  self.bm25 = BM25Okapi([jieba.lcut(str(d).lower()) for d in self.documents])
178
  self.client = OpenAI(base_url=API_BASE, api_key=API_KEY)
179
+ # Reranker 初始化移到这里,减少重复调用
180
+ self.rerank_headers = {"Content-Type": "application/json", "Authorization": f"Bearer {API_KEY}"}
181
+ self.rerank_url = f"{API_BASE}/rerank"
182
 
183
  def _get_emb(self, q):
184
+ try: return self.client.embeddings.create(model=EMBEDDING_MODEL, input=[q]).data[0].embedding
185
+ except: return [0.0] * 1024
 
 
186
 
187
  def hybrid_search(self, query: str, top_k=5):
188
  # 1. Vector
189
  q_emb = self._get_emb(query)
190
  vec_scores = cosine_similarity([q_emb], self.embeddings)[0]
191
  vec_idx = np.argsort(vec_scores)[-100:][::-1]
 
192
  # 2. Keyword
193
  kw_idx = np.argsort(self.bm25.get_scores(jieba.lcut(query.lower())))[-100:][::-1]
 
194
  # 3. RRF Fusion
195
  fused = {}
196
  for r, i in enumerate(vec_idx): fused[i] = fused.get(i, 0) + 1/(60+r+1)
197
  for r, i in enumerate(kw_idx): fused[i] = fused.get(i, 0) + 1/(60+r+1)
 
198
  c_idxs = [x[0] for x in sorted(fused.items(), key=lambda x:x[1], reverse=True)[:50]]
199
  c_docs = [self.documents[i] for i in c_idxs]
200
 
201
  # 4. Rerank
202
+ try:
203
+ payload = {"model": RERANK_MODEL, "query": query, "documents": c_docs, "top_n": top_k}
204
+ resp = requests.post(self.rerank_url, headers=self.rerank_headers, json=payload, timeout=10)
205
+ results = resp.json().get('results', [])
206
+ except:
207
+ results = [{"index": i, "relevance_score": 0.0} for i in range(len(c_docs))][:top_k]
208
+
209
  final_res = []
210
  context = ""
211
  for i, item in enumerate(results):
212
  orig_idx = c_idxs[item['index']]
213
  row = self.df.iloc[orig_idx]
214
  final_res.append({
 
215
  "score": item['relevance_score'],
216
  "filename": row['filename'],
217
  "content": row['content']
 
224
  real_path = get_data_file_path()
225
  return FullRetriever(real_path)
226
 
227
+ # ================= 3. UI 主程序 =================
228
 
229
  def main():
 
230
  st.markdown("""
231
  <div class="custom-header">
232
  <div style="font-size: 3rem;">🌌</div>
233
  <div>
234
  <div class="glitch-text">COMSOL DARK EXPERT</div>
235
  <div style="color: #666; font-size: 0.9rem; letter-spacing: 1px;">
236
+ NEURAL SIMULATION ASSISTANT <span style="color:#29B5E8">V4.1 Fixed</span>
237
  </div>
238
  </div>
239
  </div>
 
241
 
242
  retriever = load_engine()
243
 
 
244
  with st.sidebar:
245
  st.markdown("### ⚙️ 控制台")
246
+ top_k = st.slider("检索深度", 1, 10, 4)
247
+ temp = st.slider("发散度", 0.0, 1.0, 0.3)
248
  st.markdown("---")
249
+ if st.button("🗑️ 清空记忆 (Clear)", use_container_width=True):
 
250
  st.session_state.messages = []
251
  st.session_state.current_refs = []
252
  st.rerun()
 
 
 
 
 
 
 
 
 
253
 
 
254
  if "messages" not in st.session_state: st.session_state.messages = []
255
  if "current_refs" not in st.session_state: st.session_state.current_refs = []
256
 
 
257
  col_chat, col_evidence = st.columns([0.65, 0.35], gap="large")
258
 
259
+ # ------------------ 处理输入源 ------------------
260
+ # 我们定义一个变量 user_input,不管它来自按钮还是输入框
261
+ user_input = None
262
+
263
  with col_chat:
264
+ # 1. 如果历史为空,显示快捷按钮
265
  if not st.session_state.messages:
266
  st.markdown("##### 💡 初始化提问序列 (Starter Sequence)")
267
  c1, c2, c3 = st.columns(3)
268
+ # 点击按钮直接赋值给 user_input
269
  if c1.button("🌊 流固耦合接口设置"):
270
+ user_input = "怎么设置流固耦合接口?"
271
+ elif c2.button("⚡ 低频电磁场网格"):
272
+ user_input = "低频电磁场网格划分有哪些技巧?"
273
+ elif c3.button("📉 求解器不收敛"):
274
+ user_input = "求解器不收敛通常怎么解决?"
275
+
 
 
 
276
  # 2. 渲染历史消息
277
  for msg in st.session_state.messages:
278
  with st.chat_message(msg["role"]):
279
  st.markdown(msg["content"])
280
 
281
+ # 3. 处理底部输入框 (如果有按钮输入,这里会被跳过,因为 user_input 已经有值了)
282
+ if not user_input:
283
+ user_input = st.chat_input("输入指令或物理参数问题...")
284
+
285
+ # ------------------ 统一处理消息追加 ------------------
286
+ if user_input:
287
+ st.session_state.messages.append({"role": "user", "content": user_input})
288
+ # 强制刷新以立即在 UI 上显示用户的提问(对于按钮点击尤为重要)
289
+ st.rerun()
290
 
291
+ # ------------------ 统一触发生成 (修复的核心) ------------------
292
+ # 检查:如果有消息,且最后一条是 User 发的,说明需要 Assistant 回答
293
+ if st.session_state.messages and st.session_state.messages[-1]["role"] == "user":
294
+
295
+ # 获取最后一条用户消息
296
+ last_query = st.session_state.messages[-1]["content"]
297
+
298
+ with col_chat: # 确保在聊天栏显示
299
  with st.spinner("🔍 正在扫描向量空间..."):
300
+ refs, context = retriever.hybrid_search(last_query, top_k=top_k)
301
  st.session_state.current_refs = refs
302
 
 
303
  system_prompt = f"""你是一个COMSOL高级仿真专家。请基于提供的文档回答问题。
 
304
  要求:
305
  1. 语气专业、客观,逻辑严密。
306
  2. 涉及物理公式时,**必须**使用 LaTeX 格式(例如 $E = mc^2$)。
307
  3. 涉及步骤或参数对比时,优先使用 Markdown 列表或表格。
 
308
 
309
  参考文档:
310
  {context}
311
  """
312
 
 
313
  with st.chat_message("assistant"):
314
  resp_cont = st.empty()
315
  full_resp = ""
 
318
  try:
319
  stream = client.chat.completions.create(
320
  model=GEN_MODEL_NAME,
321
+ messages=[{"role": "system", "content": system_prompt}] + st.session_state.messages[-6:], # 除去当前的System
322
  temperature=temp,
323
  stream=True
324
  )
 
326
  txt = chunk.choices[0].delta.content
327
  if txt:
328
  full_resp += txt
 
329
  resp_cont.markdown(full_resp + " ▌")
330
+ resp_cont.markdown(full_resp)
331
  st.session_state.messages.append({"role": "assistant", "content": full_resp})
 
 
332
  except Exception as e:
333
  st.error(f"Neural Generation Failed: {e}")
334
 
335
+ # ------------------ 渲染右侧证据栏 ------------------
336
  with col_evidence:
337
  st.markdown("### 📚 神经记忆 (Evidence)")
338
  if st.session_state.current_refs:
 
340
  score = ref['score']
341
  score_color = "#00ff41" if score > 0.6 else "#ffb700" if score > 0.4 else "#ff003c"
342
 
 
343
  with st.expander(f"📄 Doc {i+1}: {ref['filename'][:20]}...", expanded=(i==0)):
344
  st.markdown(f"""
345
  <div style="margin-bottom:5px;">