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 = ["
"] if show_quote: badge = "📜" if not unsure else "❓" chap_line = f"《{chap}》" if chap else "(未标明篇名)" parts.append(f"{badge} {chap_line}

") parts.append(f"「{quote}」

") else: parts.append("⚠️ 为避免误引,已隐藏原文与篇名(模型不确定出典)。

") parts.append(f"启示:{insight}

") parts.append(f"做法:{action}

") parts.append("
") return "".join(parts) def render_speech(result: dict) -> str: """单独渲染话术示例区域。""" speech = html.escape(result.get("speech", "").strip()) if not speech: return ( "
" "当前没有可用的「话术示例」,请先点击「生成兵法」。" "
" ) return ( "
" "
💬 话术示例:
" f"
{speech}
" "
" ) def render_explain_cn(result: dict) -> str: """为“我是老外”按钮渲染特别简单的中文解释。""" explain_cn = html.escape(result.get("explain_cn", "").strip()) if not explain_cn: return ( "
" "暂时没有找到这句名言的简单解释,请先生成一次兵法再试。" "
" ) return ( "
" "
🌏 给外国朋友解释:
" f"
{explain_cn}
" "
" ) def render_explain_en(text_en: str) -> str: """渲染英文翻译结果。""" return ( "
" "
🔁 英文解释:
" f"
{text_en}
" "
" ) # ===================== 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("
孙子你好
") 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 = ( "
" "请先在上面输入你的问题,比如:朋友借钱不还怎么办?
" ) 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 ( "
" "请先点击「生成兵法」,再查看话术示例。
" ) return render_speech(res) def on_show_foreign(res): # 返回:out_explain_cn, btn_translate_update if not isinstance(res, dict) or not res: msg = ( "
" "请先点击「生成兵法」,再点「我是老外」。
" ) 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 ( "
" "请先生成兵法并点击「我是老外」后,再翻译成英文。
" ) text_cn = res.get("explain_cn", "").strip() if not text_cn: return ( "
" "当前没有可以翻译的中文解释,请先点「我是老外」。
" ) 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()