sofiamak commited on
Commit
a70f60a
·
verified ·
1 Parent(s): 5db8941

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +117 -149
app.py CHANGED
@@ -1,7 +1,7 @@
1
- import json, os, html, random
2
  import gradio as gr
3
 
4
- # =============== 新增:DeepSeek 客户端(OpenAI 兼容) ===============
5
  try:
6
  from openai import OpenAI
7
  _HAS_OPENAI = True
@@ -11,51 +11,11 @@ try:
11
  )
12
  except Exception:
13
  _HAS_OPENAI = False
14
- # ================================================================
15
 
16
  APP_TITLE = "孙子你好"
17
- DATA_FILE = "playbook.json"
18
-
19
- # 读取固定答案库
20
- def load_kb():
21
- if not os.path.exists(DATA_FILE):
22
- raise RuntimeError("未找到 playbook.json,请先上传。")
23
- with open(DATA_FILE, "r", encoding="utf-8") as f:
24
- data = json.load(f)
25
- # 章节小副标(可根据需要增补)
26
- chap_hint = {
27
- "计篇":"先算后战","作战篇":"速战速决","谋攻篇":"上兵伐谋","军形篇":"先固己势",
28
- "兵势篇":"势能制胜","虚实篇":"避实击虚","军争篇":"抢占先机","九变篇":"因变制胜",
29
- "行军篇":"察情定位","地形篇":"因地用兵","九地篇":"九地九策","火攻篇":"借势放大",
30
- "用间篇":"情报为先","始计篇":"先计而后战"
31
- }
32
- for item in data:
33
- item["chapter_hint"] = chap_hint.get(item.get("chapter",""), "")
34
- item["triggers"] = item.get("triggers", [])
35
- return data
36
-
37
- KB = load_kb()
38
 
39
- def match_candidates(user_text: str):
40
- """
41
- 简单触发词计分 → 取最高分 intent → 返回该 intent 的条目池(打乱顺序)。
42
- 若无匹配,返回空池和提示,交给 DeepSeek 回退生成。
43
- """
44
- text = (user_text or "").lower()
45
- scored = []
46
- for it in KB:
47
- score = sum(1 for t in it.get("triggers", []) if t.lower() in text)
48
- if score > 0:
49
- scored.append((score, it))
50
- if not scored:
51
- return [], "(未匹配到明确场景,已用智能推荐生成)"
52
- scored.sort(key=lambda x: -x[0])
53
- top_intent = scored[0][1]["intent"]
54
- pool = [it for _, it in scored if it["intent"] == top_intent]
55
- random.shuffle(pool)
56
- return pool, ""
57
-
58
- # =============== 新增:将 DeepSeek 输出为与本地条目一致的结构 ===============
59
  CHAP_HINT = {
60
  "计篇":"先算后战","作战篇":"速战速决","谋攻篇":"上兵伐谋","军形篇":"先固己势",
61
  "兵势篇":"势能制胜","虚实篇":"避实击虚","军争篇":"抢占先机","九变篇":"因变制胜",
@@ -63,83 +23,17 @@ CHAP_HINT = {
63
  "用间篇":"情报为先","始计篇":"先计而后战"
64
  }
65
 
66
- def ai_generate_item(user_text: str) -> dict:
67
- """
68
- 调用 DeepSeek 让模型输出 JSON:
69
- {"chapter": "...", "quote": "...", "idea": "...", "methods": ["...","...","..."], "speech": "..."}
70
- 如失败则返回一个简易兜底条目。
71
- """
72
- if not _HAS_OPENAI:
73
- return {
74
- "chapter": "始计篇",
75
- "chapter_hint": CHAP_HINT.get("始计篇",""),
76
- "quote": "先计而后战。",
77
- "idea": "先捋清状况,再出手更稳。",
78
- "methods": ["明确目标与约束","先礼后兵,给台阶","设期限与分步方案"],
79
- "speech": "我先把情况梳理一下,给你两个可选方案,我们按这个节奏推进。"
80
- }
81
-
82
- sys = (
83
- "你是中文军师,用《孙子兵法》给出策略。"
84
- "请严格输出一个 JSON 对象,字段仅限:"
85
- "chapter(篇名,如“虚实篇/谋攻篇/始计篇”等)、"
86
- "quote(兵法原文,尽量贴切)、"
87
- "idea(思路,1-2句)、"
88
- "methods(数组,3条以内,简短可执行)、"
89
- "speech(一段可直接使用的话术,口吻礼貌、委婉、幽默有边界)。"
90
- )
91
- user = f"用户问题:{user_text}\n只输出 JSON。不要任何解释或前后缀。"
92
- try:
93
- resp = client.chat.completions.create(
94
- model="deepseek-chat",
95
- messages=[
96
- {"role":"system","content":sys},
97
- {"role":"user","content":user}
98
- ],
99
- temperature=0.6,
100
- max_tokens=600
101
- )
102
- content = resp.choices[0].message.content.strip()
103
- # 尝试提取 JSON
104
- import json as _json, re as _re
105
- # 清除围栏
106
- m = _re.search(r"\{.*\}", content, flags=_re.S)
107
- js = m.group(0) if m else content
108
- data = _json.loads(js)
109
- chapter = str(data.get("chapter","始计篇"))
110
- item = {
111
- "chapter": chapter,
112
- "chapter_hint": CHAP_HINT.get(chapter, ""),
113
- "quote": str(data.get("quote","先计而后战。")),
114
- "idea": str(data.get("idea","先捋清状况,再出手更稳。")),
115
- "methods": list(data.get("methods", []))[:3] or ["设清晰目标","给台阶与期限","分步推进"],
116
- "speech": str(data.get("speech","我们这样推进最稳,也方便彼此配合。"))
117
- }
118
- return item
119
- except Exception as e:
120
- # 兜底
121
- return {
122
- "chapter": "虚实篇",
123
- "chapter_hint": CHAP_HINT.get("虚实篇",""),
124
- "quote": "避实而击虚。",
125
- "idea": "避开正面冲突,转到对方更在意的点。",
126
- "methods": ["承诺结果而非加班","给两个选项让其选择","设置温和期限"],
127
- "speech": "我更关注把结果做好,不如按这个更稳的节奏来?您看哪种更合适。"
128
- }
129
- # =================================================================
130
-
131
  def render_plan(item, show_speech=False, tip_msg=""):
132
- """渲染完整方案;show_speech=True 时附带话术卡片"""
133
  chap = html.escape(item.get("chapter",""))
134
- hint = html.escape(item.get("chapter_hint",""))
135
  quote= html.escape(item.get("quote",""))
136
- idea = html.escape(item.get("idea",""))
137
- methods = item.get("methods", [])
138
- methods_html = "".join([f"<div> {i+1}. {html.escape(m)}</div>" for i,m in enumerate(methods)])
139
  speech = html.escape(item.get("speech","")).replace("\n","<br>")
140
 
141
  speech_html = f"""
142
- <div style="margin-top:10px;padding:12px 14px;border:1px dashed #e2d7f2;border-radius:10px;background:#fbf8ff;">
143
  <div style="font-weight:600;">💬 话术示例:</div>
144
  <div>{speech}</div>
145
  </div>
@@ -153,15 +47,13 @@ def render_plan(item, show_speech=False, tip_msg=""):
153
  🧠 《{chap}》:{hint}<br><br>
154
  「{quote}」<br><br>
155
  ⸻<br><br>
156
- <span style="font-weight:600;">思路:</span>{idea}<br><br>
157
- <span style="font-weight:600;">做法:</span>
158
- {methods_html}
159
  {speech_html}
160
  </div>
161
  """
162
 
163
  def render_speech_only(item):
164
- """只显示话术示例卡片"""
165
  speech = html.escape(item.get("speech","")).replace("\n", "<br>")
166
  return f"""
167
  <div style="font-size:22px;line-height:1.8em;padding:20px;background:#fbf8ff;border-radius:12px;border:1px solid #e2d7f2;">
@@ -170,7 +62,6 @@ def render_speech_only(item):
170
  </div>
171
  """
172
 
173
- # 为了控制按钮显隐,统一一个输出打包方法
174
  def pack_outputs(html_out, pool, idx, show_flag, show_switch, show_speech_btn, show_back, show_again, user_text_keep=True):
175
  return (
176
  html_out,
@@ -184,42 +75,125 @@ def pack_outputs(html_out, pool, idx, show_flag, show_switch, show_speech_btn, s
184
  (gr.update() if user_text_keep else gr.update(value="")) # Textbox
185
  )
186
 
187
- # ---- 回调:生成 / 换兵法 / 只看话术 / 返回兵法 / 重新提问 ----
188
- def on_generate(user_text, state_pool, state_idx, state_show):
189
- pool, tip = match_candidates(user_text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
- # 若没命中本地语料:调用 DeepSeek 生成一个“条目”,塞入池
192
- if not pool:
193
- ai_item = ai_generate_item(user_text)
194
- pool = [ai_item]
195
 
196
- # 生成后:显示“换兵法”“显示话术示例”“问孙子其他问题”,不显示“返回兵法”
197
- html_out = render_plan(pool[0], False, tip)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  return pack_outputs(html_out, pool, 0, False, True, True, False, True, True)
199
 
200
- def on_switch(state_pool, state_idx, state_show):
201
- pool = state_pool or []
202
- if not pool:
203
  return pack_outputs("<div style='font-size:20px;'>请先输入你的问题并点击生成兵法。</div>",
204
- pool, state_idx, state_show, False, False, False, False, True)
205
- idx = (state_idx + 1) % len(pool)
206
- # 切换后:仍显示“换兵法”“显示话术示例”“问孙子其他问题”,隐藏“返回兵法”
207
- html_out = render_plan(pool[idx], False, "")
208
- return pack_outputs(html_out, pool, idx, False, True, True, False, True, True)
209
 
210
  def on_show_speech(state_pool, state_idx, state_show):
211
- """只显示话术示例;此时需要出现“返回兵法”"""
212
  pool = state_pool or []
213
  if not pool:
214
  return pack_outputs("<div style='font-size:20px;'>请先输入你的问题并点击生成兵法。</div>",
215
  pool, state_idx, state_show, False, False, False, False, True)
216
  idx = max(0, min(state_idx, len(pool)-1))
217
  html_out = render_speech_only(pool[idx])
218
- # 此时:显示“返回兵法”“换兵法”“显示话术示例”“问孙子其他问题”都可以保留
219
  return pack_outputs(html_out, pool, idx, True, True, True, True, True, True)
220
 
221
  def on_back(state_pool, state_idx, state_show):
222
- """从“只看话术”返回到完整兵法视图;隐藏‘返回兵法’"""
223
  pool = state_pool or []
224
  if not pool:
225
  return pack_outputs("<div style='font-size:20px;'>请先输入你的问题并点击生成兵法。</div>",
@@ -229,13 +203,9 @@ def on_back(state_pool, state_idx, state_show):
229
  return pack_outputs(html_out, pool, idx, False, True, True, False, True, True)
230
 
231
  def on_again(state_pool, state_idx, state_show):
232
- """回到初始状态:清空输出与输入,只显示生成兵法"""
233
- html_out = ""
234
- empty_pool = []
235
- # 初始:只有“生成兵法”可见(其它按钮隐藏)
236
- return pack_outputs(html_out, empty_pool, 0, False, False, False, False, False, user_text_keep=False)
237
 
238
- # ---- UI ----
239
  theme = gr.themes.Soft(primary_hue="pink", neutral_hue="slate")
240
  CSS = """
241
  body,.gradio-container{background:#f7f6fb!important;}
@@ -257,16 +227,14 @@ with gr.Blocks(title=APP_TITLE, theme=theme, css=CSS, analytics_enabled=False) a
257
 
258
  out = gr.HTML()
259
 
260
- # 状态:候选池、当前下标、是否处于“只看话术”
261
  state_pool = gr.State([])
262
  state_idx = gr.State(0)
263
  state_show = gr.State(False)
264
 
265
- # 事件绑定(注意输出多了按钮可见性与文本框)
266
  btn_gen.click(on_generate, inputs=[user_text, state_pool, state_idx, state_show],
267
  outputs=[out, state_pool, state_idx, state_show, btn_switch, btn_speech, btn_back, btn_again, user_text])
268
 
269
- btn_switch.click(on_switch, inputs=[state_pool, state_idx, state_show],
270
  outputs=[out, state_pool, state_idx, state_show, btn_switch, btn_speech, btn_back, btn_again, user_text])
271
 
272
  btn_speech.click(on_show_speech, inputs=[state_pool, state_idx, state_show],
 
1
+ import os, json, html, re
2
  import gradio as gr
3
 
4
+ # =============== DeepSeek 客户端(OpenAI 兼容) ===============
5
  try:
6
  from openai import OpenAI
7
  _HAS_OPENAI = True
 
11
  )
12
  except Exception:
13
  _HAS_OPENAI = False
14
+ # ============================================================
15
 
16
  APP_TITLE = "孙子你好"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
+ # 章节小副标(可按需修改)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  CHAP_HINT = {
20
  "计篇":"先算后战","作战篇":"速战速决","谋攻篇":"上兵伐谋","军形篇":"先固己势",
21
  "兵势篇":"势能制胜","虚实篇":"避实击虚","军争篇":"抢占先机","九变篇":"因变制胜",
 
23
  "用间篇":"情报为先","始计篇":"先计而后战"
24
  }
25
 
26
+ # ---------- 渲染 ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  def render_plan(item, show_speech=False, tip_msg=""):
 
28
  chap = html.escape(item.get("chapter",""))
29
+ hint = html.escape(CHAP_HINT.get(chap,""))
30
  quote= html.escape(item.get("quote",""))
31
+ insight = html.escape(item.get("insight",""))
32
+ action = html.escape(item.get("action",""))
 
33
  speech = html.escape(item.get("speech","")).replace("\n","<br>")
34
 
35
  speech_html = f"""
36
+ <div style="margin-top:12px;padding:12px 14px;border:1px dashed #e2d7f2;border-radius:10px;background:#fbf8ff;">
37
  <div style="font-weight:600;">💬 话术示例:</div>
38
  <div>{speech}</div>
39
  </div>
 
47
  🧠 《{chap}》:{hint}<br><br>
48
  「{quote}」<br><br>
49
  ⸻<br><br>
50
+ <span style="font-weight:600;">启示:</span>{insight}<br><br>
51
+ <span style="font-weight:600;">做法:</span>{action}
 
52
  {speech_html}
53
  </div>
54
  """
55
 
56
  def render_speech_only(item):
 
57
  speech = html.escape(item.get("speech","")).replace("\n", "<br>")
58
  return f"""
59
  <div style="font-size:22px;line-height:1.8em;padding:20px;background:#fbf8ff;border-radius:12px;border:1px solid #e2d7f2;">
 
62
  </div>
63
  """
64
 
 
65
  def pack_outputs(html_out, pool, idx, show_flag, show_switch, show_speech_btn, show_back, show_again, user_text_keep=True):
66
  return (
67
  html_out,
 
75
  (gr.update() if user_text_keep else gr.update(value="")) # Textbox
76
  )
77
 
78
+ # ================= DeepSeek 生成(短模板) =================
79
+ GEN_SYS = (
80
+ "你是中文军师,用《孙子兵法》给出极简、可执行的建议。"
81
+ "严格只输出一个 JSON 对象,字段仅限:"
82
+ "chapter(篇名,如“虚实篇/谋攻篇/始计篇”等)、"
83
+ "quote(贴切原文,保留原句标点)、"
84
+ "insight(≤18字,人话,直白精炼)、"
85
+ "action(仅1条,≤40字,具体到动作/步骤/策略,不要泛泛而谈)、"
86
+ "speech(一段≤80字、可直接对对方说的话;礼貌、委婉、幽默有边界)。"
87
+ "不要输出除 JSON 以外的任何内容。"
88
+ )
89
+
90
+ # few-shot:锚定“短、实、口语化”
91
+ GEN_FEWSHOT = [
92
+ {
93
+ "role":"user",
94
+ "content":"用户问题:老板让我临时加班但我状态很差,如何拒绝更体面?只输出JSON。"
95
+ },
96
+ {
97
+ "role":"assistant",
98
+ "content":json.dumps({
99
+ "chapter":"作战篇",
100
+ "quote":"夫兵久而国利者,未之有也。",
101
+ "insight":"别打消耗战,用质量说话。",
102
+ "action":"承诺结果与时间:给“今晚初稿/明早终版”两选项,让对方决定。",
103
+ "speech":"老板,我怕今晚赶出来质量不稳。不如我明早7:30交完整版本,保证更稳,您看可以吗?"
104
+ }, ensure_ascii=False)
105
+ }
106
+ ]
107
 
108
+ def _extract_json(text: str) -> dict:
109
+ m = re.search(r"\{.*\}", text, flags=re.S)
110
+ js = m.group(0) if m else text
111
+ return json.loads(js)
112
 
113
+ def ai_generate_item(user_text: str, alt: bool=False) -> dict:
114
+ """
115
+ DeepSeek直生:给出 chapter/quote/insight/action/speech。
116
+ alt=True 时要求更换角度/章节,避免重复。
117
+ """
118
+ if not _HAS_OPENAI:
119
+ # 无 API 的兜底
120
+ return {
121
+ "chapter": "始计篇",
122
+ "quote": "先计而后战。",
123
+ "insight": "先摸清再出手。",
124
+ "action": "列两种可行方案并承诺时间点,让对方选。",
125
+ "speech": "我这边按更稳的节奏推进更安全。给您两个方案,咱们选一个最稳的。"
126
+ }
127
+
128
+ user = (
129
+ f"用户问题:{user_text}\n"
130
+ "请从《孙子兵法》全书挑选最贴切的一句原文,并按上述字段输出。"
131
+ "只输出 JSON。"
132
+ )
133
+ if alt:
134
+ user += "\n请更换角度或章节,避免与上一次重复;确保action是全新思路。"
135
+
136
+ msgs = [{"role":"system","content":GEN_SYS}] + GEN_FEWSHOT + [{"role":"user","content":user}]
137
+
138
+ resp = client.chat.completions.create(
139
+ model="deepseek-chat",
140
+ messages=msgs,
141
+ temperature=0.3 if not alt else 0.6,
142
+ max_tokens=480
143
+ )
144
+ content = resp.choices[0].message.content.strip()
145
+ try:
146
+ data = _extract_json(content)
147
+ chapter = str(data.get("chapter","始计篇")).strip()
148
+ item = {
149
+ "chapter": chapter,
150
+ "quote": str(data.get("quote","先计而后战。")).strip(),
151
+ "insight": str(data.get("insight","先捋清状况,再出手。")).strip(),
152
+ "action": str(data.get("action","给出两种稳妥方案并承诺时间点。")).strip(),
153
+ "speech": str(data.get("speech","我们按更稳的节奏推进,彼此都放心。")).strip()
154
+ }
155
+ return item
156
+ except Exception:
157
+ # 解析失败兜底
158
+ return {
159
+ "chapter": "虚实篇",
160
+ "quote": "避实而击虚。",
161
+ "insight": "绕开硬碰硬。",
162
+ "action": "承诺结果,改为更稳时间点完成,给对方选择权。",
163
+ "speech": "我更关注把结果做好。不如按这个更稳的节奏来?您看哪种合适。"
164
+ }
165
+
166
+ # ----------------- 回调(沿用你的按钮逻辑) -----------------
167
+ def on_generate(user_text, state_pool, state_idx, state_show):
168
+ user_text = (user_text or "").strip()
169
+ if not user_text:
170
+ return pack_outputs("<div style='font-size:20px;'>请先输入你的问题。</div>",
171
+ [], 0, False, False, False, False, False, True)
172
+ item = ai_generate_item(user_text, alt=False)
173
+ pool = [item]
174
+ html_out = render_plan(item, False, "")
175
  return pack_outputs(html_out, pool, 0, False, True, True, False, True, True)
176
 
177
+ def on_switch(state_pool, state_idx, state_show, user_text):
178
+ user_text = (user_text or "").strip()
179
+ if not user_text:
180
  return pack_outputs("<div style='font-size:20px;'>请先输入你的问题并点击生成兵法。</div>",
181
+ [], 0, False, False, False, False, False, True)
182
+ item = ai_generate_item(user_text, alt=True)
183
+ pool = [item]
184
+ html_out = render_plan(item, False, "")
185
+ return pack_outputs(html_out, pool, 0, False, True, True, False, True, True)
186
 
187
  def on_show_speech(state_pool, state_idx, state_show):
 
188
  pool = state_pool or []
189
  if not pool:
190
  return pack_outputs("<div style='font-size:20px;'>请先输入你的问题并点击生成兵法。</div>",
191
  pool, state_idx, state_show, False, False, False, False, True)
192
  idx = max(0, min(state_idx, len(pool)-1))
193
  html_out = render_speech_only(pool[idx])
 
194
  return pack_outputs(html_out, pool, idx, True, True, True, True, True, True)
195
 
196
  def on_back(state_pool, state_idx, state_show):
 
197
  pool = state_pool or []
198
  if not pool:
199
  return pack_outputs("<div style='font-size:20px;'>请先输入你的问题并点击生成兵法。</div>",
 
203
  return pack_outputs(html_out, pool, idx, False, True, True, False, True, True)
204
 
205
  def on_again(state_pool, state_idx, state_show):
206
+ return pack_outputs("", [], 0, False, False, False, False, False, user_text_keep=False)
 
 
 
 
207
 
208
+ # ----------------- UI(保持你的样式与顺序) -----------------
209
  theme = gr.themes.Soft(primary_hue="pink", neutral_hue="slate")
210
  CSS = """
211
  body,.gradio-container{background:#f7f6fb!important;}
 
227
 
228
  out = gr.HTML()
229
 
 
230
  state_pool = gr.State([])
231
  state_idx = gr.State(0)
232
  state_show = gr.State(False)
233
 
 
234
  btn_gen.click(on_generate, inputs=[user_text, state_pool, state_idx, state_show],
235
  outputs=[out, state_pool, state_idx, state_show, btn_switch, btn_speech, btn_back, btn_again, user_text])
236
 
237
+ btn_switch.click(on_switch, inputs=[state_pool, state_idx, state_show, user_text],
238
  outputs=[out, state_pool, state_idx, state_show, btn_switch, btn_speech, btn_back, btn_again, user_text])
239
 
240
  btn_speech.click(on_show_speech, inputs=[state_pool, state_idx, state_show],