Spaces:
Runtime error
Runtime error
File size: 20,266 Bytes
d99ae44 1ecaafa d99ae44 cdf96a9 77f08ac cdf96a9 cdf70d4 cdf96a9 d99ae44 77f08ac d99ae44 20cb253 cdf70d4 20cb253 d99ae44 cdf96a9 d99ae44 cdf96a9 d99ae44 cdf96a9 77f08ac cdf96a9 77f08ac d99ae44 cdf96a9 d99ae44 cdf70d4 20cb253 cdf96a9 d99ae44 cdf96a9 77f08ac cdf96a9 77f08ac cdf96a9 77f08ac cdf70d4 20cb253 d99ae44 cdf70d4 20cb253 d99ae44 cdf70d4 20cb253 cdf96a9 20cb253 ab34c21 20cb253 77f08ac cdf96a9 77f08ac cdf96a9 77f08ac 20cb253 cdf96a9 77f08ac d99ae44 77f08ac cdf96a9 77f08ac cdf96a9 ab34c21 cdf96a9 77f08ac d99ae44 77f08ac d99ae44 77f08ac d99ae44 20cb253 77f08ac 20cb253 77f08ac cdf96a9 20cb253 ab34c21 20cb253 cdf96a9 77f08ac cdf96a9 ab34c21 20cb253 cdf96a9 77f08ac cdf96a9 20cb253 77f08ac 20cb253 cdf96a9 20cb253 cdf70d4 20cb253 cdf96a9 1ecaafa cdf96a9 1ecaafa c6e4036 1ecaafa cdf96a9 20cb253 ab34c21 cdf96a9 ab34c21 20cb253 ab34c21 20cb253 ab34c21 20cb253 ab34c21 cdf96a9 ab34c21 20cb253 ab34c21 cdf96a9 ab34c21 20cb253 ab34c21 20cb253 ab34c21 20cb253 ab34c21 20cb253 ab34c21 20cb253 ab34c21 20cb253 ab34c21 20cb253 ab34c21 20cb253 cdf96a9 20cb253 1ecaafa | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 | import os, json, html, requests, re, random
import gradio as gr
APP_TITLE = "孙子你好(DeepSeek 多样化·诊断版)"
# 可通过 Secrets 覆盖:
# DEEPSEEK_API_KEY = sk-xxxxxxxx
# DEEPSEEK_MODEL = deepseek-chat(默认)
# DEEPSEEK_BASE_URL = https://api.deepseek.com/v1/chat/completions(多数账号)
# 或 https://api.deepseek.com/chat/completions(少量旧线路)
MODEL_NAME = os.getenv("DEEPSEEK_MODEL", "deepseek-chat").strip() or "deepseek-chat"
# ===================== 系统提示词(增加 explain_cn 字段 + 做法语气要求) =====================
SYS_PROMPT = (
"你是中文军师,擅长将《孙子兵法》的智慧用于现实问题。\n"
"任务:针对用户的问题,从《孙子兵法》十三篇中找出最贴合的一章,引用原文(若能确定),并转化为现代生活策略。\n\n"
"规则:\n"
"1. 优先根据问题性质挑选不同篇章:\n"
" - 计划/评估:〈始计篇〉〈作战篇〉\n"
" - 谈判/攻防:〈谋攻篇〉〈虚实篇〉\n"
" - 节奏/风险:〈军形篇〉〈兵势篇〉\n"
" - 主动出击/竞争:〈军争篇〉〈九变篇〉〈地形篇〉\n"
" - 困境/沟通:〈行军篇〉〈九地篇〉〈用间篇〉〈火攻篇〉\n"
"2. 若问题重复,尽量换不同篇章或换角度回答,保持多样性。\n"
"3. 若无法百分之百确认原文或篇名,不要硬编,改为思想意译并标记 unsure=true。\n"
"4. 严格仅输出 JSON,固定字段:\n"
" {\"chapter\":\"<篇名>\","
" \"quote\":\"<原文或思想>\","
" \"unsure\":false/true,"
" \"insight\":\"<一句人话启示≤24字>\","
" \"action\":\"<一步可执行的做法≤100字,用轻松幽默的口吻表达,但不要改变建议本身的含义>\","
" \"speech\":\"<直接可说的话术≤120字>\","
" \"explain_cn\":\"<用非常简单的话解释这句名言的大意≤60字>\"}\n"
"5. 语气简洁、机智、委婉、有边界;不得人身攻击、威胁或违法建议。\n"
"6. 禁止输出 JSON 以外的任何文字。"
)
# few-shot:覆盖多篇章,减少模型只用某一章的倾向
FEWSHOT = [
# 加班 → 作战篇
{
"role": "user",
"content": "问题:老板临时要我通宵加班,我状态很差,怎么体面拒绝?只输出JSON。"
},
{
"role": "assistant",
"content": json.dumps({
"chapter": "作战篇",
"quote": "夫兵久而国利者,未之有也。",
"unsure": False,
"insight": "避免持久消耗,保整体战力。",
"action": "跟老板提一个“明早高质量交付”的方案,比硬熬一整晚靠谱多了,也能保证你明天还是个清醒的人。",
"speech": "今晚赶出来质量可能不稳,我建议明早整理成终版,保证更可靠,您看可以吗?",
"explain_cn": "意思是战争拖太久没有好处,打仗要快准狠,不能一直消耗。"
}, ensure_ascii=False)
},
# 借钱 → 始计篇
{
"role": "user",
"content": "问题:朋友借钱不还,怎样体面要回?只输出JSON。"
},
{
"role": "assistant",
"content": json.dumps({
"chapter": "始计篇",
"quote": "兵者,诡道也。",
"unsure": False,
"insight": "绕开硬催,设局顺势收回。",
"action": "就说自己在清账、要安排资金,先把一部分或者分期要回来,既能把钱收回,又不至于把气氛搞得像开战。",
"speech": "我这边在清账需要安排下,能否先转一部分?分期也行,别影响咱们交情。",
"explain_cn": "打仗要讲策略,不是直来直去,要懂得绕弯子、用计谋。"
}, ensure_ascii=False)
},
# 邻居噪音 → 行军篇
{
"role": "user",
"content": "问题:邻居太吵,怎么友好沟通?只输出JSON。"
},
{
"role": "assistant",
"content": json.dumps({
"chapter": "行军篇",
"quote": "知彼知己,百战不殆。(通用于沟通前的情势判断)",
"unsure": True, # 行军篇原文无此句,标不确定,演示 unsure 机制
"insight": "先辨来源时机,再择机沟通。",
"action": "挑对方不忙不生气的时候,先表示理解,再轻轻说出自己的困扰,让TA自己不好意思再把家当KTV用。",
"speech": "最近晚上声音有点明显,我怕打扰到楼上楼下。能否帮忙晚间稍微注意下?谢谢啦~",
"explain_cn": "先搞清楚对方情况和自己底牌,这样做事比较稳,不容易吃亏。"
}, ensure_ascii=False)
},
# 职场竞争 → 军争篇
{
"role": "user",
"content": "问题:同事跟我抢功劳,怎么应对?只输出JSON。"
},
{
"role": "assistant",
"content": json.dumps({
"chapter": "军争篇",
"quote": "以迂为直,以患为利。",
"unsure": False,
"insight": "不硬碰,转势为己用。",
"action": "开会前先发一份“小而精”的纪要,把自己负责的部分写清楚,让功劳自动写进流程里,对方想抢也得先面对记录。",
"speech": "我把这次我的负责点和关键产出提前发一份纪要,方便会里对齐,避免遗漏。",
"explain_cn": "看起来绕远路,其实是走捷径;把麻烦事变成自己有利的机会。"
}, ensure_ascii=False)
}
]
# ===================== 调用 DeepSeek:主分析 =====================
def call_deepseek(user_question: str) -> dict:
"""自动尝试两个端点,返回结构化兵法解析结果。"""
key = os.getenv("DEEPSEEK_API_KEY", "").strip()
if not key:
return {
"chapter": "",
"quote": "未配置 DEEPSEEK_API_KEY",
"unsure": True,
"insight": "请先在 Settings → Secrets 配置密钥。",
"action": "键名必须是 DEEPSEEK_API_KEY,值不要带引号或空格;配置后重启。",
"speech": "密钥配好我就能正常回答。",
"explain_cn": "当前还无法解释名言,因为没有配置密钥。"
}
endpoints = [
(os.getenv("DEEPSEEK_BASE_URL", "").strip()
or "https://api.deepseek.com/v1/chat/completions"),
"https://api.deepseek.com/chat/completions",
]
# 多样化提示:鼓励换篇章/换角度
diversity_hint = random.choice([
"尽量避免重复使用同一篇章,尝试换篇章回答。",
"优先考虑《军形篇》《兵势篇》《九变篇》《地形篇》的角度。",
"如果问题可多解,换一个不同篇章来解释。",
"引用少见但贴合的篇章,保持回答多样性。",
"可从节奏、态势、资源、时机等不同维度选篇章。"
])
msgs = [{"role": "system", "content": SYS_PROMPT + "\n" + diversity_hint}] + FEWSHOT + [
{"role": "user", "content": f"问题:{user_question} 只输出JSON。"}
]
last_err = None
for ep in endpoints:
try:
payload = {
"model": MODEL_NAME,
"messages": msgs,
"temperature": 0.7,
"top_p": 0.9,
"max_tokens": 450
}
r = requests.post(
ep,
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
json=payload,
timeout=60
)
print(f"[DeepSeek] endpoint={ep} status={r.status_code} model={MODEL_NAME}") # Runtime logs
if r.status_code // 100 != 2:
try:
err_detail = r.json()
except Exception:
err_detail = {"raw": r.text[:500]}
last_err = f"HTTP {r.status_code} @ {ep} (model={MODEL_NAME}) → {err_detail}"
continue
data = r.json()
content = data.get("choices", [{}])[0].get("message", {}).get("content", "").strip()
m = re.search(r"\{.*\}", content, flags=re.S)
js = m.group(0) if m else content
result = json.loads(js)
return {
"chapter": result.get("chapter", "")[:20],
"quote": result.get("quote", "")[:200],
"unsure": bool(result.get("unsure", False)),
"insight": result.get("insight", "")[:40],
"action": result.get("action", "")[:120],
"speech": result.get("speech", "")[:140],
"explain_cn": result.get("explain_cn", "")[:80],
}
except Exception as e:
last_err = f"{type(e).__name__}: {e}"
return {
"chapter": "",
"quote": "为避免误引,已隐藏原文与篇名(模型不确定出典)。",
"unsure": True,
"insight": "DeepSeek 请求未成功。",
"action": f"诊断信息:{last_err or '未知错误'}",
"speech": "请按诊断检查:①密钥②端点③模型名④配额/限速,然后重启。",
"explain_cn": "当前无法给出名言解释,请检查配置。"
}
# ===================== 调用 DeepSeek:中译英翻译 =====================
def translate_to_en(text_cn: str) -> str:
"""使用 DeepSeek 将简单中文解释翻译成英文。"""
key = os.getenv("DEEPSEEK_API_KEY", "").strip()
if not key:
return "⚠️ 未配置 DEEPSEEK_API_KEY,暂时无法翻译成英文。"
endpoints = [
(os.getenv("DEEPSEEK_BASE_URL", "").strip()
or "https://api.deepseek.com/v1/chat/completions"),
"https://api.deepseek.com/chat/completions",
]
msgs = [
{
"role": "system",
"content": "你是专业的中译英助手,只把用户提供的中文翻译成自然简洁的英文,不要添加解释或多余内容。"
},
{
"role": "user",
"content": text_cn
}
]
last_err = None
for ep in endpoints:
try:
payload = {
"model": MODEL_NAME,
"messages": msgs,
"temperature": 0.3,
"top_p": 0.9,
"max_tokens": 200
}
r = requests.post(
ep,
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
json=payload,
timeout=60
)
print(f"[DeepSeek-Translate] endpoint={ep} status={r.status_code} model={MODEL_NAME}")
if r.status_code // 100 != 2:
try:
err_detail = r.json()
except Exception:
err_detail = {"raw": r.text[:500]}
last_err = f"HTTP {r.status_code} @ {ep} (model={MODEL_NAME}) → {err_detail}"
continue
data = r.json()
content = data.get("choices", [{}])[0].get("message", {}).get("content", "").strip()
return html.escape(content)
except Exception as e:
last_err = f"{type(e).__name__}: {e}"
return f"⚠️ 翻译失败:{last_err or '未知错误'}"
# ===================== 渲染函数 =====================
def render_main(result: dict) -> str:
"""
主体渲染(不包含话术示例、不包含“我是老外”解释)。
默认严格校验:若 unsure=True 则隐藏原文与篇名。
"""
unsure = bool(result.get("unsure"))
chap = html.escape(result.get("chapter", ""))
quote = html.escape(result.get("quote", ""))
insight = html.escape(result.get("insight", ""))
action = html.escape(result.get("action", ""))
# 默认“严格出处校验”:模型不确定时,隐藏原文与篇名
show_quote = not unsure
parts = ["<div style='font-size:22px;line-height:1.7em;'>"]
if show_quote:
badge = "📜" if not unsure else "❓"
chap_line = f"《{chap}》" if chap else "(未标明篇名)"
parts.append(f"{badge} {chap_line}<br><br>")
parts.append(f"「{quote}」<br><br>")
else:
parts.append("⚠️ 为避免误引,已隐藏原文与篇名(模型不确定出典)。<br><br>")
parts.append(f"<b>启示:</b>{insight}<br><br>")
parts.append(f"<b>做法:</b>{action}<br><br>")
parts.append("</div>")
return "".join(parts)
def render_speech(result: dict) -> str:
"""单独渲染话术示例区域。"""
speech = html.escape(result.get("speech", "").strip())
if not speech:
return (
"<div style='margin-top:8px;font-size:16px;color:#777;'>"
"当前没有可用的「话术示例」,请先点击「生成兵法」。"
"</div>"
)
return (
"<div style='margin-top:12px;padding:12px 14px;border:1px dashed #e2d7f2;"
"border-radius:10px;background:#fbf8ff;font-size:18px;line-height:1.7em;'>"
"<div style='font-weight:600;margin-bottom:6px;'>💬 话术示例:</div>"
f"<div>{speech}</div>"
"</div>"
)
def render_explain_cn(result: dict) -> str:
"""为“我是老外”按钮渲染特别简单的中文解释。"""
explain_cn = html.escape(result.get("explain_cn", "").strip())
if not explain_cn:
return (
"<div style='margin-top:8px;font-size:16px;color:#777;'>"
"暂时没有找到这句名言的简单解释,请先生成一次兵法再试。"
"</div>"
)
return (
"<div style='margin-top:12px;padding:12px 14px;border:1px solid #d7e6ff;"
"border-radius:10px;background:#f5f8ff;font-size:18px;line-height:1.7em;'>"
"<div style='font-weight:600;margin-bottom:6px;'>🌏 给外国朋友解释:</div>"
f"<div>{explain_cn}</div>"
"</div>"
)
def render_explain_en(text_en: str) -> str:
"""渲染英文翻译结果。"""
return (
"<div style='margin-top:8px;padding:12px 14px;border:1px dashed #cddfea;"
"border-radius:10px;background:#f6f9ff;font-size:18px;line-height:1.7em;'>"
"<div style='font-weight:600;margin-bottom:6px;'>🔁 英文解释:</div>"
f"<div>{text_en}</div>"
"</div>"
)
# ===================== UI =====================
theme = gr.themes.Soft(primary_hue="pink", neutral_hue="slate")
CSS = """
body,.gradio-container{background:#f7f6fb!important;}
.header{font-weight:900;font-size:28px;color:#111;text-align:center;padding:24px 0;}
.card{background:#fff;border:1px solid #eddcf3;border-radius:14px;box-shadow:0 4px 18px rgba(0,0,0,.05);padding:16px;}
"""
with gr.Blocks(title=APP_TITLE, theme=theme, css=CSS) as demo:
gr.HTML("<div class='header'>孙子你好</div>")
with gr.Group(elem_classes=["card"]):
q = gr.Textbox(
label="你的问题",
placeholder="例如:朋友借钱不还;老板让我加班;邻居太吵;甲方拖款…",
lines=3
)
# 保存最近一次 DeepSeek 返回的结构化结果,用于“话术示例”和“我是老外”
state_result = gr.State({})
# 首页:只显示两个按钮
with gr.Row():
b1 = gr.Button("🎯 生成兵法", variant="primary")
b3 = gr.Button("🧹 清空")
# 下面这些按钮一开始不显示,生成兵法后再出现
with gr.Row():
btn_speech = gr.Button("💬 话术示例", visible=False)
btn_foreign = gr.Button("🌏 我是老外", visible=False)
btn_translate = gr.Button("🔁 翻译成英文", visible=False)
out_main = gr.HTML()
out_speech = gr.HTML()
out_explain_cn = gr.HTML()
out_explain_en = gr.HTML()
# ---------- 回调函数 ----------
def on_gen(user_q):
# 返回顺序:
# out_main, out_speech, out_explain_cn, out_explain_en, state_result,
# btn_speech_update, btn_foreign_update, btn_translate_update
if not (user_q or "").strip():
empty_msg = (
"<div style='font-size:16px;color:#777;'>"
"请先在上面输入你的问题,比如:朋友借钱不还怎么办?</div>"
)
hide = gr.update(visible=False)
return empty_msg, "", "", "", {}, hide, hide, hide
res = call_deepseek(user_q)
main_html = render_main(res)
# 生成新结果时,显示「话术示例」「我是老外」,但先不显示「翻译成英文」
return (
main_html,
"",
"",
"",
res,
gr.update(visible=True), # btn_speech
gr.update(visible=True), # btn_foreign
gr.update(visible=False), # btn_translate
)
def on_clear():
# 清空输入框和所有输出 & 状态,并隐藏所有二级按钮
hide = gr.update(visible=False)
return "", "", "", "", "", {}, hide, hide, hide
def on_show_speech(res):
if not isinstance(res, dict) or not res:
return (
"<div style='margin-top:8px;font-size:16px;color:#777;'>"
"请先点击「生成兵法」,再查看话术示例。</div>"
)
return render_speech(res)
def on_show_foreign(res):
# 返回:out_explain_cn, btn_translate_update
if not isinstance(res, dict) or not res:
msg = (
"<div style='margin-top:8px;font-size:16px;color:#777;'>"
"请先点击「生成兵法」,再点「我是老外」。</div>"
)
return msg, gr.update(visible=False)
return render_explain_cn(res), gr.update(visible=True)
def on_translate(res):
if not isinstance(res, dict) or not res:
return (
"<div style='margin-top:8px;font-size:16px;color:#777;'>"
"请先生成兵法并点击「我是老外」后,再翻译成英文。</div>"
)
text_cn = res.get("explain_cn", "").strip()
if not text_cn:
return (
"<div style='margin-top:8px;font-size:16px;color:#777;'>"
"当前没有可以翻译的中文解释,请先点「我是老外」。</div>"
)
text_en = translate_to_en(text_cn)
return render_explain_en(text_en)
# ---------- 事件绑定 ----------
b1.click(
on_gen,
inputs=[q],
outputs=[
out_main,
out_speech,
out_explain_cn,
out_explain_en,
state_result,
btn_speech,
btn_foreign,
btn_translate,
]
)
b3.click(
on_clear,
inputs=None,
outputs=[
q,
out_main,
out_speech,
out_explain_cn,
out_explain_en,
state_result,
btn_speech,
btn_foreign,
btn_translate,
]
)
btn_speech.click(
on_show_speech,
inputs=[state_result],
outputs=[out_speech]
)
btn_foreign.click(
on_show_foreign,
inputs=[state_result],
outputs=[out_explain_cn, btn_translate]
)
btn_translate.click(
on_translate,
inputs=[state_result],
outputs=[out_explain_en]
)
if __name__ == "__main__":
demo.launch() |