Compare commits

..

3 Commits

2 changed files with 23 additions and 41 deletions

View File

@@ -8,7 +8,7 @@ from src.retry import retry
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
SYSTEM_PROMPT = """你是一个邮件摘要助手。请分析邮件内容并以 JSON 格式返回结构化摘要。所有输出subject、summary 等)必须使用简体中文,但 reply_suggestions智能回复请使用与邮件相同的语言以便用户直接回复 SYSTEM_PROMPT = """你是一个邮件摘要助手。请分析邮件内容并以 JSON 格式返回结构化摘要。所有输出subject、summary 等)必须使用简体中文。
返回格式必须严格遵循以下 JSON schema 返回格式必须严格遵循以下 JSON schema
{ {
@@ -21,12 +21,7 @@ SYSTEM_PROMPT = """你是一个邮件摘要助手。请分析邮件内容并以
"verification_code": "", "verification_code": "",
"app_name": "", "app_name": "",
"links": [{"text": "按钮文字", "url": "https://..."}], "links": [{"text": "按钮文字", "url": "https://..."}],
"can_reply": true/false, "can_reply": true/false
"reply_suggestions": [
{"text": "回复内容", "is_simple": true/false},
{"text": "回复内容", "is_simple": true/false},
{"text": "回复内容", "is_simple": true/false}
]
} }
其中: 其中:
@@ -38,11 +33,8 @@ SYSTEM_PROMPT = """你是一个邮件摘要助手。请分析邮件内容并以
- 退订链接提取规则推广邮件必须提取退订链接。退订链接通常在邮件最底部包含以下关键词之一unsubscribe、退订、取消订阅、opt out、manage preferences、email preferences。即使链接被包裹在 HTML 标签中,也要提取 href 属性里的完整 URL。如果邮件底部同时有多个退订相关链接只提取最主要的那个 - 退订链接提取规则推广邮件必须提取退订链接。退订链接通常在邮件最底部包含以下关键词之一unsubscribe、退订、取消订阅、opt out、manage preferences、email preferences。即使链接被包裹在 HTML 标签中,也要提取 href 属性里的完整 URL。如果邮件底部同时有多个退订相关链接只提取最主要的那个
- plain_text: 将邮件全文转换为可读的纯文本版本。去除所有HTML标签、CSS样式、脚本代码保留完整的文字内容和原始段落结构。不删减、不修改、不总结任何内容完整保留原文所有信息 - plain_text: 将邮件全文转换为可读的纯文本版本。去除所有HTML标签、CSS样式、脚本代码保留完整的文字内容和原始段落结构。不删减、不修改、不总结任何内容完整保留原文所有信息
- summary 要简洁,把需要知道的信息浓缩成一句话,不需要分条列出 - summary 要简洁,把需要知道的信息浓缩成一句话,不需要分条列出
- 当 category 为 "promotion"summary 直接写成一句话:「[发送方名称] 在推广 [推广的产品/服务/活动]」,例如「腾讯云在推广双十一云服务器折扣」。此时 can_reply 为 falsereply_suggestions 为空数组 - 当 category 为 "promotion"summary 直接写成一句话:「[发送方名称] 在推广 [推广的产品/服务/活动]」,例如「腾讯云在推广双十一云服务器折扣」。此时 can_reply 为 false
- can_reply 为 false 表示无需回复或不适合建议回复(此时忽略 reply_suggestions - can_reply 为 false 表示无需回复或不适合建议回复
- is_simple 为 true 表示纯确认性回复(如"好的""收到"),用户选择后可直发
- is_simple 为 false 表示涉及实质内容,需要用户确认再发送
- 每条回复控制在 50 字以内
只返回 JSON不要包含任何其他文字。""" 只返回 JSON不要包含任何其他文字。"""
@@ -109,7 +101,7 @@ MORE_REPLIES_PROMPT_SHORT = """你是一个邮件回复助手。根据以下邮
MORE_REPLIES_PROMPT_FULL = """你是一个邮件回复助手。根据以下邮件内容生成3个不同的正式邮件回复。 MORE_REPLIES_PROMPT_FULL = """你是一个邮件回复助手。根据以下邮件内容生成3个不同的正式邮件回复。
规则: 规则:
- 完整模式:生成完整的邮件格式,包括称呼(如 Dear xxx,)和正文,结尾只写问候语逗号(如 Best regards,不写落款,用户会自己编辑添加。 - 完整模式:生成完整的邮件格式,包括称呼(如 Dear xxx,)和正文,结尾只写问候语逗号不写落款,用户会自己编辑添加。
- 必须使用与邮件相同的语言回复(英文邮件用英文,中文邮件用中文)。 - 必须使用与邮件相同的语言回复(英文邮件用英文,中文邮件用中文)。
- 语气正式专业,适合商务邮件。 - 语气正式专业,适合商务邮件。

View File

@@ -362,11 +362,8 @@ def _handle_callback(tg_cfg: TelegramConfig, cfg: Config, cb: dict):
elif action in (CALLBACK_AI_REPLY_FULL, CALLBACK_AI_REPLY_SHORT) and ctx: elif action in (CALLBACK_AI_REPLY_FULL, CALLBACK_AI_REPLY_SHORT) and ctx:
style = "full" if action == CALLBACK_AI_REPLY_FULL else "short" style = "full" if action == CALLBACK_AI_REPLY_FULL else "short"
suggestions = ctx["summary_data"].get("reply_suggestions", []) # 总是用对应风格重新生成,不使用摘要时的通用缓存
logger.info(" 重新生成 AI 回复 (style=%s)", style)
# 缓存为空时重新生成
if not suggestions:
logger.info(" 缓存为空,重新生成 AI 回复 (style=%s)", style)
_tg_req(tg_cfg, "editMessageText", { _tg_req(tg_cfg, "editMessageText", {
"chat_id": chat_id, "message_id": msg_id, "chat_id": chat_id, "message_id": msg_id,
"text": "🤖 *AI思考中…*", "parse_mode": "MarkdownV2", "text": "🤖 *AI思考中…*", "parse_mode": "MarkdownV2",
@@ -378,17 +375,6 @@ def _handle_callback(tg_cfg: TelegramConfig, cfg: Config, cb: dict):
conv["all_suggestions"] = [] conv["all_suggestions"] = []
save_conversation(chat_id, conv) save_conversation(chat_id, conv)
_ai_pool.submit(_do_regen, cfg, chat_id, msg_id, ctx, conv) _ai_pool.submit(_do_regen, cfg, chat_id, msg_id, ctx, conv)
return
save_conversation(chat_id, {
"state": "ai_reply_selection",
"summary_msg_id": msg_id,
"ai_suggestions": suggestions,
"all_suggestions": list(suggestions),
"reply_style": style,
})
logger.info(" 显示 AI 回复建议 (%d 条, style=%s)", len(suggestions), style)
_show_ai_suggestions(tg_cfg, chat_id, msg_id, suggestions)
elif action == CALLBACK_SEND_REPLY and ctx: elif action == CALLBACK_SEND_REPLY and ctx:
idx = int(parts[2]) idx = int(parts[2])
@@ -404,10 +390,12 @@ def _handle_callback(tg_cfg: TelegramConfig, cfg: Config, cb: dict):
send_text(tg_cfg, str(chat_id), f"✅ 回复已发送") send_text(tg_cfg, str(chat_id), f"✅ 回复已发送")
delete_conversation(chat_id) delete_conversation(chat_id)
can_reply = ctx.get("can_reply", True) can_reply = ctx.get("can_reply", True)
is_promotion = ctx.get("summary_data", {}).get("category") == "promotion"
links = ctx.get("summary_data", {}).get("links", [])
_tg_req(tg_cfg, "editMessageText", { _tg_req(tg_cfg, "editMessageText", {
"chat_id": chat_id, "message_id": msg_id, "chat_id": chat_id, "message_id": msg_id,
"text": ctx["summary_text"], "parse_mode": "MarkdownV2", "text": ctx["summary_text"], "parse_mode": "MarkdownV2",
"reply_markup": _summary_keyboard(can_reply), "reply_markup": _summary_keyboard(can_reply, is_promotion, links),
}) })
elif action == CALLBACK_SELECT_REPLY and ctx: elif action == CALLBACK_SELECT_REPLY and ctx:
@@ -475,10 +463,12 @@ def _handle_callback(tg_cfg: TelegramConfig, cfg: Config, cb: dict):
ctx["summary_data"]["reply_suggestions"] = [] ctx["summary_data"]["reply_suggestions"] = []
save_email_context(msg_id, ctx) save_email_context(msg_id, ctx)
can_reply = ctx.get("can_reply", True) can_reply = ctx.get("can_reply", True)
is_promotion = ctx.get("summary_data", {}).get("category") == "promotion"
links = ctx.get("summary_data", {}).get("links", [])
_tg_req(tg_cfg, "editMessageText", { _tg_req(tg_cfg, "editMessageText", {
"chat_id": chat_id, "message_id": msg_id, "chat_id": chat_id, "message_id": msg_id,
"text": ctx["summary_text"], "parse_mode": "MarkdownV2", "text": ctx["summary_text"], "parse_mode": "MarkdownV2",
"reply_markup": _summary_keyboard(can_reply), "reply_markup": _summary_keyboard(can_reply, is_promotion, links),
}) })
logger.info(" 取消, 恢复摘要视图") logger.info(" 取消, 恢复摘要视图")
else: else: