refactor: merge reply suggestions into single AI request

- Summary prompt now includes can_reply + reply_suggestions fields
- Removed separate generate_reply_suggestions function and REPLY_PROMPT
- tg_bot reads reply suggestions from cached summary_data
- AI Reply button conditionally hidden based on can_reply
This commit is contained in:
2026-07-02 20:22:54 +08:00
parent 0dbc7ee661
commit 870ab4a59a
2 changed files with 50 additions and 85 deletions

View File

@@ -15,10 +15,21 @@ SYSTEM_PROMPT = """你是一个邮件摘要助手。请分析邮件内容并以
"priority": "high/medium/low",
"action_required": true/false,
"action_items": ["待办事项1", "待办事项2"],
"key_points": ["关键要点1", "关键要点2"]
"key_points": ["关键要点1", "关键要点2"],
"can_reply": true/false,
"reply_suggestions": [
{"text": "回复内容", "is_simple": true/false},
{"text": "回复内容", "is_simple": true/false},
{"text": "回复内容", "is_simple": true/false}
]
}
其中 subject 字段不能照抄原邮件主题,必须是你总结生成的简短标题。
其中
- subject 不能照抄原邮件主题,必须是你总结生成的简短标题
- can_reply 为 false 表示无需回复或不适合建议回复(此时忽略 reply_suggestions
- is_simple 为 true 表示纯确认性回复(如"好的""收到"),用户选择后可直发
- is_simple 为 false 表示涉及实质内容,需要用户确认再发送
- 每条回复控制在 50 字以内
只返回 JSON不要包含任何其他文字。"""
@@ -35,46 +46,6 @@ def summarize_email(ai_cfg: AIConfig, recipient: str, subject: str, sender: str,
"response_format": {"type": "json_object"},
}
headers = {
"Authorization": f"Bearer {ai_cfg.api_key}",
"Content-Type": "application/json",
}
return _call_deepseek(ai_cfg, payload)
REPLY_PROMPT = """你是一个邮件回复助手。根据以下邮件内容生成3个建议回复。
返回 JSON:
{
"can_reply": true/false,
"suggestions": [
{"text": "回复内容", "is_simple": true/false},
{"text": "回复内容", "is_simple": true/false},
{"text": "回复内容", "is_simple": true/false}
]
}
- can_reply 为 false 时表示无需回复或不适合自动建议(此时忽略 suggestions
- is_simple 为 true 表示纯确认性回复(如"好的""收到""明白"),用户选择后可直接发送
- is_simple 为 false 表示涉及实质内容,需要用户确认后再发送
- 每条回复控制在 50 字以内
只返回 JSON不要包含任何其他文字。"""
@retry()
def generate_reply_suggestions(ai_cfg: AIConfig, sender: str, subject: str, body: str) -> dict:
content = f"发件人: {sender}\n主题: {subject}\n正文:\n{body[:4000]}"
payload = {
"model": ai_cfg.model,
"messages": [
{"role": "system", "content": REPLY_PROMPT},
{"role": "user", "content": content},
],
"response_format": {"type": "json_object"},
}
return _call_deepseek(ai_cfg, payload)

View File

@@ -2,7 +2,7 @@ import json
import logging
import requests
from src.config import TelegramConfig, Config
from src.ai_client import generate_reply_suggestions
# reply suggestions come embedded in summary_data from summarize_email
from src.smtp_client import send_reply
from src.retry import retry
@@ -21,11 +21,12 @@ _conversations: dict[int, dict] = {}
@retry()
def send_summary(tg_cfg: TelegramConfig, chat_id: str, summary_text: str,
summary_data: dict, original_body: str, account_idx: int) -> int:
can_reply = summary_data.get("can_reply", True)
result = _tg_req(tg_cfg, "sendMessage", {
"chat_id": chat_id,
"text": summary_text,
"parse_mode": "MarkdownV2",
"reply_markup": _summary_keyboard(),
"reply_markup": _summary_keyboard(can_reply),
})
msg_id = result["result"]["message_id"]
_email_contexts[msg_id] = {
@@ -35,6 +36,7 @@ def send_summary(tg_cfg: TelegramConfig, chat_id: str, summary_text: str,
"account_idx": account_idx,
"sender": summary_data.get("sender", ""),
"subject": summary_data.get("subject", ""),
"can_reply": can_reply,
}
return msg_id
@@ -108,32 +110,28 @@ CALLBACK_CONFIRM_REPLY = "cr"
CALLBACK_CANCEL = "c"
def _summary_keyboard() -> dict:
return {
"inline_keyboard": [
[
{"text": "查看原文", "callback_data": CALLBACK_VIEW_ORIG},
{"text": "回复", "callback_data": CALLBACK_REPLY},
],
[
{"text": "AI回复", "callback_data": CALLBACK_AI_REPLY},
],
]
}
def _summary_keyboard(can_reply: bool = True) -> dict:
kb = [
[
{"text": "查看原文", "callback_data": CALLBACK_VIEW_ORIG},
{"text": "回复", "callback_data": CALLBACK_REPLY},
],
]
if can_reply:
kb.append([{"text": "AI回复", "callback_data": CALLBACK_AI_REPLY}])
return {"inline_keyboard": kb}
def _orig_keyboard() -> dict:
return {
"inline_keyboard": [
[
{"text": "返回摘要", "callback_data": CALLBACK_VIEW_SUMM},
{"text": "回复", "callback_data": CALLBACK_REPLY},
],
[
{"text": "AI回复", "callback_data": CALLBACK_AI_REPLY},
],
]
}
def _orig_keyboard(can_reply: bool = True) -> dict:
kb = [
[
{"text": "返回摘要", "callback_data": CALLBACK_VIEW_SUMM},
{"text": "回复", "callback_data": CALLBACK_REPLY},
],
]
if can_reply:
kb.append([{"text": "AI回复", "callback_data": CALLBACK_AI_REPLY}])
return {"inline_keyboard": kb}
def _ai_reply_keyboard(msg_id: int, suggestions: list) -> dict:
@@ -191,57 +189,52 @@ def _handle_callback(tg_cfg: TelegramConfig, cfg: Config, cb: dict):
ctx = _email_contexts.get(msg_id)
if action == CALLBACK_VIEW_ORIG and ctx:
can_reply = ctx.get("can_reply", True)
text = f"*📄 原文 \\- {_escape(ctx['subject'])}*\n\n{_escape(ctx['original_body'][:3500])}"
_tg_req(tg_cfg, "editMessageText", {
"chat_id": chat_id, "message_id": msg_id,
"text": text, "parse_mode": "MarkdownV2",
"reply_markup": _orig_keyboard(),
"reply_markup": _orig_keyboard(can_reply),
})
elif action == CALLBACK_VIEW_SUMM and ctx:
can_reply = ctx.get("can_reply", True)
_tg_req(tg_cfg, "editMessageText", {
"chat_id": chat_id, "message_id": msg_id,
"text": ctx["summary_text"], "parse_mode": "MarkdownV2",
"reply_markup": _summary_keyboard(),
"reply_markup": _summary_keyboard(can_reply),
})
elif action == CALLBACK_REPLY and ctx:
_conversations[chat_id] = {"state": "awaiting_reply", "msg_id": msg_id}
can_reply = ctx.get("can_reply", True)
_tg_req(tg_cfg, "editMessageText", {
"chat_id": chat_id, "message_id": msg_id,
"text": ctx["summary_text"], "parse_mode": "MarkdownV2",
"reply_markup": _summary_keyboard(),
"reply_markup": _summary_keyboard(can_reply),
})
send_text(tg_cfg, str(chat_id), "请输入你的回复内容:")
elif action == CALLBACK_AI_REPLY and ctx:
try:
acct = cfg.email_accounts[ctx["account_idx"]]
suggestions = generate_reply_suggestions(
cfg.ai, ctx["sender"], ctx["subject"], ctx["original_body"],
)
except Exception as e:
send_text(tg_cfg, str(chat_id), f"AI 生成回复失败: {e}")
return
if not suggestions.get("can_reply") or not suggestions.get("suggestions"):
suggestions = ctx["summary_data"].get("reply_suggestions", [])
if not suggestions:
send_text(tg_cfg, str(chat_id), "此邮件无需回复,已隐藏 AI 回复功能。")
return
_conversations[chat_id] = {
"state": "ai_reply_selection",
"msg_id": msg_id,
"ai_suggestions": suggestions["suggestions"],
"ai_suggestions": suggestions,
}
text = "*AI 建议回复,请选择:*\n\n"
for i, s in enumerate(suggestions["suggestions"], 1):
for i, s in enumerate(suggestions, 1):
tag = "✅ 可直接发送" if s.get("is_simple") else "📝 需确认"
text += f"{i}\\. {_escape(s['text'])} _{tag}_\n"
_tg_req(tg_cfg, "editMessageText", {
"chat_id": chat_id, "message_id": msg_id,
"text": text, "parse_mode": "MarkdownV2",
"reply_markup": _ai_reply_keyboard(msg_id, suggestions["suggestions"]),
"reply_markup": _ai_reply_keyboard(msg_id, suggestions),
})
elif action == CALLBACK_SELECT_REPLY and ctx:
@@ -282,10 +275,11 @@ def _handle_callback(tg_cfg: TelegramConfig, cfg: Config, cb: dict):
elif action == CALLBACK_CANCEL:
_conversations.pop(chat_id, None)
if ctx:
can_reply = ctx.get("can_reply", True)
_tg_req(tg_cfg, "editMessageText", {
"chat_id": chat_id, "message_id": msg_id,
"text": ctx["summary_text"], "parse_mode": "MarkdownV2",
"reply_markup": _summary_keyboard(),
"reply_markup": _summary_keyboard(can_reply),
})