feat: AI reply modes - full (formal email) and short (concise) - Split 'AI回复' button into 'AI完整回复' and 'AI简洁回复' - Full mode: complete email format with greeting/signature, 300 char limit - Short mode: brief text message style, 50 char limit - Style persists in conversation state for regen/hint consistency - Tests: 6/6 passing
This commit is contained in:
@@ -86,7 +86,9 @@ def expand_promo_detail(ai_cfg: AIConfig, sender: str, subject: str, body: str)
|
|||||||
return result.get("details", [])
|
return result.get("details", [])
|
||||||
|
|
||||||
|
|
||||||
MORE_REPLIES_PROMPT = """你是一个邮件回复助手。根据以下邮件内容生成3个完全不同的建议回复。
|
MORE_REPLIES_PROMPT_SHORT = """你是一个邮件回复助手。根据以下邮件内容生成3个完全不同的简洁回复。
|
||||||
|
|
||||||
|
简洁模式:直接说要点,不需要称呼、落款、邮件格式。像微信/短信一样简短。
|
||||||
|
|
||||||
以下是一些已经生成过的回复,请生成全新的回复,不要与已有回复重复。
|
以下是一些已经生成过的回复,请生成全新的回复,不要与已有回复重复。
|
||||||
|
|
||||||
@@ -101,22 +103,41 @@ MORE_REPLIES_PROMPT = """你是一个邮件回复助手。根据以下邮件内
|
|||||||
每条回复控制在 50 字以内。
|
每条回复控制在 50 字以内。
|
||||||
只返回 JSON,不要包含任何其他文字。"""
|
只返回 JSON,不要包含任何其他文字。"""
|
||||||
|
|
||||||
|
MORE_REPLIES_PROMPT_FULL = """你是一个邮件回复助手。根据以下邮件内容生成3个不同的正式邮件回复。
|
||||||
|
|
||||||
|
完整模式:生成完整的邮件格式,包括称呼、正文、落款。语气正式专业,适合商务邮件。
|
||||||
|
|
||||||
|
以下是一些已经生成过的回复,请生成全新的回复,不要与已有回复重复。
|
||||||
|
|
||||||
|
返回 JSON:
|
||||||
|
{
|
||||||
|
"suggestions": [
|
||||||
|
{"text": "回复内容"},
|
||||||
|
{"text": "回复内容"},
|
||||||
|
{"text": "回复内容"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
每条回复控制在 300 字以内。
|
||||||
|
只返回 JSON,不要包含任何其他文字。"""
|
||||||
|
|
||||||
|
|
||||||
@retry()
|
@retry()
|
||||||
def generate_more_replies(ai_cfg: AIConfig, sender: str, subject: str, body: str,
|
def generate_more_replies(ai_cfg: AIConfig, sender: str, subject: str, body: str,
|
||||||
previous_suggestions: list[dict], user_hint: str = "") -> list[dict]:
|
previous_suggestions: list[dict], user_hint: str = "",
|
||||||
logger.info("AI 换批请求: sender=%s subject=%s user_hint=%s prev=%d 条",
|
style: str = "short") -> list[dict]:
|
||||||
sender, subject, user_hint or "(无)", len(previous_suggestions))
|
logger.info("AI 换批请求: sender=%s subject=%s user_hint=%s prev=%d 条 style=%s",
|
||||||
|
sender, subject, user_hint or "(无)", len(previous_suggestions), style)
|
||||||
content = f"发件人: {sender}\n主题: {subject}\n正文:\n{body}\n\n已生成过的回复:\n"
|
content = f"发件人: {sender}\n主题: {subject}\n正文:\n{body}\n\n已生成过的回复:\n"
|
||||||
for i, s in enumerate(previous_suggestions, 1):
|
for i, s in enumerate(previous_suggestions, 1):
|
||||||
content += f"{i}. {s['text']}\n"
|
content += f"{i}. {s['text']}\n"
|
||||||
if user_hint:
|
if user_hint:
|
||||||
content += f"\n用户要求: {user_hint}"
|
content += f"\n用户要求: {user_hint}"
|
||||||
|
|
||||||
|
prompt = MORE_REPLIES_PROMPT_FULL if style == "full" else MORE_REPLIES_PROMPT_SHORT
|
||||||
payload = {
|
payload = {
|
||||||
"model": ai_cfg.model,
|
"model": ai_cfg.model,
|
||||||
"messages": [
|
"messages": [
|
||||||
{"role": "system", "content": MORE_REPLIES_PROMPT},
|
{"role": "system", "content": prompt},
|
||||||
{"role": "user", "content": content},
|
{"role": "user", "content": content},
|
||||||
],
|
],
|
||||||
"response_format": {"type": "json_object"},
|
"response_format": {"type": "json_object"},
|
||||||
|
|||||||
@@ -169,7 +169,8 @@ def _escape(text: str) -> str:
|
|||||||
CALLBACK_VIEW_ORIG = "v"
|
CALLBACK_VIEW_ORIG = "v"
|
||||||
CALLBACK_VIEW_SUMM = "s"
|
CALLBACK_VIEW_SUMM = "s"
|
||||||
CALLBACK_REPLY = "r"
|
CALLBACK_REPLY = "r"
|
||||||
CALLBACK_AI_REPLY = "a"
|
CALLBACK_AI_REPLY_FULL = "af"
|
||||||
|
CALLBACK_AI_REPLY_SHORT = "as"
|
||||||
CALLBACK_SELECT_REPLY = "sr"
|
CALLBACK_SELECT_REPLY = "sr"
|
||||||
CALLBACK_REGEN = "rg"
|
CALLBACK_REGEN = "rg"
|
||||||
CALLBACK_HINT = "h"
|
CALLBACK_HINT = "h"
|
||||||
@@ -190,7 +191,10 @@ def _summary_keyboard(can_reply: bool = True, is_promotion: bool = False, links:
|
|||||||
],
|
],
|
||||||
]
|
]
|
||||||
if can_reply:
|
if can_reply:
|
||||||
kb.append([{"text": "AI回复", "callback_data": CALLBACK_AI_REPLY}])
|
kb.append([
|
||||||
|
{"text": "AI完整回复", "callback_data": CALLBACK_AI_REPLY_FULL},
|
||||||
|
{"text": "AI简洁回复", "callback_data": CALLBACK_AI_REPLY_SHORT},
|
||||||
|
])
|
||||||
for link in (links or []):
|
for link in (links or []):
|
||||||
url = link.get("url", "")
|
url = link.get("url", "")
|
||||||
text = link.get("text", "打开链接")[:30]
|
text = link.get("text", "打开链接")[:30]
|
||||||
@@ -208,7 +212,10 @@ def _orig_keyboard(can_reply: bool = True) -> dict:
|
|||||||
],
|
],
|
||||||
]
|
]
|
||||||
if can_reply:
|
if can_reply:
|
||||||
kb.append([{"text": "AI回复", "callback_data": CALLBACK_AI_REPLY}])
|
kb.append([
|
||||||
|
{"text": "AI完整回复", "callback_data": CALLBACK_AI_REPLY_FULL},
|
||||||
|
{"text": "AI简洁回复", "callback_data": CALLBACK_AI_REPLY_SHORT},
|
||||||
|
])
|
||||||
return {"inline_keyboard": kb}
|
return {"inline_keyboard": kb}
|
||||||
|
|
||||||
|
|
||||||
@@ -252,10 +259,11 @@ def _tg_req(tg_cfg: TelegramConfig, method: str, payload: dict) -> dict:
|
|||||||
|
|
||||||
def _do_regen(cfg: Config, chat_id: int, msg_id: int, ctx: dict, conv: dict):
|
def _do_regen(cfg: Config, chat_id: int, msg_id: int, ctx: dict, conv: dict):
|
||||||
"""在 AI 线程池中执行换一批,不阻塞按钮轮询"""
|
"""在 AI 线程池中执行换一批,不阻塞按钮轮询"""
|
||||||
|
style = conv.get("reply_style", "short")
|
||||||
try:
|
try:
|
||||||
new_suggestions = generate_more_replies(
|
new_suggestions = generate_more_replies(
|
||||||
cfg.ai, ctx["sender"], ctx["subject"], ctx["original_body"],
|
cfg.ai, ctx["sender"], ctx["subject"], ctx["original_body"],
|
||||||
conv.get("all_suggestions", []),
|
conv.get("all_suggestions", []), style=style,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
send_text(cfg.telegram, str(chat_id), f"生成失败: {e}")
|
send_text(cfg.telegram, str(chat_id), f"生成失败: {e}")
|
||||||
@@ -264,16 +272,17 @@ def _do_regen(cfg: Config, chat_id: int, msg_id: int, ctx: dict, conv: dict):
|
|||||||
conv["ai_suggestions"] = new_suggestions
|
conv["ai_suggestions"] = new_suggestions
|
||||||
conv["all_suggestions"].extend(new_suggestions)
|
conv["all_suggestions"].extend(new_suggestions)
|
||||||
save_conversation(chat_id, conv)
|
save_conversation(chat_id, conv)
|
||||||
logger.info("[ai_regen] 新生成 %d 条回复", len(new_suggestions))
|
logger.info("[ai_regen] 新生成 %d 条回复 (style=%s)", len(new_suggestions), style)
|
||||||
_show_ai_suggestions(cfg.telegram, chat_id, msg_id, new_suggestions)
|
_show_ai_suggestions(cfg.telegram, chat_id, msg_id, new_suggestions)
|
||||||
|
|
||||||
|
|
||||||
def _do_ai_hint(cfg: Config, chat_id: int, conv: dict, ctx: dict, hint: str):
|
def _do_ai_hint(cfg: Config, chat_id: int, conv: dict, ctx: dict, hint: str):
|
||||||
"""在 AI 线程池中根据用户提示生成回复"""
|
"""在 AI 线程池中根据用户提示生成回复"""
|
||||||
|
style = conv.get("reply_style", "short")
|
||||||
try:
|
try:
|
||||||
new_suggestions = generate_more_replies(
|
new_suggestions = generate_more_replies(
|
||||||
cfg.ai, ctx["sender"], ctx["subject"], ctx["original_body"],
|
cfg.ai, ctx["sender"], ctx["subject"], ctx["original_body"],
|
||||||
conv.get("all_suggestions", []), user_hint=hint,
|
conv.get("all_suggestions", []), user_hint=hint, style=style,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
send_text(cfg.telegram, str(chat_id), f"生成失败: {e}")
|
send_text(cfg.telegram, str(chat_id), f"生成失败: {e}")
|
||||||
@@ -346,8 +355,9 @@ def _handle_callback(tg_cfg: TelegramConfig, cfg: Config, cb: dict):
|
|||||||
})
|
})
|
||||||
logger.info(" 进入回复流程, prompt_msg_id=%d", prompt_msg_id)
|
logger.info(" 进入回复流程, prompt_msg_id=%d", prompt_msg_id)
|
||||||
|
|
||||||
elif action == CALLBACK_AI_REPLY and ctx:
|
elif action in (CALLBACK_AI_REPLY_FULL, CALLBACK_AI_REPLY_SHORT) and ctx:
|
||||||
suggestions = ctx["summary_data"].get("reply_suggestions", [])
|
suggestions = ctx["summary_data"].get("reply_suggestions", [])
|
||||||
|
style = "full" if action == CALLBACK_AI_REPLY_FULL else "short"
|
||||||
if not suggestions:
|
if not suggestions:
|
||||||
send_text(tg_cfg, str(chat_id), "此邮件无需回复。")
|
send_text(tg_cfg, str(chat_id), "此邮件无需回复。")
|
||||||
logger.info(" AI 判定无需回复,隐藏")
|
logger.info(" AI 判定无需回复,隐藏")
|
||||||
@@ -358,8 +368,9 @@ def _handle_callback(tg_cfg: TelegramConfig, cfg: Config, cb: dict):
|
|||||||
"summary_msg_id": msg_id,
|
"summary_msg_id": msg_id,
|
||||||
"ai_suggestions": suggestions,
|
"ai_suggestions": suggestions,
|
||||||
"all_suggestions": list(suggestions),
|
"all_suggestions": list(suggestions),
|
||||||
|
"reply_style": style,
|
||||||
})
|
})
|
||||||
logger.info(" 显示 AI 回复建议 (%d 条)", len(suggestions))
|
logger.info(" 显示 AI 回复建议 (%d 条, style=%s)", len(suggestions), style)
|
||||||
_show_ai_suggestions(tg_cfg, chat_id, msg_id, suggestions)
|
_show_ai_suggestions(tg_cfg, chat_id, msg_id, suggestions)
|
||||||
|
|
||||||
elif action == CALLBACK_SELECT_REPLY and ctx:
|
elif action == CALLBACK_SELECT_REPLY and ctx:
|
||||||
|
|||||||
Reference in New Issue
Block a user