1oscon commited on
Commit
71f20e5
·
verified ·
1 Parent(s): d3d246a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +248 -167
app.py CHANGED
@@ -1,6 +1,6 @@
1
  import os, json, gc, datetime
2
- from typing import List, Tuple
3
  from pathlib import Path
 
4
 
5
  # 性能与日志
6
  os.environ["OMP_NUM_THREADS"] = "2"
@@ -17,10 +17,10 @@ from huggingface_hub import snapshot_download
17
  模型本地目录 = "./phi4_model"
18
  历史目录 = "./chat_history"
19
 
20
- 系统提示 = "你是一个友好的中文 AI 助手。"
21
  结束标记 = "<|end|>"
22
- 上下文窗口 = 4096 # 估计
23
- 默认回复长度 = 256
24
 
25
  # ================ 工具函数 ================
26
  def 确保目录():
@@ -46,20 +46,25 @@ class 历史管理:
46
  return os.path.join(历史目录, f"{会话}.json")
47
 
48
  @staticmethod
49
- def 保存(历史: List[List[str]], 会话="默认会话"):
50
- data = {"history": 历史, "time": datetime.datetime.now().isoformat()}
51
  with open(历史管理.路径(会话), "w", encoding="utf-8") as f:
52
  json.dump(data, f, ensure_ascii=False)
53
 
54
  @staticmethod
55
- def 加载(会话="默认会话") -> List[List[str]]:
56
  p = 历史管理.路径(会话)
57
  if os.path.exists(p):
58
  with open(p, "r", encoding="utf-8") as f:
59
- return json.load(f).get("history", [])
60
- return []
 
 
 
 
 
61
 
62
- # ORT GenAI 兼容(不同版本的长度/搜索参数)
63
  def 设定长度参数(params: "og.GeneratorParams", 最大长度: int) -> bool:
64
  if hasattr(params, "set_length_options"):
65
  params.set_length_options(max_length=int(最大长度))
@@ -67,8 +72,8 @@ def 设定长度参数(params: "og.GeneratorParams", 最大长度: int) -> bool:
67
  return False
68
 
69
  def 设定搜索参数(params: "og.GeneratorParams",
70
- 采样: bool, 温度: float, top_p: float, top_k: int, 重复惩罚: float,
71
- 最大长度_if_needed: int | None):
72
  kwargs = dict(
73
  do_sample=bool(采样),
74
  temperature=float(温度),
@@ -81,7 +86,6 @@ def 设定搜索参数(params: "og.GeneratorParams",
81
  try:
82
  params.set_search_options(**kwargs)
83
  except TypeError:
84
- # 极旧版不支持时,移除不兼容键
85
  kwargs.pop("top_k", None)
86
  kwargs.pop("repetition_penalty", None)
87
  params.set_search_options(**kwargs)
@@ -107,7 +111,6 @@ def 按窗口裁剪(input_ids: list, 新token上限: int, 上下文上限: int)
107
  print("🚀 初始化中...")
108
  try:
109
  模型路径 = 下载模型()
110
- # 降低 ORT 日志
111
  try:
112
  import onnxruntime as ort
113
  ort.set_default_logger_severity(3)
@@ -135,16 +138,17 @@ except Exception as e:
135
  分词器 = None
136
 
137
  # ================ 生成(流式) ================
138
- def 流式回复(用户消息: str, 历史: List[List[str]], 回复长度: int, 温度: float, 记忆轮数: int, 上下文tokens: int):
 
 
139
  if not 模型 or not 分词器:
140
- yield "❌ 模型未加载,请稍后重试"
141
  return
142
  try:
143
- # 仅保留最近 N 轮,避免过长
144
  if 记忆轮数 > 0 and len(历史) > 记忆轮数:
145
  历史 = 历史[-记忆轮数:]
146
 
147
- 提示 = 构建模板(系统提示, 历史, 用户消息)
148
  输入ID = 分词器.encode(提示)
149
  输入ID = 按窗口裁剪(输入ID, 回复长度, min(上下文tokens, 上下文窗口))
150
 
@@ -164,6 +168,7 @@ def 流式回复(用户消息: str, 历史: List[List[str]], 回复长度: int,
164
  流 = 分词器.create_stream()
165
 
166
  回复 = ""
 
167
  while not 生成器.is_done():
168
  生成器.compute_logits()
169
  生成器.generate_next_token()
@@ -171,203 +176,279 @@ def 流式回复(用户消息: str, 历史: List[List[str]], 回复长度: int,
171
  片段 = 流.decode(新)
172
  if not 片段:
173
  continue
174
-
175
  回复 += 片段
 
 
176
  if 结束标记 in 回复:
177
  回复 = 回复.split(结束标记)[0].rstrip()
178
- yield 回复
179
  break
180
 
181
- # 适度降频更新,提升流畅度
182
- if len(回复) % 8 == 0:
183
- yield 回复
184
-
185
  else:
186
- yield 回复.strip()
187
 
188
  del 生成器, params
189
  gc.collect()
190
-
191
  except Exception as e:
192
- yield f"❌ 生成错误: {str(e)}"
193
 
194
- # ================== UI(极简 + 自适应所有设备) ==================
195
  css = """
196
- /* 页面三段式:头部 / 聊天 / 输入 */
197
- #layout {
198
- height: 100dvh; /* 移动端更准确的视口单位 */
199
- display: grid;
200
- grid-template-rows: auto 1fr auto;
201
- gap: 8px;
202
- max-width: 880px;
203
- margin: 0 auto;
204
- padding: 8px 10px 12px;
205
- }
206
-
207
- /* 头部 */
208
- #hdr {
209
- background: #ffffff;
210
- border-radius: 12px;
211
- padding: 10px 14px;
212
- box-shadow: 0 2px 10px rgba(0,0,0,0.05);
213
  }
 
 
214
 
215
- /* 聊天容器可滚动 */
216
- #chat_region {
217
- min-height: 0; /* 允许子元素收缩 */
218
- overflow: hidden; /* 由内部滚动 */
219
- }
220
- #chatbox [data-testid="chatbot"] {
221
- height: 100% !important;
222
  }
223
- #chatbox {
224
- height: 100%;
225
- overflow: auto; /* 滚动由此容器承担 */
226
- background: #fff;
227
- border-radius: 12px;
228
- box-shadow: 0 2px 10px rgba(0,0,0,0.05);
229
  }
230
-
231
- /* 输入区 */
232
- #input_region {
233
- background: #ffffff;
234
- border-radius: 12px;
235
- box-shadow: 0 2px 10px rgba(0,0,0,0.05);
236
  padding: 8px;
237
  }
238
 
239
- /* 自适应:小屏压缩间距与字号 */
240
  @media (max-width: 640px) {
241
- #hdr h1 { font-size: 18px; }
242
- #hdr p { font-size: 12px; }
243
- }
244
-
245
- /* 简洁按钮样式 */
246
- .gr-button { border-radius: 10px !important; }
247
- .gr-button-primary {
248
- background: #6d28d9 !important;
249
  }
250
  """
251
 
252
- # 用 JS 动态计算聊天区高度(更稳适配各种设备与键盘/地址栏)
253
- autosize_js = """
254
- <script>
255
- function sizeChat() {
256
- const layout = document.getElementById('layout');
257
- const hdr = document.getElementById('hdr');
258
- const input = document.getElementById('input_region');
259
- const chat = document.getElementById('chatbox');
260
- if (!layout || !chat) return;
261
- const vh = (window.visualViewport && window.visualViewport.height) || window.innerHeight;
262
- const used = (hdr?.offsetHeight || 0) + (input?.offsetHeight || 0) + 24; // padding + gap
263
- const h = Math.max(240, vh - used);
264
- chat.style.height = h + 'px';
265
- }
266
- window.addEventListener('resize', sizeChat);
267
- window.addEventListener('orientationchange', sizeChat);
268
- setTimeout(sizeChat, 50);
269
- setTimeout(sizeChat, 300);
270
- </script>
271
- """
272
-
273
- 历史 = 历史管理.加载("默认会话")
274
 
275
  with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
276
- # 页面布局
277
- with gr.Column(elem_id="layout"):
278
- with gr.Column(elem_id="hdr"):
279
- gr.Markdown("### 💬 Phi-4 中文助手")
280
- gr.Markdown("简洁 · 智能 · 自适应所有设备")
281
-
282
- with gr.Column(elem_id="chat_region"):
283
- 聊天框 = gr.Chatbot(
284
- value=历史,
285
- type="tuples", # [[user, assistant], ...] 兼容,无警告
286
- elem_id="chatbox",
287
- show_copy_button=True
288
- )
289
-
290
- with gr.Column(elem_id="input_region"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  with gr.Row():
292
- 消息 = gr.Textbox(
293
- placeholder="输入你的问题...",
294
- lines=1,
295
- max_lines=4,
296
- scale=6
297
- )
298
- 发送 = gr.Button("发送", variant="primary", scale=1)
299
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  with gr.Row():
301
- 清空 = gr.Button("清空", size="sm")
302
- 撤销 = gr.Button("撤销", size="sm")
303
- 重试 = gr.Button("重试", size="sm")
304
- 停止 = gr.Button("停止", size="sm")
305
-
 
 
306
  with gr.Row():
307
- 回复长度 = gr.Slider(50, 512, value=默认回复长度, step=16, label="回复长度")
308
- 温度 = gr.Slider(0.0, 1.0, value=0.7, step=0.1, label="创造性")
309
- 记忆轮数 = gr.Slider(1, 12, value=5, step=1, label="记忆轮数")
310
- 上下文限制 = gr.Slider(512, 上下文窗口, value=2048, step=128, label=f"上下文上限(≤{上下文窗口})")
311
-
312
- # 自动高度脚本
313
- gr.HTML(autosize_js)
314
-
315
- # 交互逻辑
316
- def 用户提交(文本, 历史):
317
- 文本 = (文本 or "").strip()
318
- if not 文本:
319
- return "", 历史
320
- return "", 历史 + [[文本, None]]
321
-
322
- def 机器人应答(历史, 回复长度, 温度, 记忆轮数, 上下文限制):
323
- if not 历史 or 历史[-1][1] is not None:
324
- return 历史
325
- 用户消息 = 历史[-1][0]
326
- 历史[-1][1] = ""
327
- for 段落 in 流式回复(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  用户消息=用户消息,
329
- 历史=历史[:-1],
330
- 回复长度=int(回复长度),
331
- 温度=float(温度),
332
- 记忆轮数=int(记忆轮数),
333
- 上下文tokens=int(上下文限制),
334
  ):
335
- 历史[-1][1] = 段落
336
- yield 历史
337
- 历史管理.保存(历史, "默认会话")
338
 
339
- 提交事件 = 消息.submit(
 
340
  用户提交, [消息, 聊天框], [消息, 聊天框], queue=False
341
  ).then(
342
- 机器人应答, [聊天框, 回复长度, 温度, 记忆轮数, 上下文限制], 聊天框
 
343
  )
344
-
345
- 点击事件 = 发送.click(
346
  用户提交, [消息, 聊天框], [消息, 聊天框], queue=False
347
  ).then(
348
- 机器人应答, [聊天框, 回复长度, 温度, 记忆轮数, 上下文限制], 聊天框
 
 
 
 
 
 
 
 
 
 
 
349
  )
350
 
351
- 停止.click(fn=None, inputs=None, outputs=None, cancels=[提交事件, 点击事件])
 
352
 
 
353
  清空.click(lambda: [], None, 聊天框)
354
  撤销.click(lambda h: h[:-1] if h else h, 聊天框, 聊天框)
355
-
356
- def 重试一轮(历史):
357
- if not 历史:
358
- return 历史
359
- 最后用户 = 历史[-1][0]
360
- return 历史[:-1] + [[最后用户, None]]
361
  重试.click(重试一轮, 聊天框, 聊天框).then(
362
- 机器人应答, [聊天框, 回复长度, 温度, 记忆轮数, 上下文限制], 聊天框
 
363
  )
364
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  # 自动保存
366
- 聊天框.change(lambda h: 历史管理.保存(h, "默认会话") if h else None, 聊天框, None)
 
 
 
 
 
 
 
 
367
 
368
  if __name__ == "__main__":
369
  if 模型:
370
  print("🎉 启动服务...")
371
- demo.queue(max_size=12).launch(server_name="0.0.0.0", server_port=7860, share=False)
372
  else:
373
  print("❌ 无法启动")
 
1
  import os, json, gc, datetime
 
2
  from pathlib import Path
3
+ from typing import List, Tuple
4
 
5
  # 性能与日志
6
  os.environ["OMP_NUM_THREADS"] = "2"
 
17
  模型本地目录 = "./phi4_model"
18
  历史目录 = "./chat_history"
19
 
20
+ 系统提示默认 = "你是一个友好的中文 AI 助手,请清晰、简洁地回答。"
21
  结束标记 = "<|end|>"
22
+ 上下文窗口 = 4096 # 估计值
23
+ 默认回复长度 = 300
24
 
25
  # ================ 工具函数 ================
26
  def 确保目录():
 
46
  return os.path.join(历史目录, f"{会话}.json")
47
 
48
  @staticmethod
49
+ def 保存(历史: List[List[str]], 会话="默认会话", 元数据: dict = None):
50
+ data = {"history": 历史, "meta": 元数据 or {}, "time": datetime.datetime.now().isoformat()}
51
  with open(历史管理.路径(会话), "w", encoding="utf-8") as f:
52
  json.dump(data, f, ensure_ascii=False)
53
 
54
  @staticmethod
55
+ def 加载(会话="默认会话") -> Tuple[List[List[str]], dict]:
56
  p = 历史管理.路径(会话)
57
  if os.path.exists(p):
58
  with open(p, "r", encoding="utf-8") as f:
59
+ js = json.load(f)
60
+ return js.get("history", []), js.get("meta", {})
61
+ return [], {}
62
+
63
+ @staticmethod
64
+ def 列表() -> List[str]:
65
+ return sorted([p.stem for p in Path(历史目录).glob("*.json")], reverse=True)
66
 
67
+ # ORT GenAI 兼容
68
  def 设定长度参数(params: "og.GeneratorParams", 最大长度: int) -> bool:
69
  if hasattr(params, "set_length_options"):
70
  params.set_length_options(max_length=int(最大长度))
 
72
  return False
73
 
74
  def 设定搜索参数(params: "og.GeneratorParams",
75
+ 采样: bool, 温度: float, top_p: float, top_k: int, 重复惩罚: float,
76
+ 最大长度_if_needed: int | None):
77
  kwargs = dict(
78
  do_sample=bool(采样),
79
  temperature=float(温度),
 
86
  try:
87
  params.set_search_options(**kwargs)
88
  except TypeError:
 
89
  kwargs.pop("top_k", None)
90
  kwargs.pop("repetition_penalty", None)
91
  params.set_search_options(**kwargs)
 
111
  print("🚀 初始化中...")
112
  try:
113
  模型路径 = 下载模型()
 
114
  try:
115
  import onnxruntime as ort
116
  ort.set_default_logger_severity(3)
 
138
  分词器 = None
139
 
140
  # ================ 生成(流式) ================
141
+ def 流式回复(用户消息: str, 历史: List[List[str]],
142
+ 回复长度: int, 温度: float,
143
+ 记忆轮数: int, 上下文tokens: int):
144
  if not 模型 or not 分词器:
145
+ yield "❌ 模型未加载,请稍后重试", 0
146
  return
147
  try:
 
148
  if 记忆轮数 > 0 and len(历史) > 记忆轮数:
149
  历史 = 历史[-记忆轮数:]
150
 
151
+ 提示 = 构建模板(系统提示默认, 历史, 用户消息)
152
  输入ID = 分词器.encode(提示)
153
  输入ID = 按窗口裁剪(输入ID, 回复长度, min(上下文tokens, 上下文窗口))
154
 
 
168
  流 = 分词器.create_stream()
169
 
170
  回复 = ""
171
+ t = 0
172
  while not 生成器.is_done():
173
  生成器.compute_logits()
174
  生成器.generate_next_token()
 
176
  片段 = 流.decode(新)
177
  if not 片段:
178
  continue
 
179
  回复 += 片段
180
+ t += 1
181
+
182
  if 结束标记 in 回复:
183
  回复 = 回复.split(结束标记)[0].rstrip()
184
+ yield 回复, t
185
  break
186
 
187
+ if t % 6 == 0:
188
+ yield 回复, t
 
 
189
  else:
190
+ yield 回复.strip(), t
191
 
192
  del 生成器, params
193
  gc.collect()
 
194
  except Exception as e:
195
+ yield f"❌ 生成错误: {str(e)}", 0
196
 
197
+ # ================== UI(全功能 + 自适应) ==================
198
  css = """
199
+ /* 页面:顶部标题 + 选项卡 +(聊天页:聊天+输入区网格) */
200
+ html, body { height: 100%; }
201
+ .gradio-container { max-width: 1100px !important; margin: 0 auto; }
202
+
203
+ /* 顶部标题条 */
204
+ #app_hdr {
205
+ background: linear-gradient(135deg, #6d28d9 0%, #ec4899 100%);
206
+ color: #fff; padding: 14px 16px; border-radius: 12px;
207
+ box-shadow: 0 4px 20px rgba(109,40,217,.25);
 
 
 
 
 
 
 
 
208
  }
209
+ #app_hdr h1 { margin: 0; font-size: 20px; }
210
+ #app_hdr p { margin: 4px 0 0; font-size: 12px; opacity: .95; }
211
 
212
+ /* 聊天页布局:自适应高度,输入区固定在底部,不会被遮挡 */
213
+ #chat_layout {
214
+ height: calc(100dvh - 160px); /* 留出标题和tabs空间 */
215
+ display: grid; grid-template-rows: 1fr auto; gap: 8px;
 
 
 
216
  }
217
+ #chat_scroll {
218
+ min-height: 0; overflow: auto;
219
+ background: #fff; border-radius: 10px;
220
+ box-shadow: 0 2px 10px rgba(0,0,0,.06);
221
+ padding: 6px;
 
222
  }
223
+ #input_bar {
224
+ background: #fff; border-radius: 10px;
225
+ box-shadow: 0 2px 10px rgba(0,0,0,.06);
 
 
 
226
  padding: 8px;
227
  }
228
 
229
+ /* 小屏优化 */
230
  @media (max-width: 640px) {
231
+ #app_hdr h1 { font-size: 18px; }
232
+ #app_hdr p { font-size: 11px; }
233
+ #chat_layout { height: calc(100dvh - 150px); }
 
 
 
 
 
234
  }
235
  """
236
 
237
+ # 初始历史
238
+ 初始历史, 初始元数据 = 历史管理.加载("默认会话")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
  with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
241
+ 会话ID = gr.State("默认会话")
242
+ 系统提示状态 = gr.State(系统提示默认)
243
+
244
+ # 顶部
245
+ with gr.Column(elem_id="app_hdr"):
246
+ gr.Markdown("### 💬 Phi-4 中文助手")
247
+ gr.Markdown("流式生成 · 自动保存 · 多会话 · 上下文可调 · 继续/停止 · 导入导出")
248
+
249
+ with gr.Tabs():
250
+ # ========= 聊天 Tab =========
251
+ with gr.Tab("💬 聊天"):
252
+ with gr.Column(elem_id="chat_layout"):
253
+ with gr.Column(elem_id="chat_scroll"):
254
+ 聊���框 = gr.Chatbot(
255
+ value=初始历史,
256
+ type="tuples", # 与 [[user, assistant], ...] 兼容
257
+ show_copy_button=True,
258
+ height="100%" # 由外层容器控制实际高度
259
+ )
260
+ with gr.Column(elem_id="input_bar"):
261
+ with gr.Row():
262
+ 消息 = gr.Textbox(
263
+ placeholder="输入你的消息…(Enter 发送,Shift+Enter 换行)",
264
+ scale=8, lines=1, max_lines=4, container=False
265
+ )
266
+ 发送 = gr.Button("发送", variant="primary", scale=1)
267
+ with gr.Row():
268
+ 清空 = gr.Button("🗑️ 清空", size="sm")
269
+ 撤销 = gr.Button("↩️ 撤销", size="sm")
270
+ 重试 = gr.Button("🔄 重试", size="sm")
271
+ 继续 = gr.Button("⏭️ 继续", size="sm")
272
+ 停止 = gr.Button("⏹️ 停止", size="sm")
273
+ token计数 = gr.Markdown("Tokens: 0")
274
+
275
+ # ========= 会话与设置 Tab =========
276
+ with gr.Tab("⚙️ 会话与设置"):
277
  with gr.Row():
278
+ with gr.Column(scale=1):
279
+ gr.Markdown("#### 🎯 预设")
280
+ 预设精准 = gr.Button("📏 精准", size="sm")
281
+ 预设平衡 = gr.Button("📘 平衡", size="sm")
282
+ 预设创意 = gr.Button("🎨 创意", size="sm")
283
+ gr.Markdown("#### 💬 系统提示词")
284
+ 系统提示框 = gr.Textbox(
285
+ label="系统提示词(影响风格)",
286
+ value=系统提示默认, lines=3
287
+ )
288
+ with gr.Column(scale=2):
289
+ gr.Markdown("#### 🔧 生成参数")
290
+ with gr.Row():
291
+ 最大生成长度 = gr.Slider(50, 1024, value=默认回复长度, step=10, label="📝 最大生成长度 (tokens)")
292
+ 温度 = gr.Slider(0.0, 1.2, value=0.7, step=0.1, label="🌡️ 温度")
293
+ with gr.Row():
294
+ top_p = gr.Slider(0.1, 1.0, value=0.9, step=0.05, label="🎲 Top-p")
295
+ top_k = gr.Slider(1, 100, value=40, step=1, label="🔝 Top-k")
296
+ with gr.Row():
297
+ 记忆轮数 = gr.Slider(1, 12, value=6, step=1, label="🧠 记忆轮数(保留最近N轮)")
298
+ 上下文限制 = gr.Slider(512, 上下文窗口, value=上下文窗口, step=64, label=f"📚 上下文上限 (≤{上下文窗口})")
299
+
300
+ gr.Markdown("#### 💾 会话管理")
301
  with gr.Row():
302
+ 会话列表 = gr.Dropdown(
303
+ label="会话列表", choices=(历史管理.列表() or ["默认会话"]),
304
+ value="默认会话", interactive=True
305
+ )
306
+ 加载 = gr.Button("📂 加载", size="sm")
307
+ 保存 = gr.Button("💾 保存", size="sm")
308
+ 新建 = gr.Button("➕ 新建", size="sm")
309
  with gr.Row():
310
+ 导入文件 = gr.File(label="导入JSON", file_types=[".json"])
311
+ 导入 = gr.Button("⬆️ 导入", size="sm")
312
+ 导出 = gr.Button("⬇️ 导出当前会话", size="sm")
313
+ 导出文件 = gr.File(label="导出文件", interactive=False)
314
+
315
+ # ========= 逻�� =========
316
+ # 预设
317
+ def 用预设(模式):
318
+ if 模式 == "精准":
319
+ return 200, 0.2, 0.85, 20
320
+ if 模式 == "创意":
321
+ return 500, 0.9, 0.95, 60
322
+ return 300, 0.7, 0.9, 40 # 平衡
323
+ 预设精准.click(lambda: 用预设("精准"), outputs=[最大生成长度, 温度, top_p, top_k])
324
+ 预设平衡.click(lambda: 用预设("平衡"), outputs=[最大生成长度, 温度, top_p, top_k])
325
+ 预设创意.click(lambda: 用预设("创意"), outputs=[最大生成长度, 温度, top_p, top_k])
326
+
327
+ # 系统提示词
328
+ def 更新系统提示(s):
329
+ return s.strip() if s.strip() else 系统提示默认
330
+ 系统提示框.change(更新系统提示, 系统提示框, 系统提示状态)
331
+
332
+ # 基础交互
333
+ def 用户提交(msg, hist):
334
+ msg = (msg or "").strip()
335
+ if not msg:
336
+ return "", hist
337
+ return "", hist + [[msg, None]]
338
+
339
+ def 机器人应答(hist, sys_prompt_state, max_len, temp, tp, tk, keep_rounds, ctx_limit, sid):
340
+ # 临时把系统提示覆盖为用户设置的值
341
+ global 系统提示默认
342
+ 系统提示默认 = sys_prompt_state
343
+
344
+ if not hist or hist[-1][1] is not None:
345
+ return hist, gr.update(value="Tokens: 0")
346
+ 用户消息 = hist[-1][0]
347
+ hist[-1][1] = ""
348
+ latest = ""
349
+ for latest, t in 流式回复(
350
  用户消息=用户消息,
351
+ 历史=hist[:-1],
352
+ 回复长度=int(max_len),
353
+ 温度=float(temp),
354
+ 记忆轮数=int(keep_rounds),
355
+ 上下文tokens=int(ctx_limit),
356
  ):
357
+ hist[-1][1] = latest
358
+ yield hist, gr.update(value=f"Tokens: {t}")
359
+ 历史管理.保存(hist, sid, {"system_prompt": sys_prompt_state})
360
 
361
+ # 发送
362
+ 提交_evt = 消息.submit(
363
  用户提交, [消息, 聊天框], [消息, 聊天框], queue=False
364
  ).then(
365
+ 机器人应答, [聊天框, 系统提示状态, 最大生成长度, 温度, top_p, top_k, 记忆轮数, 上下文限制, 会话ID],
366
+ [聊天框, token计数]
367
  )
368
+ 点击_evt = 发送.click(
 
369
  用户提交, [消息, 聊天框], [消息, 聊天框], queue=False
370
  ).then(
371
+ 机器人应答, [聊天框, 系统提示状态, 最大生成长度, 温度, top_p, top_k, 记忆轮数, 上下文限制, 会话ID],
372
+ [聊天框, token计数]
373
+ )
374
+
375
+ # 继续
376
+ def 继续输出(hist):
377
+ if not hist:
378
+ return hist
379
+ return hist + [["请从上句继续输出。", None]]
380
+ 继续.click(继续输出, 聊天框, 聊天框).then(
381
+ 机器人应答, [聊天框, 系统提示状态, 最大生成长度, 温度, top_p, top_k, 记忆轮数, 上下文限制, 会话ID],
382
+ [聊天框, token计数]
383
  )
384
 
385
+ # 停止(取消队列中事件)
386
+ 停止.click(fn=None, inputs=None, outputs=None, cancels=[提交_evt, 点击_evt])
387
 
388
+ # 清空/撤销/重试
389
  清空.click(lambda: [], None, 聊天框)
390
  撤销.click(lambda h: h[:-1] if h else h, 聊天框, 聊天框)
391
+ def 重试一轮(h):
392
+ if not h: return h
393
+ return h[:-1] + [[h[-1][0], None]]
 
 
 
394
  重试.click(重试一轮, 聊天框, 聊天框).then(
395
+ 机器人应答, [聊天框, 系统提示状态, 最大生成长度, 温度, top_p, top_k, 记忆轮数, 上下文限制, 会话ID],
396
+ [聊天框, token计数]
397
  )
398
 
399
+ # 会话管理
400
+ def 保存当前(hist, sid, sys_prompt):
401
+ 历史管理.保存(hist, sid, {"system_prompt": sys_prompt})
402
+ return gr.update(choices=历史管理.列表())
403
+ 保存.click(保存当前, [聊天框, 会话ID, 系统提示状态], 会话列表)
404
+
405
+ def 加载会话(sid):
406
+ h, meta = 历史管理.加载(sid)
407
+ sp = meta.get("system_prompt", 系统提示默认)
408
+ return h, sid, sp
409
+ 加载.click(加载会话, 会话列表, [聊天框, 会话ID, 系统提示框])
410
+
411
+ def 新建会话():
412
+ sid = f"会话_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
413
+ 历史管理.保存([], sid, {"system_prompt": 系统提示默认})
414
+ return [], sid, gr.update(choices=历史管理.列表(), value=sid), 系统提示默认, gr.update(value=系统提示默认)
415
+ 新建.click(新建会话, outputs=[聊天框, 会话ID, 会话列表, 系统提示状态, 系统提示框])
416
+
417
+ # 导入 / 导出
418
+ def 导出(hist, sid):
419
+ 历史管理.保存(hist, sid, {"system_prompt": 系统提示状态.value})
420
+ return 历史管理.路径(sid)
421
+ 导出.click(导出, [聊天框, 会话ID], 导出文件)
422
+
423
+ def 导入(file, sid):
424
+ if file is None:
425
+ return gr.update(), gr.update()
426
+ try:
427
+ with open(file.name, "r", encoding="utf-8") as f:
428
+ js = json.load(f)
429
+ h = js.get("history", [])
430
+ meta = js.get("meta", {})
431
+ sp = meta.get("system_prompt", 系统提示默认)
432
+ 历史管理.保存(h, sid, {"system_prompt": sp})
433
+ return h, sp
434
+ except Exception as e:
435
+ return gr.update(), gr.update(value=f"导入失败: {e}")
436
+ 导入.click(导入, [导入文件, 会话ID], [聊天框, 系统提示框])
437
+
438
  # 自动保存
439
+ 聊天框.change(lambda h, sid, sp: 历史管理.保存(h, sid, {"system_prompt": sp}) if h else None,
440
+ [聊天框, 会话ID, 系统提示状态], None)
441
+
442
+ # 首次加载:保证默认会话存在 + 列表更新
443
+ def 初始化():
444
+ if "默认会话" not in 历史管理.列表():
445
+ 历史管理.保存(初始历史 or [], "默认会话", {"system_prompt": 系统提示默认})
446
+ return gr.update(choices=历史管理.列表(), value="默认会话")
447
+ demo.load(初始化, outputs=会话列表)
448
 
449
  if __name__ == "__main__":
450
  if 模型:
451
  print("🎉 启动服务...")
452
+ demo.queue(max_size=16).launch(server_name="0.0.0.0", server_port=7860, share=False)
453
  else:
454
  print("❌ 无法启动")