247 lines
8.5 KiB
Python
247 lines
8.5 KiB
Python
|
|
"""
|
|||
|
|
飞书卡片动作处理器 — 处理用户点击反馈按钮(有用/无用/重试)
|
|||
|
|
|
|||
|
|
每个 WS Handler 在 _build_event_handler() 中注册此处理器。
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
import logging
|
|||
|
|
from typing import Any, Dict
|
|||
|
|
|
|||
|
|
from app.services.feishu_card_builder import (
|
|||
|
|
ACTION_FEEDBACK_THUMBS_UP,
|
|||
|
|
ACTION_FEEDBACK_THUMBS_DOWN,
|
|||
|
|
ACTION_FEEDBACK_REGENERATE,
|
|||
|
|
build_feedback_response_card,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def handle_card_action(event: Any) -> Any:
|
|||
|
|
"""处理飞书卡片按钮点击事件 (p2_card_action_trigger)。
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
event: P2CardActionTrigger 事件对象
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
P2CardActionTriggerResponse 响应对象
|
|||
|
|
"""
|
|||
|
|
from lark_oapi.event.callback.model.p2_card_action_trigger import (
|
|||
|
|
CallBackToast,
|
|||
|
|
CallBackCard,
|
|||
|
|
P2CardActionTriggerResponse,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 解析 action value
|
|||
|
|
action_value_str = getattr(event.event.action, "value", "{}")
|
|||
|
|
if isinstance(action_value_str, str):
|
|||
|
|
action_value = json.loads(action_value_str)
|
|||
|
|
else:
|
|||
|
|
action_value = action_value_str or {}
|
|||
|
|
|
|||
|
|
action = action_value.get("action", "")
|
|||
|
|
execution_log_id = action_value.get("execution_log_id", "")
|
|||
|
|
agent_name = action_value.get("agent_name", "")
|
|||
|
|
|
|||
|
|
# 获取操作者信息
|
|||
|
|
open_id = ""
|
|||
|
|
try:
|
|||
|
|
operator = event.event.operator
|
|||
|
|
open_id = getattr(getattr(operator, "open_id", None), "union_id", None) or getattr(operator, "open_id", "")
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
if not action or not execution_log_id:
|
|||
|
|
logger.warning("卡片动作缺少 action 或 execution_log_id: %s", action_value)
|
|||
|
|
return _toast_response("操作无效")
|
|||
|
|
|
|||
|
|
card_title = action_value.get("card_title", agent_name or "Agent 回复")
|
|||
|
|
|
|||
|
|
# 处理不同的反馈动作
|
|||
|
|
if action == ACTION_FEEDBACK_THUMBS_UP:
|
|||
|
|
_record_feedback(execution_log_id, agent_name, "thumbs_up", open_id)
|
|||
|
|
toast = "收到,感谢反馈!"
|
|||
|
|
replacement_card = _build_replacement_card(
|
|||
|
|
execution_log_id, card_title, ACTION_FEEDBACK_THUMBS_UP
|
|||
|
|
)
|
|||
|
|
return _card_toast_response(toast, replacement_card)
|
|||
|
|
|
|||
|
|
elif action == ACTION_FEEDBACK_THUMBS_DOWN:
|
|||
|
|
_record_feedback(execution_log_id, agent_name, "thumbs_down", open_id)
|
|||
|
|
toast = "收到,我们会持续改进。"
|
|||
|
|
replacement_card = _build_replacement_card(
|
|||
|
|
execution_log_id, card_title, ACTION_FEEDBACK_THUMBS_DOWN
|
|||
|
|
)
|
|||
|
|
return _card_toast_response(toast, replacement_card)
|
|||
|
|
|
|||
|
|
elif action == ACTION_FEEDBACK_REGENERATE:
|
|||
|
|
_trigger_regeneration(execution_log_id, agent_name, open_id, event)
|
|||
|
|
toast = "正在重新生成..."
|
|||
|
|
# 改卡片为"重新生成中"状态,移除反馈按钮
|
|||
|
|
regenerating_card = build_feedback_response_card(
|
|||
|
|
card_title, "", ACTION_FEEDBACK_REGENERATE
|
|||
|
|
)
|
|||
|
|
return _card_toast_response(toast, regenerating_card)
|
|||
|
|
|
|||
|
|
else:
|
|||
|
|
logger.warning("未知卡片动作: %s", action)
|
|||
|
|
return _toast_response("操作无效")
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error("处理卡片动作异常: %s", e, exc_info=True)
|
|||
|
|
return _toast_response("处理失败,请稍后重试")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _record_feedback(
|
|||
|
|
execution_log_id: str, agent_name: str, signal_type: str, open_id: str
|
|||
|
|
) -> None:
|
|||
|
|
"""记录用户反馈到 FeedbackLearner 或直接更新日志评分。"""
|
|||
|
|
try:
|
|||
|
|
from app.services.feedback_learner import feedback_learner
|
|||
|
|
|
|||
|
|
if signal_type == "thumbs_up":
|
|||
|
|
feedback_learner.record_feedback(
|
|||
|
|
user_id=open_id,
|
|||
|
|
signal_type="thumbs_up",
|
|||
|
|
execution_log_id=execution_log_id,
|
|||
|
|
agent_name=agent_name,
|
|||
|
|
feedback_context={"source": "feishu_card", "open_id": open_id},
|
|||
|
|
)
|
|||
|
|
_update_execution_rating(execution_log_id, rating=5)
|
|||
|
|
elif signal_type == "thumbs_down":
|
|||
|
|
feedback_learner.record_feedback(
|
|||
|
|
user_id=open_id,
|
|||
|
|
signal_type="thumbs_down",
|
|||
|
|
execution_log_id=execution_log_id,
|
|||
|
|
agent_name=agent_name,
|
|||
|
|
feedback_context={"source": "feishu_card", "open_id": open_id},
|
|||
|
|
)
|
|||
|
|
_update_execution_rating(execution_log_id, rating=1)
|
|||
|
|
logger.info(
|
|||
|
|
"用户反馈已记录: signal=%s execution_log=%s agent=%s",
|
|||
|
|
signal_type, execution_log_id, agent_name,
|
|||
|
|
)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("记录反馈失败: %s", e)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _update_execution_rating(execution_log_id: str, rating: int) -> None:
|
|||
|
|
"""更新 AgentExecutionLog 的用户评分。"""
|
|||
|
|
try:
|
|||
|
|
from app.core.database import SessionLocal
|
|||
|
|
from app.models.agent_execution_log import AgentExecutionLog
|
|||
|
|
|
|||
|
|
db = SessionLocal()
|
|||
|
|
try:
|
|||
|
|
log_entry = db.query(AgentExecutionLog).filter(
|
|||
|
|
AgentExecutionLog.id == execution_log_id
|
|||
|
|
).first()
|
|||
|
|
if log_entry:
|
|||
|
|
log_entry.user_rating = rating
|
|||
|
|
db.commit()
|
|||
|
|
finally:
|
|||
|
|
db.close()
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("更新执行日志评分失败: %s", e)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _trigger_regeneration(
|
|||
|
|
execution_log_id: str, agent_name: str, open_id: str, event: Any
|
|||
|
|
) -> None:
|
|||
|
|
"""触发 Agent 重新生成回复。"""
|
|||
|
|
try:
|
|||
|
|
from app.tasks.agent_tasks import execute_agent_task
|
|||
|
|
from app.models.agent_execution_log import AgentExecutionLog
|
|||
|
|
|
|||
|
|
db = _get_db_session()
|
|||
|
|
if not db:
|
|||
|
|
return
|
|||
|
|
try:
|
|||
|
|
log_entry = db.query(AgentExecutionLog).filter(
|
|||
|
|
AgentExecutionLog.id == execution_log_id
|
|||
|
|
).first()
|
|||
|
|
if log_entry and log_entry.input_text:
|
|||
|
|
execute_agent_task.delay(
|
|||
|
|
agent_name,
|
|||
|
|
{"USER_INPUT": log_entry.input_text, "regenerate": True},
|
|||
|
|
)
|
|||
|
|
logger.info("已触发重新生成: agent=%s input=%s", agent_name, log_entry.input_text[:50])
|
|||
|
|
finally:
|
|||
|
|
db.close()
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("触发重新生成失败: %s", e)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_db_session():
|
|||
|
|
"""获取数据库会话。"""
|
|||
|
|
try:
|
|||
|
|
from app.core.database import SessionLocal
|
|||
|
|
return SessionLocal()
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("获取数据库会话失败: %s", e)
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _toast_response(toast_text: str) -> Any:
|
|||
|
|
"""创建卡片动作 Toast 响应。"""
|
|||
|
|
from lark_oapi.event.callback.model.p2_card_action_trigger import (
|
|||
|
|
CallBackToast,
|
|||
|
|
P2CardActionTriggerResponse,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return P2CardActionTriggerResponse(
|
|||
|
|
toast=CallBackToast(type_="info", content=toast_text)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _card_toast_response(toast_text: str, card: Dict[str, Any]) -> Any:
|
|||
|
|
"""创建同时包含 Toast 和替换卡片的响应。"""
|
|||
|
|
from lark_oapi.event.callback.model.p2_card_action_trigger import (
|
|||
|
|
CallBackToast,
|
|||
|
|
CallBackCard,
|
|||
|
|
P2CardActionTriggerResponse,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return P2CardActionTriggerResponse(
|
|||
|
|
toast=CallBackToast(type_="info", content=toast_text),
|
|||
|
|
card=CallBackCard(data=card),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _build_replacement_card(
|
|||
|
|
execution_log_id: str, card_title: str, action: str
|
|||
|
|
) -> Dict[str, Any]:
|
|||
|
|
"""为反馈动作构建替换卡片(移除反馈按钮,显示确认信息)。"""
|
|||
|
|
original_content = ""
|
|||
|
|
try:
|
|||
|
|
from app.core.database import SessionLocal
|
|||
|
|
from app.models.agent_execution_log import AgentExecutionLog
|
|||
|
|
|
|||
|
|
db = SessionLocal()
|
|||
|
|
try:
|
|||
|
|
log_entry = db.query(AgentExecutionLog).filter(
|
|||
|
|
AgentExecutionLog.id == execution_log_id
|
|||
|
|
).first()
|
|||
|
|
if log_entry and log_entry.output_text:
|
|||
|
|
# 截断过长内容,保留前 3000 字符
|
|||
|
|
original_content = log_entry.output_text
|
|||
|
|
if len(original_content) > 3000:
|
|||
|
|
original_content = original_content[:3000] + "\n\n...(内容过长已截断)"
|
|||
|
|
finally:
|
|||
|
|
db.close()
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("获取原始回复内容失败: %s", e)
|
|||
|
|
|
|||
|
|
return build_feedback_response_card(
|
|||
|
|
original_title=card_title,
|
|||
|
|
original_content=original_content,
|
|||
|
|
action=action,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 导出 handler 函数引用,供 WS Handler 注册
|
|||
|
|
card_action_handler = handle_card_action
|