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:
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user