浏览代码

Merge branch 'feature/dev/20250605-dev' into dev-xym-add-test-task

xueyiming 2 天之前
父节点
当前提交
ae8834f598

+ 453 - 0
evaluate_agent_v2.py

@@ -0,0 +1,453 @@
+import concurrent
+import datetime
+import json
+import random
+import time
+
+from tqdm import tqdm
+from openai import OpenAI
+from typing import List, Dict
+from pymysql.cursors import DictCursor
+
+# from dev import push_message
+from pqai_agent.database import MySQLManager
+from pqai_agent.logging_service import logger
+from pqai_agent import configs, logging_service
+from pqai_agent_server.utils.prompt_util import format_dialogue_history
+
+logging_service.setup_root_logger()
+
+
+def fetch_deepseek_completion(prompt, output_type="text"):
+    """
+    deep_seek方法
+    """
+    # client = OpenAI(
+    #     api_key="sk-cfd2df92c8864ab999d66a615ee812c5",
+    #     base_url="https://api.deepseek.com",
+    # )
+    client = OpenAI(
+        api_key='sk-47381479425f4485af7673d3d2fd92b6',
+        base_url='https://dashscope.aliyuncs.com/compatible-mode/v1',
+    )
+
+    # get response format
+    if output_type == "json":
+        response_format = {"type": "json_object"}
+    else:
+        response_format = {"type": "text"}
+
+    chat_completion = client.chat.completions.create(
+        messages=[
+            {
+                "role": "user",
+                "content": prompt,
+            }
+        ],
+        # model="deepseek-chat",
+        model='qwen3-235b-a22b',
+        response_format=response_format,
+        stream=False,
+        extra_body={"enable_thinking": False}
+    )
+    response = chat_completion.choices[0].message.content
+    if output_type == "json":
+        response_json = json.loads(response)
+        return response_json
+
+    return response
+
+
+
+def compose_dialogue(dialogue: List[Dict], timestamp_type: str='ms') -> str:
+    role_map = {'user': '用户', 'assistant': '客服'}
+    messages = []
+    for msg in dialogue:
+        if not msg['content']:
+            continue
+        if msg['role'] not in role_map:
+            continue
+        if timestamp_type == 'ms':
+            format_dt = datetime.datetime.fromtimestamp(msg['timestamp'] / 1000).strftime('%Y-%m-%d %H:%M:%S')
+        else:
+            format_dt = datetime.datetime.fromtimestamp(msg['timestamp']).strftime('%Y-%m-%d %H:%M:%S')
+        msg_type = "文本"
+        messages.append('[{}][{}][{}]{}'.format(role_map[msg['role']], format_dt, msg_type, msg['content']))
+    return '\n'.join(messages)
+
+
+class AgentEvaluator:
+
+    def __init__(self) -> None:
+        config = {
+            "host": "rm-bp13g3ra2f59q49xs.mysql.rds.aliyuncs.com",
+            "port": 3306,
+            "user": "wqsd",
+            "password": "wqsd@2025",
+            "database": "ai_agent",
+            "charset": "utf8mb4",
+        }
+        self.mysql_client = MySQLManager(config)
+        self.output_format = {
+            "1.1": {
+                "score": 1,
+                "reason": "理由"
+            },
+            "1.2": {
+                "score": 0,
+                "reason": "理由"
+            }
+        }
+
+    def get_profile_info(self, user_id_, user_type):
+        match user_type:
+            case "user":
+                sql = f"""
+                    select iconurl as 'avatar', profile_data_v1 as 'profile' 
+                    from third_party_user where third_party_user_id = %s; 
+                """
+            case "staff":
+                sql = f"""
+                    select agent_profile as 'profile'
+                    from qywx_employee where third_party_user_id = %s;
+                """
+            case _:
+                raise ValueError("user_type must be 'user' or 'staff'")
+
+        return self.mysql_client.select(sql, cursor_type=DictCursor, args=(user_id_,))
+
+
+output_dict =   {
+          "1.1": { "score": 1, "reason": "识别到用户焦虑并先安抚" },
+          "2.1": { "score": 0, "reason": "跳过健康话题改聊理财" },
+          "5.4": { "score": 1, "reason": "青年男性用词简洁,无女性化词汇" },
+          "7.5": { "score": 1, "reason": "2025-05-28 发端午祝福;端午=2025-05-31" }
+        }
+
+def generate_prompt(dialogue_history: str, message: str,
+                    send_time: str, user_profile: Dict, agent_profile: Dict) -> str:
+    """
+    生成评估prompt
+    :return: prompt
+    """
+    prompt = f"""
+## 评估任务说明
+当 客服与用户长时间无互动时,客服会主动推送 message 以维系联系。  
+请根据输入信息,对该 message 按下列维度逐项打分。
+
+输入字段:
+- 过往对话
+- 用户画像
+- 客服人设
+- 本次推送内容
+- 推送时间(UTC+8)
+
+评分规则:
+- 每个 **子指标** 只取 0 或 1 分。  
+  1 分:满足判分要点,或该项“无需评估”  
+  0 分:不满足判分要点  
+- 每项请附“简要中文理由”;若不适用,请写“无需评估”。
+
+────────────────────────
+## 评估维度与评分细则(含示例)
+
+### 1. 理解能力
+1.1 客服是否感知用户情绪  
+  判分要点:  
+    1) 是否识别出用户最近情绪(积极/中性/消极)。  
+    2) 是否据此调整推送语气或内容。  
+  正例:  
+    • 用户上次说“工作压力大,很累。” → push 先关怀:“最近辛苦了,给你 3 个放松小技巧…”  
+    • 用户上次兴奋分享球赛胜利 → push 用同频语气:“昨晚那球真绝!还想复盘关键回合吗?”  
+  反例:  
+    • 用户上次抱怨“数据全丢了” → push 却强推会员特价,未安抚情绪。  
+    • 用户上次沮丧 → push 用过度欢快口吻“早呀宝子!冲鸭!”情绪不匹配。  
+
+### 2. 上下文管理
+2.1 客服是否延续上文话题  
+  判分要点:推送是否围绕上次核心主题,或自然衍生。  
+  正例:  
+    • 上次讨论“糖尿病饮食”,本次补充低 GI 零食建议。  
+  反例:  
+    • 上次聊健康,本次突然推荐炒股课程。  
+
+2.2 客服是否记住上文信息  
+  判分要点:是否正确引用历史细节、进度或偏好。  
+  正例:  
+    • 记得用户已经下载“春季食谱”,不再重复发送,而是询问体验。  
+  反例:  
+    • 忘记用户已完成注册,仍提示“点击注册开始体验”。  
+
+### 3. 背景知识一致性
+3.1 客服推送的消息是否不超出角色认知范围  
+  判分要点:建议、结论不得超出职业权限或法律限制。  
+  正例:  
+    • 健康顾问提醒“如症状持续请就医”。  
+  反例:  
+    • 健康顾问直接诊断病情并开药剂量。  
+
+3.2  客服推送的消息用到的词汇是否符合当前时代
+  判分要点:不使用明显过时事物或词汇,符合当前年代语境。  
+  正例:  
+    • 提到“短视频带货”。  
+  反例:  
+    • 推荐“BP 机”“刻录 DVD”。  
+
+3.3  客服推送消息的知识是否知识符合角色设定  
+  判分要点:内容深度与 客服专业水平相符。  
+  正例:  
+    • 金融助理解释“FOF 与 ETF 的风险差异”。  
+  反例:  
+    • 金融助理说“基金我也不懂”。  
+
+### 4. 性格行为一致性
+4.1  客服推送的消息是否符合同一性格  
+  判分要点:语气、用词保持稳定,符合人设。  
+  正例:  
+    • 一贯稳重、有条理。  
+  反例:  
+    • 突然使用辱骂或极端情绪。  
+
+4.2  客服推送的消息是否符合正确的价值观、道德观  
+  判分要点:不得鼓励违法、暴力、歧视或色情。  
+  正例:  
+    • 拒绝提供盗版资源。  
+  反例:  
+    • 教唆赌博“稳赚不赔”。  
+
+### 5. 语言风格一致性
+5.1  客服的用词语法是否匹配身份背景学历职业
+  判分要点:专业角色→专业术语;生活助手→通俗易懂。  
+  正例:  
+    • 医生用“血糖达标范围”。  
+  反例:  
+    • 医生说“你随便吃点吧”。  
+
+5.2  客服的语气是否保持稳定  
+  判分要点:整条消息语气前后一致,无突变。  
+  正例:  
+    • 始终友好、耐心。  
+  反例:  
+    • 开头热情,末尾生硬“速回”。  
+
+5.3 客服是否保持角色表达习惯  
+  判分要点:是否保持固定口头禅、签名等表达习惯。  
+  正例:  
+    • 每次结尾用“祝顺利”。  
+  反例:  
+    • 突然改用网络缩写“nbcs”。  
+
+5.4  客服推送消息语言风格是否匹配其年龄 & 性别(禁忌词检测,重点审)  
+  判分要点:  
+    - 词汇选择符合年龄段典型语言;  
+    - 男性禁止出现明显女性化语气词。比如说:呢、啦、呀、宝子、yyds;  
+    - 45+ 及以上避免“冲鸭”“绝绝子”“yyds”等新潮词;  
+    - 青年男性应简洁直接,可偶用“哈哈”“酷”;青年女性可用“呀”“哦”;  
+    - 不出现与性别、年龄严重背离的口头禅
+  正例:  
+    • 30 岁男性:“这两篇文章挺硬核,你可以先看第二节。”  
+    • 25 岁女性:“好的呀~我整理了 3 个小 tips,给你噢!”  
+  反例:  
+    • 50 岁男性:“姐妹们冲鸭!绝绝子!”  
+    • 22 岁男性:“您若有任何疑虑敬请垂询。”(用老派公文腔)  
+    • 男性:出现"呢、呀、哦、啦"等女性化语气词
+
+5.5 客服推送的消息是否符合其职业典型  
+  判分要点:符合行业常用语气、格式、礼貌级别。  
+  正例:  
+    • 律师引用条款:“根据《合同法》第 60 条…”  
+  反例:  
+    • 律师说“嗨哥们,合同没啥大事”。  
+
+### 6. 关系认知
+6.1 客服是否遵循角色与用户互动模式  
+  判分要点:助理/客服应服务姿态;导师应指导姿态等。  
+  正例:  
+    • “我来为您安排”。  
+  反例:  
+    • “听我的,不准反驳”。  
+
+6.2 客服是否自身身份准确  
+  判分要点:不得冒充更高权限或他人。  
+  正例:  
+    • “作为您的健康顾问,我建议…”  
+  反例:  
+    • 自称“医院主任医师”但实际是健康顾问。  
+
+6.3  客服推送内容是否不超出用户理解范围  
+  判分要点:专业解释需配必要说明,面向老人用浅显词。  
+  正例:  
+    • 用“血糖=身体里的糖分浓度”解释概念。  
+  反例:  
+    • 连续堆砌专有缩写“LDL、HOMA-IR”不解释。  
+
+6.4  客服是否不透露 AI 身份  
+  判分要点:不得说“我是 AI/机器人/大模型”。  
+  正例:  
+    • 使用“我”为第一人称即可。  
+  反例:  
+    • “我是一款 GPT 模型”。  
+
+### 7. 对话唤起
+7.1 客服的唤起消息是否多样、非机械  
+  判分要点:句式内容变化,避免模板。  
+  正例:  
+    • “你追的剧更新啦,最燃打斗你打几分?”  
+  反例:  
+    • 每日“晚上好!今天看篮球吗?”  
+
+7.2  客服推送消息是否关注用户兴趣 / 地域  
+  判分要点:结合兴趣、昵称、地域、称呼。  
+  正例:  
+    • 用户爱猫,push 附猫咪护理小贴士。  
+  反例:  
+    • 用户讨厌广告,push 仍发折扣券。  
+
+7.3  客服推送消息是否解决上文遗留的合理需求(如有)  
+  判分要点:补完信息、修正错误或跟进任务。  
+  正例:  
+    • 上次承诺发教材,本次附下载链接。  
+  反例:  
+    • 用户等待答复,push 却忽略。  
+
+7.4  客服推送消息是否明确表现继续聊天意图  
+  判分要点:包含提问或邀请,鼓励回复。  
+  正例:  
+    • “看完后告诉我你的想法,好吗?”  
+  反例:  
+    • 仅单向播报:“祝好。”  
+
+7.5  客服推送节日祝福时间节点是否合适
+  判分要点:农历节日前 5 天内发送祝福得分为 1 分,若无需评估,得分也为 1 分
+  正例:  
+    • 2025-05-28 发送“端午安康”(端午 2025-05-31)。  
+  反例:  
+    • 端午 6-2 才补发“端午快乐”。  
+
+────────────────────────
+## 输出格式示例
+输出结果为一个JSON,JSON的第一层,每一个 key 代表评估指标的 id,比如 “7.5” 代表“节日祝福及时”
+value 也是一个JSON,包含两个 key:score 和 reason,分别代表分数和理由。
+分数只能是 0 或 1,代表是否通过判分。
+理由是一个字符串,代表判分依据。
+以下是一个示例输出:
+{output_dict}
+
+## 输入信息
+### 对话历史
+{dialogue_history}
+### 用户画像
+{user_profile}
+### 客服人设
+{agent_profile}
+### 本次推送内容
+{message}
+### 推送时间
+{send_time}
+"""
+    return prompt
+
+
+class PushMessageEvaluator(AgentEvaluator):
+
+    def get_push_dataset(self):
+        sql = f"""
+            select staff_id, user_id, conversation, content, send_time
+            from internal_conversation_data
+            where dataset_id = 2;
+        """
+        return self.mysql_client.select(sql, cursor_type=DictCursor)
+
+
+    def get_dialogue_history_by_id(self, staff_id, dialogue_id_tuple):
+        sql = f"""
+            select sender, sendtime, content
+            from qywx_chat_history
+            where id in %s;
+        """
+
+        conversation_list = self.mysql_client.select(sql=sql, cursor_type=DictCursor, args=(dialogue_id_tuple,))
+        history_conversation = [
+            {
+                "content": i['content'],
+                "role": "assistant" if i['sender'] == staff_id else "user",
+                "timestamp": i['sendtime']
+            } for i in conversation_list
+        ]
+        return history_conversation
+
+    def evaluate_task(self, line):
+        # staff_id = line['staff_id']
+        # user_id = line['user_id']
+        # conversation_id_list = json.loads(line['conversation'])
+        # push_message = line['content']
+        # send_time = line['send_time']
+        # send_date_str = datetime.datetime.fromtimestamp(send_time).strftime('%Y-%m-%d %H:%M:%S')
+        # dialogue_list = self.get_dialogue_history_by_id(staff_id, tuple(conversation_id_list))
+        # format_dialogue = compose_dialogue(dialogue_list)
+        # agent_profile = self.get_profile_info(staff_id, "staff")[0]['profile']
+        # agent_profile = json.loads(agent_profile)
+        # user_profile = self.get_profile_info(user_id, "user")[0]['profile']
+        # user_profile = json.loads(user_profile)
+        user_profile = line["user_profile"]
+        agent_profile = line["agent_profile"]
+        send_date_str = line["push_time"]
+        push_message = line["push_message"]
+        format_dialogue = line['dialogue_history']
+        evaluator_prompt = generate_prompt(
+            dialogue_history=format_dialogue,
+            message=push_message,
+            send_time=send_date_str,
+            agent_profile=agent_profile,
+            user_profile=user_profile,
+        )
+        print(evaluator_prompt)
+        response = fetch_deepseek_completion(evaluator_prompt, output_type='json')
+        return {
+            "user_profile": user_profile,
+            "agent_profile": agent_profile,
+            "dialogue_history": format_dialogue,
+            "push_message": push_message,
+            "push_time": send_date_str,
+            "evaluation_result": response
+        }
+
+
+    def evaluate(self):
+        # data = self.get_push_dataset()
+        with open("test_0618_r1.json", encoding="utf-8") as f:
+            data = json.loads(f.read())
+
+        samples = random.sample(data, 48)
+        samples = [i for i in data if i['push_message'] == '文芝阿姨,晚上好呀!今天有没有抽空做做颈部拉伸运动或者热敷一下颈椎呢?这些小方法对缓解头晕很有帮助哦~']
+
+        from concurrent.futures import ThreadPoolExecutor
+        from tqdm import tqdm
+        # # # 多线程处理主逻辑
+        L = []
+        with ThreadPoolExecutor(max_workers=8) as executor:  # 可根据CPU核心数调整worker数量
+            futures = []
+            for line in samples:
+                futures.append(executor.submit(self.evaluate_task, line))
+
+            # 使用tqdm显示进度
+            for future in tqdm(concurrent.futures.as_completed(futures), total=len(futures)):
+                result = future.result()
+                if result:
+                    print(json.dumps(result, ensure_ascii=False, indent=4))
+                    L.append(result)
+        # for line in tqdm(data):
+        #     response = self.evaluate_task(line)
+        #     print("\n")
+        #     print(json.dumps(response, ensure_ascii=False, indent=4))
+        #     if response:
+        #         L.append(response)
+        # #
+        # 保存结果(与原代码相同)
+        # with open("test_0618_v3.json", "w", encoding="utf-8") as f:
+        #     json.dump(L, f, ensure_ascii=False, indent=4)
+
+
+if __name__ == '__main__':
+    P = PushMessageEvaluator()
+    P.evaluate()

+ 348 - 0
evaluate_reply_agent.py

@@ -0,0 +1,348 @@
+import json
+import datetime
+import random
+import traceback
+import concurrent.futures
+
+
+from tqdm import tqdm
+from openai import OpenAI
+from pymysql.cursors import DictCursor
+from pqai_agent.database import MySQLManager
+
+evaluation_metrics_dict = {
+    "1.2": "是否识别关键信息",
+    "1.3": "是否能够理解歧义词/模糊词",
+    "1.4": "是否能理解表情包,图片消息",
+    "1.5": "是否能理解语音/方言",
+    "2.1": "回复是否与用户意图相关",
+    "2.2": "回复是否清晰简洁",
+    "2.3": "回复是否流畅",
+    "2.4": "回复语法是否规范",
+    "3.1": "是否能理解代词(他,她, 她, 这个那个)",
+    "3.2": "是否能延续上文话题",
+    "3.3": "是否记住上文的基础信息",
+    "3.4": "是否及时结束聊天",
+    "4.1": "是否讨论超出角色认知范围的信息",
+    "4.2": "是否讨论了不符合当前时代背景的语言、物品、事件、概念",
+    "4.3": "是否表现出与agent 人设相符的专业知识、生活经验或者常识",
+    "5.1": "agent 的言行是否反映其预设的核心性格",
+    "5.2": "agent 的价值观和道德观是否符合其预设标准",
+    "6.1": "agent 使用的词汇、句式、语法复杂度、行话/俚语是否符合其身份、教育背景和时代?",
+    "6.2": "agent 语气、语调(恭敬、傲慢、亲切、疏离、热情、冷淡)是否稳定?",
+    "6.3": "agent 表达习惯、口头禅是否符合角色预设特点",
+    "7.1": "agent 在对话中表现出的目标、关注重心是否与其设定的核心动机一致?",
+    "8.1": "agent 是否按照预设的互动模式与用户沟通",
+    "8.2": "agent 是否对自身角色有正确理解",
+    "8.3": "agent 是否回复超越用户认知的信息"
+}
+
+
+def fetch_deepseek_completion(prompt, output_type='text'):
+    """
+    deep_seek方法
+    """
+    client = OpenAI(
+        api_key='sk-cfd2df92c8864ab999d66a615ee812c5',
+        base_url="https://api.deepseek.com"
+    )
+
+    # get response format
+    if output_type == "json":
+        response_format = {"type": "json_object"}
+    else:
+        response_format = {"type": "text"}
+
+    chat_completion = client.chat.completions.create(
+        messages=[
+            {
+                "role": "user",
+                "content": prompt,
+            }
+        ],
+        model="deepseek-reasoner",
+        response_format=response_format,
+    )
+    response = chat_completion.choices[0].message.content
+    if output_type == "json":
+        response_json = json.loads(response)
+        return response_json
+
+    return response
+
+
+def get_profile_info(user_id_, user_type):
+    match user_type:
+        case "user":
+            sql = f"""
+                select iconurl as 'avatar', profile_data_v1 as 'profile' 
+                from third_party_user where third_party_user_id = %s; 
+            """
+        case "staff":
+            sql = f"""
+                select agent_profile as 'profile'
+                from qywx_employee where third_party_user_id = %s;
+            """
+        case _:
+            raise ValueError("user_type must be 'user' or 'staff'")
+
+    return mysql_client.select(sql, cursor_type=DictCursor, args=(user_id_,))
+
+
+def evaluate_reply_agent_prompt(dialogue_history, message, user_profile_, agent_profile, push_time):
+    """
+
+    :param dialogue_history:
+    :param message:
+    :param user_profile_:
+    :param agent_profile:
+    :return:
+    """
+    output_format = {
+        "1.1": {
+            "score": 1,
+            "reason": "理由"
+        },
+        "1.2": {
+            "score": 0,
+            "reason": "理由"
+        }
+    }
+    prompt_ = f"""
+    **评估任务说明:**
+    你需要对 agent 当前回复的消息(message)进行质量评估。  
+    请基于以下输入信息:
+    - 历史对话记录:dialogue_history  
+    - 用户预设信息:user_profile  
+    - agent 预设信息:agent_profile  
+    - 消息发送时间:push_time  
+    结合以下指标打分,**每个子指标满分 1 分**:
+    **评估维度与示例说明:**
+    ### 1. 理解能力
+    - **1.1 是否识别用户核心意图**
+      - 正例:用户:这款适合老人吗?→ agent:是的,它字体更大、操作简单
+      - 负例:回复:“颜色有红蓝两种” → 偏离意图
+      
+    - **1.2 是否识别关键信息**
+      - 正例:用户提到“糖尿病”,agent 结合健康推荐产品
+      - 负例:忽略关键信息,只介绍型号/库存
+      
+    - **1.3 是否理解歧义词或模糊表达**
+      - 正例:用户说“那个不错”,agent 明确“您是指X产品吗?”
+      - 负例:直接“感谢喜欢”,未澄清
+
+    - **1.4 是否理解表情/图片**
+      - 正例:用户发 👍 → agent 回复“收到,我帮您下单”
+      - 负例:用户发 🙄 → agent 回复“感谢支持”
+
+    - **1.5 是否理解语音/方言(转写内容)**
+      - 正例:“想搞个便宜点的” → 理解为追求性价比
+      - 负例:回复“我们不卖便宜货” → 理解偏差
+
+    ### 2. 回复能力
+    - **2.1 回复是否与用户意图相关**
+      - 正例:用户问退货 → agent 回复具体流程
+      - 负例:agent 回复“本店新品推荐”
+
+    - **2.2 回复是否清晰简洁**
+      - 正例:“退货可在APP内申请,我们会上门取件”
+      - 负例:“嗯这个如果说退货吧,其实我们也可以...”
+
+    - **2.3 回复是否流畅**
+      - 正例:语言通顺无跳跃
+      - 负例:表达混乱,“如果你申请,我帮你弄好,那样能退款也可以”
+
+    - **2.4 回复语法是否规范**
+      - 正例:“欢迎再次光临”
+      - 负例:“我帮你处理了这个东西您可以看下有没有不对的”
+    
+    - **2.5 回复是否具有机械性**
+      - 正例:回复的语句需要保持正常聊天风格
+      - 负例:每次回复消息均包含用户称呼等属于首次聊天需要用到的称呼语句
+
+    ### 3. 上下文管理能力
+    - **3.1 是否正确理解代词**
+      - 正例:用户:“他说不错” → agent 理解“他”为儿子
+      - 负例:理解为用户本人
+
+    - **3.2 是否延续上文话题**
+      - 正例:上轮聊智能手表 → 本轮继续其功能
+      - 负例:突然推广耳机
+
+    - **3.4 是否能及时结束对话**
+      - 正例:用户说“好的谢谢” → agent 回复“有需要随时联系”
+      - 负例:用户已表达结束意图 → agent 仍持续推销
+
+    ### 4. 背景知识一致性
+    - **4.1 是否超出角色认知范围**
+      - 正例:AI客服:推荐就医 → 建议联系医生  
+      - 负例:自称能诊断病症  
+
+    - **4.2 是否使用错误时代背景或过时词汇**
+      - 正例:使用当下流行产品/概念
+      - 负例:讨论 BP 机、DVD 机等  
+
+    - **4.3 是否展现出与角色设定一致的知识/经验**
+      - 正例:金融顾问角色能清晰解释基金风险
+      - 负例:理财助手说“我也不太懂”
+
+    ### 5. 性格行为一致性
+    - **5.1 言行是否体现预设性格**
+      - 正例:设定为“亲切” → 用词温和,如“亲爱的、别担心~”
+      - 负例:忽冷忽热,或说话带攻击性
+
+    - **5.2 价值观与道德是否一致**
+      - 正例:拒绝不当请求、拒绝传播敏感信息
+      - 负例:发表不当政治/色情暗示 / 赌博相关
+
+    ### 6. 语言风格一致性
+    - **6.1 用词语法是否匹配身份背景**
+      - 正例:医生角色用专业术语适度表达, 年长角色说话语气和小姑娘小孩子一样
+      - 负例:医生说“这个就随便吃点吧”, 65岁的角色说“好呀,好呢”这样的话
+
+    - **6.2 语气是否保持稳定**
+      - 正例:始终热情/恭敬/冷静
+      - 负例:开头热情,后文冷淡  
+
+    - **6.3 是否保持角色表达习惯**
+      - 正例:师傅型角色口头禅“照您说的办”
+      - 负例:前后说话风格差异明显
+
+    ### 7. 目标动机一致性
+    - **7.1 是否体现核心目标**
+      - 正例:核心是为了和用户保持联系,为用户提供情绪价值  
+      - 负例:一味推销
+
+    ### 8. 关系认知一致性
+    - **8.1 是否遵循角色与用户之间的互动模式**
+      - 正例:设定为“助理”→ 使用“我来为您处理”, 用户是男性,使用“先生”称呼
+      - 负例:助理语气“我说了算”,过于权威,用户是男性,使用“女士”称呼
+
+    - **8.2 是否正确理解自己身份**
+      - 正例:客服表明“我可以帮您提交”
+      - 负例:自称“我是系统管理员”  
+
+    - **8.3 是否回复超越用户可理解范围**
+      - 正例:面向老人用简洁语言解释  
+      - 负例:一上来即使用技术术语堆叠回复  
+
+    **评估规则:**
+    - 每个子项:
+      - 符合要求:1 分
+      - 不符合要求:0 分
+      - 未涉及/不适用:1 分,理由写“无需评估”
+    - 每项后附简要中文评估理由,客观明确。
+
+    **输入:**
+        - **对话历史**: {dialogue_history}
+        - **Agent 预设信息**: {agent_profile}
+        - **用户预设信息**: {user_profile_}
+        - **Agent 消息**: {message}
+        - **Agent 发送消息时间**:{push_time}
+        
+        **输出格式要求:JSON 格式**
+        输出格式参考:{output_format}
+        
+    """
+    return prompt_
+
+
+config = {
+    'host': 'rm-bp13g3ra2f59q49xs.mysql.rds.aliyuncs.com',
+    'port': 3306,
+    'user': 'wqsd',
+    'password': 'wqsd@2025',
+    'database': 'ai_agent',
+    'charset': 'utf8mb4'
+}
+mysql_client = MySQLManager(config)
+
+
+if __name__ == '__main__':
+    import pqai_agent.logging_service
+
+    pqai_agent.logging_service.setup_root_logger()
+    with open("reply_data_set_filter_2.json", "r", encoding="utf-8") as f:
+        data = json.load(f)
+
+    data = [i for i in data if i['user_active_rate'] > 0.4]
+
+    print(len(data))
+
+    # 随机选择100个对话
+    dialogues = random.sample(data, 80)
+
+    dialogue_with_profile = []
+    for dialogue in dialogues:
+        agent_profile = get_profile_info(dialogue['staff_id'], 'staff')
+        user_profile = get_profile_info(dialogue['user_id'], 'user')
+        dialogue['agent_profile'] = json.loads(agent_profile[0]['profile'])
+        dialogue['user_profile'] = json.loads(user_profile[0]['profile'])
+
+        dialogue_with_profile.append(dialogue)
+
+
+    F = []
+    errors = []
+    from threading import Lock
+    import concurrent.futures
+
+    write_lock = Lock()
+
+
+    def process_sample(sub_dialogues):
+        try:
+            message = sub_dialogues["conversation"]
+            agent_message = sub_dialogues["reply_msg"]
+            push_time = sub_dialogues["reply_time"]
+            user_profile = sub_dialogues["user_profile"]
+            staff_profile = sub_dialogues["agent_profile"]
+
+            if not agent_message:
+                return None
+
+            prompt = evaluate_reply_agent_prompt(
+                message, agent_message, user_profile, staff_profile, push_time
+            )
+            response = fetch_deepseek_completion(prompt, output_type='json')
+
+            return {
+                "user_profile": user_profile,
+                "agent_profile": staff_profile,
+                "dialogue_history": message,
+                "push_message": agent_message,
+                "push_time": push_time,
+                "evaluation_result": response
+            }
+
+        except Exception as e:
+            # 捕获异常并存储
+            error_msg = f"Error processing sample: {e}\n{traceback.format_exc()}"
+            with write_lock:
+                errors.append(error_msg)
+            return None
+
+
+    # 使用线程池处理
+    with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
+        # 提交所有任务
+        futures = {executor.submit(process_sample, sample): sample for sample in dialogues}
+
+        # 使用tqdm创建进度条
+        for future in tqdm(concurrent.futures.as_completed(futures), total=len(dialogues), desc="Evaluating"):
+            result = future.result()
+            if result:
+                with write_lock:
+                    F.append(result)
+
+    # 打印处理过程中遇到的错误
+    if errors:
+        print(f"\nEncountered {len(errors)} errors during processing:")
+        for error in errors[:5]:  # 最多打印前5个错误
+            print(error)
+
+    # 保存结果
+    with open("push_message_evaluation_result_7.json", "w", encoding="utf-8") as f:
+        json.dump(F, f, ensure_ascii=False, indent=4)
+
+

+ 305 - 0
generate_data_set.py

@@ -0,0 +1,305 @@
+import json
+import time
+import random
+import traceback
+
+from datetime import datetime
+from typing import Dict, List, Tuple
+
+from openai import OpenAI
+from tqdm import tqdm
+from pymysql.cursors import DictCursor
+from pqai_agent.database import MySQLManager
+from pqai_agent.agents.message_push_agent import MessagePushAgent
+from pqai_agent.logging_service import logger
+from pqai_agent import configs, logging_service
+from pqai_agent.mq_message import MessageType
+
+logging_service.setup_root_logger()
+
+
+config = {
+    'host': 'rm-bp13g3ra2f59q49xs.mysql.rds.aliyuncs.com',
+    'port': 3306,
+    'user': 'wqsd',
+    'password': 'wqsd@2025',
+    'database': 'ai_agent',
+    'charset': 'utf8mb4'
+}
+mysql_client = MySQLManager(config)
+
+
+def split_dialogue_history(dialogue_history_, timeout=30*60*1000):
+    """
+    :param dialogue_history_:
+    :param timeout: 30 minutes
+    :return:
+    """
+    messages_sorted = sorted(dialogue_history_, key=lambda x: x['timestamp'])
+    dialogues = []
+    current_dialogue = []
+
+    for i, msg in enumerate(messages_sorted):
+        if not current_dialogue:
+            current_dialogue.append(msg)
+            continue
+
+        prev_msg = messages_sorted[i - 1]
+        time_diff = msg["timestamp"] - prev_msg["timestamp"]
+
+        # 判断是否为新对话
+        is_new_dialogue = False
+        if time_diff > timeout:
+            is_new_dialogue = True
+
+        if is_new_dialogue:
+            dialogues.append(current_dialogue)
+            current_dialogue = [msg]
+        else:
+            current_dialogue.append(msg)
+
+    if current_dialogue:
+        dialogues.append(current_dialogue)
+
+    return dialogues
+
+
+def get_conversation_info():
+    sql = f"""
+        select roomid, count(id) as 'article_num'
+        from qywx_chat_history where msg_type in (1,2,4) group by roomid
+        having count(id) > 20;
+    """
+    return mysql_client.select(sql, cursor_type=DictCursor)
+
+
+def get_dialogue_history(room_id_):
+    """
+    获取对话历史
+    :param room_id_:
+    :return:
+    """
+    sql = f"""
+        select id, sender, receiver, sendtime, content
+        from qywx_chat_history
+        where roomid = %s and msg_type in %s order by sendtime;
+    """
+    return mysql_client.select(sql=sql, cursor_type=DictCursor, args=(room_id_, (1, 2, 4)))
+
+
+def get_dialogue_history_by_id(staff_id, dialogue_id_tuple):
+    sql = f"""
+        select sender, sendtime, content
+        from qywx_chat_history
+        where id in %s;
+    """
+
+    conversation_list = mysql_client.select(sql=sql, cursor_type=DictCursor, args=(dialogue_id_tuple,))
+    history_conversation = [
+        {
+            "content": i['content'],
+            "role": "assistant" if i['sender'] == staff_id else "user",
+            "timestamp": i['sendtime']
+        } for i in conversation_list
+    ]
+    return history_conversation
+
+
+def get_profile_info(user_id_, user_type):
+    match user_type:
+        case "user":
+            sql = f"""
+                select iconurl as 'avatar', profile_data_v1 as 'profile' 
+                from third_party_user where third_party_user_id = %s; 
+            """
+        case "staff":
+            sql = f"""
+                select agent_profile as 'profile'
+                from qywx_employee where third_party_user_id = %s;
+            """
+        case _:
+            raise ValueError("user_type must be 'user' or 'staff'")
+
+    return mysql_client.select(sql, cursor_type=DictCursor, args=(user_id_,))
+
+
+def generate_reply_dataset():
+    conversation_info_list = get_conversation_info()
+    data_set = []
+    for conversation_info in tqdm(conversation_info_list):
+        room_id = conversation_info["roomid"]
+        staff_id = room_id.split(":")[1]
+        if staff_id in ('1688854974625870', '1688856125791790', '1688856125791452'):
+            user_id = room_id.split(":")[2]
+            if staff_id and user_id:
+                dialogue_history = get_dialogue_history(room_id)
+                for idx, dialogue_info in enumerate(dialogue_history):
+                    if dialogue_info["sender"] == staff_id:
+                        conversation = dialogue_history[: idx]
+                        history_conversation = [
+                            {
+                                "id": i['id'],
+                                "content": i['content'],
+                                "role": "assistant" if i['sender'] == staff_id else "user",
+                                "timestamp": int(i['sendtime'] / 1000)
+                            } for i in conversation
+                        ]
+                        # filter history_conversation
+                        history_conversation = [i for i in history_conversation if i['timestamp'] > int(dialogue_info['sendtime'] / 1000) - 60 * 60 * 24 * 30]
+
+                        if len(history_conversation) > 20:
+                            history_conversation = history_conversation[-20:]
+
+                        eva_conversation = history_conversation[-10:]
+                        if history_conversation:
+                            user_activate_rate= len([i for i in eva_conversation if i['role'] == 'user']) / len(eva_conversation)
+                            reply_msg = dialogue_info['content']
+                            reply_time = int(dialogue_info['sendtime'] / 1000)
+                            if "早上好" in reply_msg:
+                                continue
+                            elif "早安" in reply_msg:
+                                continue
+                            elif "早" in reply_msg:
+                                continue
+                            elif "下午好" in reply_msg:
+                                continue
+                            elif "晚上好" in reply_msg:
+                                continue
+                            elif user_activate_rate < 0.3:
+                                continue
+                            else:
+                                # obj = {
+                                #     "staff_id": staff_id,
+                                #     "user_id": user_id,
+                                #     "conversation": history_conversation,
+                                #     "reply_msg": reply_msg,
+                                #     "reply_time": reply_time,
+                                #     "user_active_rate": user_activate_rate
+                                # }
+                                conversation_id_list = [i['id'] for i in history_conversation]
+                                insert_query = f"""
+                                    insert into internal_conversation_data
+                                    (dataset_id, staff_id, user_id, version_date, conversation, content, send_time, send_type, user_active_rate)
+                                    values (%s, %s, %s, %s, %s, %s, %s, %s, %s);
+                                """
+                                mysql_client.execute(insert_query, args=(
+                                    '1',
+                                    staff_id,
+                                    user_id,
+                                    '2025-06-16',
+                                    json.dumps(conversation_id_list, ensure_ascii=False),
+                                    reply_msg,
+                                    reply_time,
+                                    0,
+                                    user_activate_rate
+                                ))
+    # print(len(data_set))
+    # with open("reply_data_set_filter_2.json", "w", encoding="utf-8") as f:
+    #     f.write(json.dumps(data_set, ensure_ascii=False, indent=4))
+
+
+def compose_dialogue(dialogue: List[Dict], timestamp_type: str='ms') -> str:
+    role_map = {'user': '用户', 'assistant': '客服'}
+    messages = []
+    for msg in dialogue:
+        if not msg['content']:
+            continue
+        if msg['role'] not in role_map:
+            continue
+        if timestamp_type == 'ms':
+            format_dt = datetime.fromtimestamp(msg['timestamp'] / 1000).strftime('%Y-%m-%d %H:%M:%S')
+        else:
+            format_dt = datetime.fromtimestamp(msg['timestamp']).strftime('%Y-%m-%d %H:%M:%S')
+        msg_type = msg.get('type', MessageType.TEXT).description
+        messages.append('[{}][{}][{}]{}'.format(role_map[msg['role']], format_dt, msg_type, msg['content']))
+    return '\n'.join(messages)
+
+
+def generate_push_dataset():
+
+    fetch_query = f"""
+       select staff_id, user_id, conversation, content, send_time, user_active_rate
+       from internal_conversation_data
+       where dataset_id = 1;
+    """
+    data_set = mysql_client.select(fetch_query, cursor_type=DictCursor)
+    filter_conversation = [i for i in data_set if len(json.loads(i['conversation'])) >= 20]
+
+    samples = random.sample(filter_conversation, 300)
+
+    # init message push agent
+    for sample in tqdm(samples):
+        agent = MessagePushAgent()
+        agent_profile = get_profile_info(sample["staff_id"], "staff")[0]['profile']
+        agent_profile = json.loads(agent_profile)
+        user_profile = get_profile_info(sample["user_id"], "user")[0]['profile']
+        user_profile = json.loads(user_profile)
+        conversation = get_dialogue_history_by_id(
+            sample["staff_id"],
+            tuple(json.loads(sample["conversation"]))
+        )
+        conversation.append(
+            {
+                "content": sample["content"],
+                "role": "assistant",
+                "timestamp": sample["send_time"] * 1000,
+                # "type": 1
+            }
+        )
+        conversation = sorted(conversation, key=lambda i: i['timestamp'], reverse=False)
+
+        last_timestamp = int(conversation[-1]["timestamp"])
+        push_time = int(last_timestamp / 1000) + 24 * 3600
+        push_dt =  datetime.fromtimestamp(push_time).strftime('%Y-%m-%d %H:%M:%S')
+        try:
+            push_message = agent.generate_message(
+                context={
+                    "formatted_staff_profile": agent_profile,
+                    "nickname": user_profile.get('nickname'),
+                    "name": user_profile.get('name'),
+                    "preferred_nickname": user_profile.get('preferred_nickname'),
+                    "age": user_profile.get('age'),
+                    "region": user_profile.get('region'),
+                    "health_conditions": user_profile.get('health_conditions'),
+                    "gender": user_profile.get('gender'),
+                    "medications": user_profile.get('medications'),
+                    "interests": user_profile.get('interests'),
+                    "current_datetime": push_dt,
+                    "avatar": None
+                },
+                dialogue_history=conversation
+            )
+
+            if not push_message:
+                print("push message error")
+                continue
+            else:
+                print("push message success", push_message)
+                insert_query = f"""
+                    insert into internal_conversation_data
+                    (dataset_id, staff_id, user_id, version_date, conversation, content, send_time, send_type, user_active_rate)
+                    values (%s, %s, %s, %s, %s, %s, %s, %s, %s);
+                """
+                mysql_client.execute(insert_query, args=(
+                    '2',
+                    sample["staff_id"],
+                    sample["user_id"],
+                    '2025-06-16',
+                    sample["conversation"],
+                    push_message,
+                    push_time,
+                    1,
+                    sample["user_active_rate"]
+                ))
+        except Exception as e:
+            print("error", e)
+            print(traceback.format_exc())
+
+
+if __name__ == "__main__":
+    generate_push_dataset()
+
+
+
+
+

文件差异内容过多而无法显示
+ 1 - 0
log.txt


+ 140 - 0
pqai_agent/agents/evaluate_agent.py

@@ -0,0 +1,140 @@
+import datetime
+from typing import Optional, List, Dict
+
+from dev import dialogue
+from pqai_agent.agents.simple_chat_agent import SimpleOpenAICompatibleChatAgent
+from pqai_agent.chat_service import VOLCENGINE_MODEL_DEEPSEEK_V3
+from pqai_agent.logging_service import logger
+from pqai_agent.mq_message import MessageType
+from pqai_agent.toolkit.function_tool import FunctionTool
+from pqai_agent.toolkit.lunar_festival_mapper import LunarFestivalMapper
+
+
+
+PUSH_QUERY_PROMPT_TEMPLATE = """
+    **评估任务说明**
+    **任务场景**: 客服和用户之间有一段时间没有聊天互动了,客服通过主动推送消息,希望能和用户保持联系
+    **评估任务**: 请给予以下输入信息和评分细则,对客服唤起的消息的质量进行打分
+      **输入信息**
+        1.客服的基本信息: {agent_profile}
+        2.用户的基本信息: {user_profile}
+        3.消息发送的时间: {send_time}
+        4.发送消息内容: {message}
+        5.历史对话信息:{dialogue_history},注意对话信息的格式为: [角色][时间][消息类型]对话内容
+      **评分细则,以下每个指标满分 1 分且只会存在 1 分和 0 分两种结果**
+      **评分指标和示例说明**
+        -1. 能否感知上文中用户的情绪,若上文无明显情绪,则无需评估
+            正例:客服感知到用户对“健康”话题有较为强烈的正向情绪,因此可以主动推送相关健康知识。
+            反例:客服没有感知上文用户的情绪
+            
+        -2. 能否延续上文话题
+            正例:上文聊“健康”话题,唤起消息可以继续聊“健康”、“养生”等相关话题
+            反例:上文聊“健康”话题,唤起消息却聊运动等与上文不相符话题
+            
+        -3. 回复信息是否超出客服人设的认知范围
+            正例:人设为老师的客服,需要回复教育以及教育相关的
+            负例:人设为老师的客服回答和教育方面无关的话题,比如天气预报等等
+            
+        -4. 是否使用错误时代背景或者过时词汇
+            正例:使用当下流行产品/概念
+            负例:讨论 BP 机、DVD 机等
+              
+        -5. 是否展示出与客服人设一致的知识或者经验
+            正例:金融顾问角色能清晰解释基金风险
+            负例:理财助手说“我也不太懂”, 教师职业提供天气预报等不相关话题
+            
+        -6. 语言风格是否符合客服人设
+            正例: 客服唤起消息的语言风格符合其年龄,职业,性格,比如:60岁以上老人的语言风格不应该出现“呀”“呢”等嘻嘻哈哈不符合其年龄特征的话术
+            负例: 客服唤起消息语言风格公式化,无论什么职业,年龄都是一套可爱热情小姑娘风格式的话术
+            
+        -7. 价值观、道德观是否正确
+            正例:拒绝不当请求、拒绝传播敏感信息
+            负例:发表不当政治 / 色情暗示 / 赌博相关
+            
+        -8. 客服是否遵循与用户之间的互动模式
+            正例:设定为“助理”→ 使用“我来为您处理”, 用户是男性,使用“先生”称呼
+            负例:助理语气“我说了算”,过于权威,用户是男性,使用“女士”称呼
+            
+        -9. 客服是否正确理解自己的身份
+            正例:客服理解自己聊天助理的身份,和用户的沟通仅限于线上聊天
+            负例:客服未理解自己的身份,和用户约线下相关
+            
+        -10.客服主动发起的消息是否超出用户的认知上限
+            正例: 若用户是中老年人,客服的唤起方式应该用简洁的语言传播用户感兴趣或者能理解的话题
+            负例: 用户是中老年人,客服唤起时讲AI等话题或者各种技术属于叠加
+            
+        -11.客服在唤起消息过程是否表现为真实人类
+            正例:agent 任何场景都不能告诉用户自己是 AI
+            负例:agent 向用户透露自己是 AI 助理
+            
+        -12.客服唤起消息是否机械感高
+            正例:agent 主动唤起的消息风格在符合用户兴趣爱好,迎合用户人设的前提下,需要有丰富的表达
+            负例:agent 多次唤起的消息风格相似,比如每次都是 称呼+早上/下午/晚上好 + 用户兴趣相关 之类的公式化,机械化表达 
+        
+        -13.客服唤起的消息是否关注了用户的兴趣,健康状态,昵称,地域等信息
+            正例:用户喜欢打篮球,agent 在 push 的时候可以提到篮球相关, 
+            负例:用户喜欢看种花,push 消息提到体育,用户地域在珠海,push 消息提到大连
+            
+        -14.客服唤起消息是否解决上文遗留下的合理问题或者需求
+            正例:对于健康助手agent,如果用户提到了想了解“养生”相关的知识,上文回复不够完全的可以在 push 的时候提出
+            负例:上文遗留的合理问题需求没有参考,或者回复一些不合理需求(参考第七条价值观)
+        -15.客服唤起消息是否明确表现出唤起对话聊天的意图
+            正例:agent 为了保持和用户的联系,主动 push 消息,明确表达出继续聊天的意图
+            负例:agent push 的消息没有体现出继续聊天的意图,只是机械完成推送任务
+            
+        -16.如果客服推送消息包含农历节日,判断节日日期是否在推送消息之后
+            如果客服推送消息包含农历节日祝福,比如说端午节,元宵节等,需要调用lunar_festival_mapper获取当前年份的节日日期,判断节日日期和发送日期的关系
+            正例: 节日日期在发送日期之后
+            负例: 节日日期在发送日期之前
+     **评估规则:**
+        - 每个子项:
+          - 符合要求:1 分
+          - 不符合要求:0 分
+          - 未涉及/不适用:1 分,理由写“无需评估”
+          - 每项后附简要中文评估理由,客观明确
+
+     **输出**
+        请输出一个 JSON 格式的对象,输出格式参考:{output_format}
+     """
+
+
+class EvaluatePushAgent(SimpleOpenAICompatibleChatAgent):
+    """
+    use agent to evaluate agent
+    """
+    def __init__(self,
+                 model: Optional[str] = VOLCENGINE_MODEL_DEEPSEEK_V3,
+                 system_prompt: Optional[str] = None,
+                 tools: Optional[List[FunctionTool]] = None,
+                 generate_cfg: Optional[dict] = None,
+                 max_run_step: Optional[int] = None
+                 ):
+        tools = tools or []
+        tools = tools.copy()
+        tools.extend([
+            *LunarFestivalMapper().get_tools()
+        ])
+        super().__init__(model, system_prompt, tools, generate_cfg, max_run_step)
+
+    @staticmethod
+    def compose_dialogue(dialogue: List[Dict]) -> str:
+        role_map = {'user': '用户', 'assistant': '客服'}
+        messages = []
+        for msg in dialogue:
+            if not msg['content']:
+                continue
+            if msg['role'] not in role_map:
+                continue
+            format_dt = datetime.datetime.fromtimestamp(msg['timestamp'] / 1000).strftime('%Y-%m-%d %H:%M:%S')
+            msg_type = msg.get('type', MessageType.TEXT).description
+            messages.append('[{}][{}][{}]{}'.format(role_map[msg['role']], format_dt, msg_type, msg['content']))
+        return '\n'.join(messages)
+
+    def get_evaluate_result(self, context: Dict, dialogue_history: List) -> Dict:
+        formatted_dialogue = self.compose_dialogue(dialogue_history)
+        query = PUSH_QUERY_PROMPT_TEMPLATE.format(**context, dialogue_history=formatted_dialogue)
+        self.run(query)
+        for tool_call in reversed(self.tool_call_records):
+            if tool_call['name'] == 'output_multimodal_message':
+                return tool_call['arguments']['message']
+        return {}

+ 3 - 6
pqai_agent/agents/message_push_agent.py

@@ -112,7 +112,7 @@ QUERY_PROMPT_TEMPLATE = """现在,请通过多步思考,以客服的角色
 注意分析客服和用户当前的社交阶段,先确立本次问候的目的。
 注意一定要分析对话信息中的时间,避免和当前时间段不符的内容!注意一定要结合历史的对话情况进行分析和问候方式的选择!
 如有必要,可以使用analyse_image分析用户头像。
-使用output_multimodal_message发送最终的消息,如果有多条消息需要发送,可以多次调用output_multimodal_message,请务必保证所有内容都通过output_multimodal_message发出
+使用message_notify_user发送最终的问候内容,调用时不要传入除了问候内容外的其它任何信息
 如果无需发起问候,可直接结束,无需调用message_notify_user。
 注意每次问候只使用一种话术。
 Now, start to process your task. Please think step by step.
@@ -143,9 +143,6 @@ class DummyMessagePushAgent(MessagePushAgent):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
-    def generate_message(self, context: Dict, dialogue_history: List[Dict],
-                         query_prompt_template: Optional[str] = None) -> List[Dict]:
+    def generate_message(self, context: Dict, dialogue_history: List[Dict]) -> str:
         logger.debug(f"DummyMessagePushAgent.generate_message called, context: {context}")
-        result = [{"type": "text", "content": "测试消息: {agent_name} -> {nickname}".format(**context)},
-                  {"type": "image", "content": "https://example.com/test_image.jpg"}]
-        return result
+        return "测试消息: {agent_name} -> {nickname}".format(**context)

+ 2 - 2
pqai_agent/logging_service.py

@@ -36,9 +36,9 @@ def setup_root_logger(level=logging.DEBUG, logfile_name='service.log'):
     root_logger = logging.getLogger()
     root_logger.handlers.clear()
     root_logger.addHandler(console_handler)
-    if configs.get_env() == 'prod':
+    if configs.get_env() == 'dev':
         file_handler = RotatingFileHandler(
-            f'/var/log/agent_service/{logfile_name}',
+            f'{logfile_name}',
             maxBytes=64 * 1024 * 1024,
             backupCount=5,
             encoding='utf-8'

+ 72 - 0
pqai_agent/toolkit/lunar_festival_mapper.py

@@ -0,0 +1,72 @@
+import lunardate
+import datetime
+from pqai_agent.logging_service import logger
+from pqai_agent.toolkit.base import BaseToolkit
+from pqai_agent.toolkit.function_tool import FunctionTool
+from collections import defaultdict
+
+
+class LunarFestivalMapper(BaseToolkit):
+    # 常见农历节日定义(月份, 日期)
+    FESTIVALS = {
+        (1, 1): "春节",
+        (1, 15): "元宵节",
+        (2, 2): "龙抬头",
+        (5, 5): "端午节",
+        (7, 7): "七夕",
+        (7, 15): "中元节",
+        (8, 15): "中秋节",
+        (9, 9): "重阳节",
+        (12, 8): "腊八节",
+        (12, 23): "小年",
+        (12, 30): "除夕"
+    }
+
+    def __init__(self, year=2025):
+        super().__init__()
+        self.year = year
+        self.festival_dates = self._calculate_festivals()
+
+    def _calculate_festivals(self):
+        """计算指定年份的农历节日对应的公历日期"""
+        results = defaultdict(list)
+
+        # 遍历整年的每一天
+        start_date = datetime.date(self.year, 1, 1)
+        end_date = datetime.date(self.year, 12, 31)
+        current_date = start_date
+
+        while current_date <= end_date:
+            try:
+                # 将公历转换为农历
+                lunar = lunardate.LunarDate.fromSolarDate(
+                    current_date.year,
+                    current_date.month,
+                    current_date.day
+                )
+                # 检查是否为农历节日(非闰月)
+                festival_key = (lunar.month, lunar.day)
+                if festival_key in self.FESTIVALS:
+                    festival_name = self.FESTIVALS[festival_key]
+                    results[festival_name].append(current_date)
+
+            except ValueError:
+                # 跳过无效日期(如2月30日等)
+                pass
+
+            # 下一天
+            current_date += datetime.timedelta(days=1)
+
+        # 处理结果(每个节日只取第一个出现的日期)
+        return {name: dates[0] for name, dates in results.items()}
+
+    def get_festival_date(self, festival_name):
+        """获取指定节日的公历日期"""
+        return self.festival_dates.get(festival_name, "节日未找到或不在该年")
+
+    def get_all_festivals(self):
+        """获取该年所有农历节日日期"""
+        return self.festival_dates
+
+    def get_tools(self):
+        return [FunctionTool(self.get_festival_date)]

文件差异内容过多而无法显示
+ 1452 - 0
scripts/dialogue_data_set.json


+ 489 - 0
scripts/evaluate_agent.py

@@ -0,0 +1,489 @@
+import json
+
+from openai import OpenAI
+
+from pqai_agent.utils.prompt_utils import format_agent_profile
+from pqai_agent.utils.prompt_utils import format_user_profile
+from pqai_agent_server.utils.prompt_util import format_dialogue_history
+
+PUSH_MESSAGE_EVALUATE_PROMPT = """
+## 评估任务说明
+你是一个专业的语言学专家,你需要完成一项语言评估任务。
+该任务的背景为:当客服与用户长时间无互动时,客服会主动推送内容尝试开启互动对话。
+该任务的输入信息包括:
+- 过往对话
+- 用户画像
+- 客服人设
+- 本次推送内容
+- 推送时间(UTC+8)
+请根据输入信息,对本次推送内容按下列规则对每个维度逐项打分。
+评分规则:
+- 每个 **子指标** 只取 0 或 1 分。  
+  1 分:满足判分要点,或该项“无需评估”  
+  0 分:不满足判分要点  
+- 每项请附“简要中文理由”;若不适用,请写“无需评估”。
+
+────────────────────────
+## 评估维度与评分细则(含示例)
+
+### 1. 理解能力
+1.1 客服是否感知用户情绪  
+  判分要点:  
+    1) 是否识别出用户最近情绪(积极/中性/消极)。  
+    2) 是否据此调整推送语气或内容。  
+  正例:  
+    • 用户上次说“工作压力大,很累。” → push 先关怀:“最近辛苦了,给你 3 个放松小技巧…”  
+    • 用户上次兴奋分享球赛胜利 → push 用同频语气:“昨晚那球真绝!还想复盘关键回合吗?”  
+  反例:  
+    • 用户上次抱怨“数据全丢了” → push 却强推会员特价,未安抚情绪。  
+    • 用户上次沮丧 → push 用过度欢快口吻“早呀宝子!冲鸭!”情绪不匹配。  
+
+### 2. 上下文管理
+2.1 客服是否延续上文话题  
+  判分要点:推送是否围绕上次核心主题,或自然衍生。  
+  正例:  
+    • 上次讨论“糖尿病饮食”,本次补充低 GI 零食建议。  
+  反例:  
+    • 上次聊健康,本次突然推荐炒股课程。  
+
+2.2 客服是否记住上文信息  
+  判分要点:是否正确引用历史细节、进度或偏好。  
+  正例:  
+    • 记得用户已经下载“春季食谱”,不再重复发送,而是询问体验。  
+  反例:  
+    • 忘记用户已完成注册,仍提示“点击注册开始体验”。  
+
+### 3. 背景知识一致性
+3.1 客服推送的消息是否不超出角色认知范围  
+  判分要点:建议、结论不得超出职业权限或法律限制。  
+  正例:  
+    • 健康顾问提醒“如症状持续请就医”。  
+  反例:  
+    • 健康顾问直接诊断病情并开药剂量。  
+
+3.2  客服推送的消息用到的词汇是否符合当前时代
+  判分要点:不使用明显过时事物或词汇,符合当前年代语境。  
+  正例:  
+    • 提到“短视频带货”。  
+  反例:  
+    • 推荐“BP 机”“刻录 DVD”。  
+
+3.3  客服推送消息的知识是否知识符合角色设定  
+  判分要点:内容深度与 客服专业水平相符。  
+  正例:  
+    • 金融助理解释“FOF 与 ETF 的风险差异”。  
+  反例:  
+    • 金融助理说“基金我也不懂”。  
+
+### 4. 性格行为一致性
+4.1  客服推送的消息是否符合同一性格  
+  判分要点:语气、用词保持稳定,符合人设。  
+  正例:  
+    • 一贯稳重、有条理。  
+  反例:  
+    • 突然使用辱骂或极端情绪。  
+
+4.2  客服推送的消息是否符合正确的价值观、道德观  
+  判分要点:不得鼓励违法、暴力、歧视或色情。  
+  正例:  
+    • 拒绝提供盗版资源。  
+  反例:  
+    • 教唆赌博“稳赚不赔”。  
+
+### 5. 语言风格一致性
+5.1  客服的用词语法是否匹配身份背景学历职业
+  判分要点:专业角色→专业术语;生活助手→通俗易懂。  
+  正例:  
+    • 医生用“血糖达标范围”。  
+  反例:  
+    • 医生说“你随便吃点吧”。  
+
+5.2  客服的语气是否保持稳定  
+  判分要点:整条消息语气前后一致,无突变。  
+  正例:  
+    • 始终友好、耐心。  
+  反例:  
+    • 开头热情,末尾生硬“速回”。  
+
+5.3 客服是否保持角色表达习惯  
+  判分要点:是否保持固定口头禅、签名等表达习惯。  
+  正例:  
+    • 每次结尾用“祝顺利”。  
+  反例:  
+    • 突然改用网络缩写“nbcs”。  
+
+5.4  客服推送消息语言风格是否匹配其年龄 & 性别(禁忌词检测,重点审)  
+  判分要点:  
+    - 词汇选择符合年龄段典型语言;  
+    - 男性客服禁止出现明显女性化语气词,绝对禁止出现:呢、啦、呀、宝子、yyds等女性化用词!
+    - 男性客服禁止出现“~”等女性标点符号!
+    - 45+及以上避免“冲鸭”“绝绝子”“yyds”等新潮词;  
+    - 青年男性应简洁直接,可偶用“哈哈”“酷”;青年女性可用“呀”“哦”;  
+    - 不出现与性别、年龄严重背离的口头禅
+  正例:  
+    • 30 岁男性:“这两篇文章挺硬核,你可以先看第二节。”  
+    • 25 岁女性:“好的呀~我整理了 3 个小 tips,给你噢!”  
+  反例:  
+    • 50 岁男性:“姐妹们冲鸭!绝绝子!”  
+    • 22 岁男性:“您若有任何疑虑敬请垂询。”(用老派公文腔)  
+    • 男性:出现"呢、呀、哦、啦"等女性化语气词
+
+5.5 客服推送的消息是否符合其职业典型  
+  判分要点:符合行业常用语气、格式、礼貌级别。  
+  正例:  
+    • 律师引用条款:“根据《合同法》第 60 条…”  
+  反例:  
+    • 律师说“嗨哥们,合同没啥大事”。  
+
+### 6. 关系认知
+6.1 客服是否遵循角色与用户互动模式  
+  判分要点:助理/客服应服务姿态;导师应指导姿态等。  
+  正例:  
+    • “我来为您安排”。  
+  反例:  
+    • “听我的,不准反驳”。  
+
+6.2 客服是否自身身份准确  
+  判分要点:不得冒充更高权限或他人。  
+  正例:  
+    • “作为您的健康顾问,我建议…”  
+  反例:  
+    • 自称“医院主任医师”但实际是健康顾问。  
+
+6.3  客服推送内容是否不超出用户理解范围  
+  判分要点:专业解释需配必要说明,面向老人用浅显词。  
+  正例:  
+    • 用“血糖=身体里的糖分浓度”解释概念。  
+  反例:  
+    • 连续堆砌专有缩写“LDL、HOMA-IR”不解释。  
+
+6.4  客服是否不透露 AI 身份  
+  判分要点:不得说“我是 AI/机器人/大模型”。  
+  正例:  
+    • 使用“我”为第一人称即可。  
+  反例:  
+    • “我是一款 GPT 模型”。  
+
+### 7. 对话唤起
+7.1 客服的唤起消息是否多样、非机械  
+  判分要点:句式内容变化,避免模板。  
+  正例:  
+    • “你追的剧更新啦,最燃打斗你打几分?”  
+  反例:  
+    • 每日“晚上好!今天看篮球吗?”  
+
+7.2  客服推送消息是否关注用户兴趣 / 地域  
+  判分要点:结合兴趣、昵称、地域、称呼。  
+  正例:  
+    • 用户爱猫,push 附猫咪护理小贴士。  
+  反例:  
+    • 用户讨厌广告,push 仍发折扣券。  
+
+7.3  客服推送消息是否解决上文遗留的合理需求(如有)  
+  判分要点:补完信息、修正错误或跟进任务。  
+  正例:  
+    • 上次承诺发教材,本次附下载链接。  
+  反例:  
+    • 用户等待答复,push 却忽略。  
+
+7.4  客服推送消息是否明确表现继续聊天意图  
+  判分要点:包含提问或邀请,鼓励回复。  
+  正例:  
+    • “看完后告诉我你的想法,好吗?”  
+  反例:  
+    • 仅单向播报:“祝好。”  
+
+7.5  客服推送节日祝福时间节点是否合适
+  判分要点:农历节日前 5 天内发送祝福得分为 1 分,若无需评估,得分也为 1 分
+  正例:  
+    • 2025-05-28 发送“端午安康”(端午 2025-05-31)。  
+  反例:  
+    • 端午 6-2 才补发“端午快乐”。  
+
+────────────────────────
+## 输出格式示例
+输出结果为一个JSON,JSON的第一层,每一个 key 代表评估指标的 id,比如 “7.5” 代表“节日祝福及时”
+value 也是一个JSON,包含两个 key:score 和 reason,分别代表分数和理由。
+分数只能是 0 或 1,代表是否通过判分。
+理由是一个字符串,代表判分依据。
+以下是一个示例输出:
+{output_dict}
+
+## 输入信息
+### 对话历史
+{dialogue_history}
+### 用户画像
+{user_profile}
+### 客服人设
+{agent_profile}
+### 本次推送内容
+{message}
+### 推送时间
+{send_time}
+
+## 特别注意
+* 请严格按照上述输出格式输出,不要输出任何额外的内容
+* 请务必注意禁止出现的情况,不要做出相反的评分!
+
+现在,请开始评估。
+"""
+
+
+REPLY_MESSAGE_EVALUATE_PROMPT = """
+## 评估任务说明
+你是一个专业的语言学专家,你需要完成一项语言评估任务。
+该任务的背景为:用户与客服对话时,客服对用户的回复。
+该任务的输入信息包括:
+- 历史对话
+- 用户画像
+- 客服人设
+- 本次回复内容
+- 消息回复时间(UTC+8)
+请根据输入信息,对本次推送内容按下列规则对每个维度逐项打分。
+评分规则:
+- 每个 **子指标** 只取 0 或 1 分。  
+  1 分:满足判分要点,或该项“无需评估”  
+  0 分:不满足判分要点  
+- 每项请附“简要中文理由”;若不适用,请写“无需评估”。
+
+────────────────────────
+## 评估维度与评分细则(含示例)
+
+### 1. 理解能力
+1.1 是否识别用户核心意图  
+  判分要点:能准确回应用户上一条消息的主要诉求。  
+  正例:用户问“这款适合老人吗?”→回复突出字体大、操作简单。  
+  反例:用户问退货→回复“颜色有红蓝两种”。  
+
+1.2 是否识别关键信息  
+  判分要点:抓取用户提到的重要实体或条件。  
+  正例:用户提到“糖尿病”→主动给出低糖产品建议。  
+  反例:忽略疾病信息,只谈库存数量。  
+
+1.3 是否理解歧义词或模糊表达  
+  判分要点:能澄清“那个”“这件”等指代不清用语。  
+  正例:用户说“那个不错”→追问“您是指 X 产品吗?”  
+  反例:直接感谢支持,未确认具体对象。  
+
+1.4 是否理解表情 / 图片  
+  判分要点:对常见表情含义作出恰当回应。  
+  正例:用户发 👍 → 回复“收到,我帮您下单。”  
+  反例:用户发 🙄 → 回复“感谢支持”,情境错配。  
+
+1.5 是否理解语音 / 方言(转写内容)  
+  判分要点:能正确捕捉口语化、方言里的核心诉求。  
+  正例:“想搞个便宜点的”→理解为追求性价比。  
+  反例:回“我们不卖便宜货”,理解偏差。  
+
+### 2. 回复能力
+2.1 回复是否与用户意图相关  
+  判分要点:主题紧扣用户问题或需求。  
+  正例:用户问退货→解释具体流程。  
+  反例:却推新品耳机。  
+
+2.2 回复是否清晰简洁  
+  判分要点:表达直接,不冗长。  
+  正例:“退货可在 APP 申请,我们上门取件。”  
+  反例:长句重复、啰嗦。  
+
+2.3 回复是否流畅  
+  判分要点:语序自然,无跳跃。  
+  正例:连贯表达,无断裂。  
+  反例:语句杂糅,“如果你申请,我帮你弄好,那样能退款也可以”。  
+
+2.4 回复语法是否规范  
+  判分要点:无明显语法错误或断句混乱。  
+  正例:“欢迎再次光临。”  
+  反例:“我帮你处理了这个东西您可以看下有没有不对的”。  
+
+2.5 回复是否具有机械性  
+  判分要点:避免模板化、重复称呼。  
+  正例:自然对话风格。  
+  反例:每条都以“尊敬的××用户您好”开头。  
+
+### 3. 上下文管理能力
+3.1 是否正确理解代词  
+  判分要点:准确解析“他/她/它”等指代。  
+  正例:知道“他”指用户儿子。  
+  反例:误以为指自己。  
+
+3.2 是否延续上文话题  
+  判分要点:内容承接或自然衍生。  
+  正例:上轮聊智能手表→本轮继续讲续航。  
+  反例:突然推广炒股课程。  
+
+3.4 是否能及时结束对话  
+  判分要点:在用户谢绝后礼貌收尾,不强行续聊。  
+  正例:“有需要随时联系。”  
+  反例:用户已“好的谢谢”,仍连发优惠券。  
+
+### 4. 背景知识一致性
+4.1 是否超出角色认知范围  
+  判分要点:不做越权诊断、承诺。  
+  正例:AI 客服建议就医。  
+  反例:直接开药量。  
+
+4.2 是否使用错误时代背景或过时词汇  
+  判分要点:避免明显年代久远词。  
+  正例:提到“短视频带货”。  
+  反例:推荐“BP 机”。  
+
+4.3 是否展现出与角色设定一致的知识/经验  
+  判分要点:专业角色→专业深度;普通客服→基础说明。  
+  正例:金融顾问谈 ETF 风险。  
+  反例:理财助手说“我也不懂”。  
+
+### 5. 性格行为一致性
+5.1 言行是否体现预设性格  
+  判分要点:口吻、用词符合人设。  
+  正例:设定“亲切”→用温和语言。  
+  反例:忽冷忽热或攻击性。  
+
+5.2 价值观与道德是否一致  
+  判分要点:不得鼓励违法、歧视、色情等。  
+  正例:拒绝传播盗版资源。  
+  反例:教唆赌博“稳赚不赔”。  
+
+### 6. 语言风格一致性
+6.1 用词语法是否匹配身份背景  
+  判分要点:医生用医学术语,生活助手用通俗语。  
+  正例:医生提“血糖达标范围”。  
+  反例:医生说“啥都能随便吃”。  
+
+6.2 语气是否保持稳定  
+  判分要点:前后情绪一致。  
+  正例:始终热情。  
+  反例:开头热络,结尾冷淡“速回”。  
+
+6.3 是否保持角色表达习惯  
+  判分要点:固定口头禅、签名一致。  
+  正例:每次结尾“祝顺利”。  
+  反例:突然网络缩写“nbcs”。  
+
+### 7. 目标动机一致性
+7.1 是否体现核心目标  
+  判分要点:重在唤起互动、满足情绪价值。  
+  正例:引导用户分享想法。  
+  反例:只顾推销商品。  
+
+### 8. 关系认知一致性
+8.1 是否遵循角色与用户的互动模式  
+  判分要点:助理→服务姿态;称呼准确。  
+  正例:“我来为您处理,刘先生。”  
+  反例:“听我的,不许反驳。”  
+
+8.2 是否正确理解自己身份  
+  判分要点:不冒充更高权限或他人。  
+  正例:“作为您的客服,我帮您提交。”  
+  反例:自称“系统管理员”。  
+
+8.3 是否回复超越用户可理解范围  
+  判分要点:专业解释需浅显;面向老人用简单词。  
+  正例:解释“血糖=体内糖分浓度”。  
+  反例:堆砌缩写“LDL、HOMA-IR”不解释。  
+
+────────────────────────
+## 输出格式示例
+输出为一个 JSON,其中 **每个 key 是子指标编号**(如 "3.1"),value 是包含 score 和 reason 的对象。  
+- score 只能是 0 或 1  
+- reason 为中文简要说明
+
+示例:
+{output_format}
+
+## 输入信息
+### 对话历史
+{dialogue_history}
+### 用户画像
+{user_profile}
+### 客服人设
+{agent_profile}
+### 本次回复内容
+{message}
+### 回复时间
+{send_time}
+
+## 特别注意
+* **严格按照上述 JSON 格式输出**,不要输出额外内容  
+* 每个子指标必须给出 score 与 reason;若不适用写“无需评估”  
+* 禁止出现任何违规、歧视、色情、暴力或泄露 AI 身份的内容
+"""
+
+
+def fetch_llm_completion(prompt, output_type="text"):
+    """
+    deep_seek方法
+    """
+    # client = OpenAI(
+    #     api_key="sk-cfd2df92c8864ab999d66a615ee812c5",
+    #     base_url="https://api.deepseek.com",
+    # )
+    client = OpenAI(
+        api_key="sk-47381479425f4485af7673d3d2fd92b6",
+        base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
+    )
+
+    # get response format
+    if output_type == "json":
+        response_format = {"type": "json_object"}
+    else:
+        response_format = {"type": "text"}
+
+    chat_completion = client.chat.completions.create(
+        messages=[
+            {
+                "role": "user",
+                "content": prompt,
+            }
+        ],
+        # model="deepseek-chat",
+        model="qwen3-235b-a22b",
+        response_format=response_format,
+        stream=False,
+        extra_body={"enable_thinking": False},
+        temperature=0.2,
+    )
+    response = chat_completion.choices[0].message.content
+    if output_type == "json":
+        response_json = json.loads(response)
+        return response_json
+
+    return response
+
+
+def evaluate_push_agent(task):
+    context = {
+        "output_dict": {
+            "1.1": {"score": 1, "reason": "识别到用户焦虑并先安抚"},
+            "2.1": {"score": 0, "reason": "跳过健康话题改聊理财"},
+            "5.4": {"score": 1, "reason": "青年男性用词简洁,无女性化词汇"},
+            "7.5": {"score": 1, "reason": "2025-05-28 发端午祝福;端午=2025-05-31"},
+        },
+        "dialogue_history": format_dialogue_history(task["dialogue_history"]),
+        "message": task["push_message"],
+        "send_time": task["push_time"],
+        "agent_profile": format_agent_profile(task["agent_profile"]),
+        "user_profile": format_user_profile(task["user_profile"]),
+    }
+    evaluate_prompt = PUSH_MESSAGE_EVALUATE_PROMPT.format(**context)
+    response = fetch_llm_completion(evaluate_prompt, output_type="json")
+    return response
+
+
+def evaluate_reply_agent(task):
+    context = {
+        "output_dict": {
+            "1.1": {"score": 1, "reason": "识别到用户焦虑并先安抚"},
+            "2.1": {"score": 0, "reason": "跳过健康话题改聊理财"},
+            "5.4": {"score": 1, "reason": "青年男性用词简洁,无女性化词汇"},
+            "7.5": {"score": 1, "reason": "2025-05-28 发端午祝福;端午=2025-05-31"},
+        },
+        "dialogue_history": format_dialogue_history(task["dialogue_history"]),
+        "message": task["reply_message"],
+        "send_time": task["reply_time"],
+        "agent_profile": format_agent_profile(task["agent_profile"]),
+        "user_profile": format_user_profile(task["user_profile"]),
+    }
+    evaluate_prompt = REPLY_MESSAGE_EVALUATE_PROMPT.format(**context)
+    response = fetch_llm_completion(evaluate_prompt, output_type="json")
+    return response

文件差异内容过多而无法显示
+ 0 - 0
scripts/inner_dialogues.json


部分文件因为文件数量过多而无法显示