BingfaGenerator / app.py
sofiamak's picture
Update app.py
cdf70d4 verified
Raw History Blame Contribute Delete
20.3 kB
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()