瀏覽代碼

Merge branch 'master' into feature/dev/20250605-dev

# Conflicts:
#	pqai_agent/logging.py
xueyiming 1 周之前
父節點
當前提交
fa562de09e
共有 66 個文件被更改,包括 3112 次插入310 次删除
  1. 2 2
      pqai_agent/abtest/client.py
  2. 1 1
      pqai_agent/abtest/models.py
  3. 1 3
      pqai_agent/agent.py
  4. 1 1
      pqai_agent/agent_config_manager.py
  5. 40 15
      pqai_agent/agent_service.py
  6. 3 3
      pqai_agent/agents/message_push_agent.py
  7. 3 3
      pqai_agent/agents/message_reply_agent.py
  8. 8 4
      pqai_agent/agents/multimodal_chat_agent.py
  9. 47 6
      pqai_agent/agents/simple_chat_agent.py
  10. 123 32
      pqai_agent/chat_service.py
  11. 22 0
      pqai_agent/clients/hot_topic_client.py
  12. 1 1
      pqai_agent/clients/relation_stage_client.py
  13. 11 6
      pqai_agent/configs/dev.yaml
  14. 13 4
      pqai_agent/configs/prod.yaml
  15. 3 2
      pqai_agent/data_models/agent_configuration.py
  16. 2 2
      pqai_agent/data_models/agent_push_record.py
  17. 22 0
      pqai_agent/data_models/agent_task.py
  18. 23 0
      pqai_agent/data_models/agent_task_detail.py
  19. 20 0
      pqai_agent/data_models/agent_test_task.py
  20. 22 0
      pqai_agent/data_models/agent_test_task_conversations.py
  21. 17 0
      pqai_agent/data_models/dataset_model.py
  22. 18 0
      pqai_agent/data_models/datasets.py
  23. 23 0
      pqai_agent/data_models/internal_conversation_data.py
  24. 22 0
      pqai_agent/data_models/qywx_chat_history.py
  25. 22 0
      pqai_agent/data_models/qywx_employee.py
  26. 2 2
      pqai_agent/data_models/service_module.py
  27. 1 1
      pqai_agent/database.py
  28. 10 4
      pqai_agent/dialogue_manager.py
  29. 1 1
      pqai_agent/history_dialogue_service.py
  30. 9 7
      pqai_agent/logging.py
  31. 2 3
      pqai_agent/message_queue_backend.py
  32. 10 3
      pqai_agent/mq_message.py
  33. 1 0
      pqai_agent/prompt_templates.py
  34. 65 38
      pqai_agent/push_service.py
  35. 1 1
      pqai_agent/rate_limiter.py
  36. 2 2
      pqai_agent/response_type_detector.py
  37. 1 1
      pqai_agent/service_module_manager.py
  38. 3 2
      pqai_agent/toolkit/__init__.py
  39. 1 1
      pqai_agent/toolkit/function_tool.py
  40. 40 0
      pqai_agent/toolkit/hot_topic_toolkit.py
  41. 1 1
      pqai_agent/toolkit/image_describer.py
  42. 1 1
      pqai_agent/toolkit/lark_sheet_record_for_human_intervention.py
  43. 0 54
      pqai_agent/toolkit/message_notifier.py
  44. 58 0
      pqai_agent/toolkit/message_toolkit.py
  45. 44 0
      pqai_agent/toolkit/sub_agent_toolkit.py
  46. 52 4
      pqai_agent/user_manager.py
  47. 3 3
      pqai_agent/user_profile_extractor.py
  48. 8 0
      pqai_agent/utils/__init__.py
  49. 24 0
      pqai_agent/utils/agent_utils.py
  50. 12 3
      pqai_agent/utils/prompt_utils.py
  51. 7 4
      pqai_agent_server/agent_server.py
  52. 227 0
      pqai_agent_server/agent_task_server.py
  53. 418 77
      pqai_agent_server/api_server.py
  54. 115 0
      pqai_agent_server/const/status_enum.py
  55. 46 0
      pqai_agent_server/const/type_enum.py
  56. 160 0
      pqai_agent_server/dataset_service.py
  57. 571 0
      pqai_agent_server/evaluate_agent.py
  58. 527 0
      pqai_agent_server/task_server.py
  59. 0 1
      pqai_agent_server/utils/__init__.py
  60. 14 3
      pqai_agent_server/utils/common.py
  61. 86 0
      pqai_agent_server/utils/odps_utils.py
  62. 3 4
      pqai_agent_server/utils/prompt_util.py
  63. 3 1
      requirements.txt
  64. 43 0
      scripts/extract_push_action_logs.py
  65. 30 0
      scripts/mq_consumer.py
  66. 40 3
      tests/unit_test.py

+ 2 - 2
pqai_agent/abtest/client.py

@@ -6,7 +6,7 @@ from pqai_agent.abtest.models import Project, Domain, Layer, Experiment, Experim
     ExperimentContext, ExperimentResult
 from alibabacloud_paiabtest20240119.models import ListProjectsRequest, ListProjectsResponseBodyProjects, \
     ListDomainsRequest, ListFeaturesRequest, ListLayersRequest, ListExperimentsRequest, ListExperimentVersionsRequest
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 
 class ExperimentClient:
     def __init__(self, client: Client):
@@ -267,7 +267,7 @@ def get_client():
     return g_client
 
 if __name__ == '__main__':
-    from pqai_agent.logging_service import setup_root_logger
+    from pqai_agent.logging import setup_root_logger
     setup_root_logger(level='DEBUG')
     experiment_client = get_client()
 

+ 1 - 1
pqai_agent/abtest/models.py

@@ -3,7 +3,7 @@ import json
 from dataclasses import dataclass, field
 import hashlib
 
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 
 
 class FNV:

+ 1 - 3
pqai_agent/agent.py

@@ -7,13 +7,11 @@ class BaseAgent(ABC):
     r"""An abstract base class for all agents."""
 
     @abstractmethod
-    def run(self, user_input: str, **kwargs) -> Any:
+    def run(self, user_input: str) -> Any:
         """Run the agent with the given user input.
 
         Args:
             user_input (str): The input from the user.
-            **kwargs: Additional keyword arguments.
-
         Returns:
             Any: The output from the agent.
         """

+ 1 - 1
pqai_agent/agent_config_manager.py

@@ -1,7 +1,7 @@
 from typing import Dict, Optional
 
 from pqai_agent.data_models.agent_configuration import AgentConfiguration
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 
 class AgentConfigManager:
     def __init__(self, session_maker):

+ 40 - 15
pqai_agent/agent_service.py

@@ -1,7 +1,7 @@
 #! /usr/bin/env python
 # -*- coding: utf-8 -*-
 # vim:fenc=utf-8
-
+import json
 import re
 import signal
 import sys
@@ -15,15 +15,16 @@ import traceback
 import apscheduler.triggers.cron
 import rocketmq
 from apscheduler.schedulers.background import BackgroundScheduler
+from rocketmq import FilterExpression
 from sqlalchemy.orm import sessionmaker
 
-from pqai_agent import configs
+from pqai_agent import configs, push_service
 from pqai_agent.abtest.utils import get_abtest_info
 from pqai_agent.agent_config_manager import AgentConfigManager
 from pqai_agent.agents.message_reply_agent import MessageReplyAgent
 from pqai_agent.configs import apollo_config
 from pqai_agent.exceptions import NoRetryException
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 from pqai_agent import chat_service
 from pqai_agent.chat_service import CozeChat, ChatServiceType
 from pqai_agent.dialogue_manager import DialogueManager, DialogueState, DialogueStateCache
@@ -96,7 +97,8 @@ class AgentService:
 
         # Push相关
         self.push_task_producer = None
-        self.push_task_consumer = None
+        self.push_generate_task_consumer = None
+        self.push_send_task_consumer = None
         self._init_push_task_queue()
         self.next_push_disabled = True
         self._resume_unfinished_push_task()
@@ -344,7 +346,7 @@ class AgentService:
             logger.debug(f"staff[{staff_id}], user[{user_id}]: no messages to send")
 
     def can_send_to_user(self, staff_id, user_id) -> bool:
-        user_tags = self.user_relation_manager.get_user_tags(user_id)
+        user_tags = self.user_manager.get_user_tags([user_id]).get(user_id, [])
         white_list_tags = set(apollo_config.get_json_value("agent_response_whitelist_tags", []))
         hit_white_list_tags = len(set(user_tags).intersection(white_list_tags)) > 0
         staff_white_lists = set(apollo_config.get_json_value("agent_response_whitelist_staffs", []))
@@ -356,16 +358,22 @@ class AgentService:
     def send_multimodal_response(self, staff_id, user_id, response: Dict, skip_check=False):
         message_type = response["type"]
         logger.warning(f"staff[{staff_id}] user[{user_id}]: response[{message_type}] {response}")
-        if message_type not in (MessageType.TEXT, MessageType.IMAGE_QW, MessageType.VOICE):
+        if message_type not in (MessageType.TEXT, MessageType.IMAGE_QW, MessageType.VOICE,
+                                MessageType.LINK, MessageType.MINI_PROGRAM):
             logger.error(f"staff[{staff_id}] user[{user_id}]: unsupported message type {message_type}")
             return
         if not skip_check and not self.can_send_to_user(staff_id, user_id):
             return
         current_ts = int(time.time() * 1000)
         self.send_rate_limiter.wait_for_sending(staff_id, response)
+        # FIXME: 小程序相关的字段
         self.send_queue.produce(
-            MqMessage.build(message_type, MessageChannel.CORP_WECHAT,
-                            staff_id, user_id, response["content"], current_ts)
+            MqMessage(type=message_type, channel=MessageChannel.CORP_WECHAT,
+                      sender=staff_id, receiver=user_id, content=response["content"], sendTime=current_ts,
+                      desc=response.get("desc"), title=response.get("title"),
+                      appIconUrl=None, pagePath=response.get("content"),
+                      coverImage=response.get("cover_url"), appOrgId=None,
+                      appId=None)
         )
 
     def _route_to_human_intervention(self, user_id: str, origin_message: MqMessage):
@@ -384,20 +392,31 @@ class AgentService:
         mq_conf = configs.get()['mq']
         rmq_client_conf = rocketmq.ClientConfiguration(mq_conf['endpoints'], credentials, mq_conf['instance_id'])
         rmq_topic = mq_conf['push_tasks_topic']
-        rmq_group = mq_conf['push_tasks_group']
+        rmq_group_generate = mq_conf['push_generate_task_group']
+        rmq_group_send = mq_conf['push_send_task_group']
         self.push_task_rmq_topic = rmq_topic
         self.push_task_producer = rocketmq.Producer(rmq_client_conf, (rmq_topic,))
         self.push_task_producer.startup()
-        self.push_task_consumer = rocketmq.SimpleConsumer(rmq_client_conf, rmq_group, await_duration=5)
-        self.push_task_consumer.startup()
-        self.push_task_consumer.subscribe(rmq_topic)
+        # FIXME: 不应该暴露到agent service中
+        self.push_generate_task_consumer = rocketmq.SimpleConsumer(rmq_client_conf, rmq_group_generate, await_duration=5)
+        self.push_generate_task_consumer.startup()
+        self.push_generate_task_consumer.subscribe(
+            rmq_topic, filter_expression=FilterExpression(push_service.TaskType.GENERATE.value)
+        )
+        self.push_send_task_consumer = rocketmq.SimpleConsumer(rmq_client_conf, rmq_group_send, await_duration=5)
+        self.push_send_task_consumer.startup()
+        self.push_send_task_consumer.subscribe(
+            rmq_topic, filter_expression=FilterExpression(push_service.TaskType.SEND.value)
+        )
 
 
     def _resume_unfinished_push_task(self):
         def run_unfinished_push_task():
             logger.info("start to resume unfinished push task")
             push_task_worker_pool = PushTaskWorkerPool(
-                self, self.push_task_rmq_topic, self.push_task_consumer, self.push_task_producer)
+                self, self.push_task_rmq_topic, self.push_generate_task_consumer,
+                self.push_send_task_consumer, self.push_task_producer
+            )
             push_task_worker_pool.start()
             push_task_worker_pool.wait_to_finish()
             self.next_push_disabled = False
@@ -427,7 +446,8 @@ class AgentService:
             push_scan_threads.append(scan_thread)
 
         push_task_worker_pool = PushTaskWorkerPool(
-            self, self.push_task_rmq_topic, self.push_task_consumer, self.push_task_producer)
+            self, self.push_task_rmq_topic,
+            self.push_generate_task_consumer, self.push_send_task_consumer, self.push_task_producer)
         push_task_worker_pool.start()
         for thread in push_scan_threads:
             thread.join()
@@ -461,9 +481,14 @@ class AgentService:
         agent_config = get_agent_abtest_config('chat', main_agent.user_id,
                                                self.service_module_manager, self.agent_config_manager)
         if agent_config:
+            try:
+                tool_names = json.loads(agent_config.tools)
+            except json.JSONDecodeError:
+                logger.error(f"Invalid JSON in agent tools: {agent_config.tools}")
+                tool_names = []
             chat_agent = MessageReplyAgent(model=agent_config.execution_model,
                                            system_prompt=agent_config.system_prompt,
-                                           tools=get_tools(agent_config.tools))
+                                           tools=get_tools(tool_names))
         else:
             chat_agent = MessageReplyAgent()
         chat_responses = chat_agent.generate_message(

+ 3 - 3
pqai_agent/agents/message_push_agent.py

@@ -2,10 +2,10 @@ from typing import Optional, List, Dict
 
 from pqai_agent.agents.multimodal_chat_agent import MultiModalChatAgent
 from pqai_agent.chat_service import VOLCENGINE_MODEL_DEEPSEEK_V3
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 from pqai_agent.toolkit.function_tool import FunctionTool
 from pqai_agent.toolkit.image_describer import ImageDescriber
-from pqai_agent.toolkit.message_notifier import MessageNotifier
+from pqai_agent.toolkit.message_toolkit import MessageToolkit
 
 DEFAULT_SYSTEM_PROMPT = '''
 <基本设定>
@@ -128,7 +128,7 @@ class MessagePushAgent(MultiModalChatAgent):
         if tools is None:
             tools = [
                 *ImageDescriber().get_tools(),
-                *MessageNotifier().get_tools()
+                *MessageToolkit().get_tools()
             ]
         super().__init__(model, system_prompt, tools, generate_cfg, max_run_step)
 

+ 3 - 3
pqai_agent/agents/message_reply_agent.py

@@ -2,10 +2,10 @@ from typing import Optional, List, Dict
 
 from pqai_agent.agents.multimodal_chat_agent import MultiModalChatAgent
 from pqai_agent.chat_service import VOLCENGINE_MODEL_DEEPSEEK_V3
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 from pqai_agent.toolkit.function_tool import FunctionTool
 from pqai_agent.toolkit.image_describer import ImageDescriber
-from pqai_agent.toolkit.message_notifier import MessageNotifier
+from pqai_agent.toolkit.message_toolkit import MessageToolkit
 
 DEFAULT_SYSTEM_PROMPT = '''
 <基本设定>
@@ -94,7 +94,7 @@ class MessageReplyAgent(MultiModalChatAgent):
         if tools is None:
             tools = [
                 *ImageDescriber().get_tools(),
-                *MessageNotifier().get_tools()
+                *MessageToolkit().get_tools()
             ]
         super().__init__(model, system_prompt, tools, generate_cfg, max_run_step)
 

+ 8 - 4
pqai_agent/agents/multimodal_chat_agent.py

@@ -2,12 +2,13 @@ import datetime
 from abc import abstractmethod
 from typing import Optional, List, Dict
 
+from pqai_agent import configs
 from pqai_agent.agents.simple_chat_agent import SimpleOpenAICompatibleChatAgent
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 from pqai_agent.mq_message import MessageType
 from pqai_agent.toolkit import get_tool
 from pqai_agent.toolkit.function_tool import FunctionTool
-from pqai_agent.toolkit.message_notifier import MessageNotifier
+from pqai_agent.toolkit.message_toolkit import MessageToolkit
 
 
 class MultiModalChatAgent(SimpleOpenAICompatibleChatAgent):
@@ -28,13 +29,16 @@ class MultiModalChatAgent(SimpleOpenAICompatibleChatAgent):
         pass
 
     def _generate_message(self, context: Dict, dialogue_history: List[Dict],
-                         query_prompt_template: str) -> List[Dict]:
+                          query_prompt_template: str) -> List[Dict]:
+        if configs.get().get('debug_flags', {}).get('disable_llm_api_call', False):
+            return [{'type': 'text', 'content': '测试消息 -> {nickname}'.format(**context)}]
+
         formatted_dialogue = MultiModalChatAgent.compose_dialogue(dialogue_history)
         query = query_prompt_template.format(**context, dialogue_history=formatted_dialogue)
         self.run(query)
         result = []
         for tool_call in self.tool_call_records:
-            if tool_call['name'] == MessageNotifier.output_multimodal_message.__name__:
+            if tool_call['name'] == MessageToolkit.output_multimodal_message.__name__:
                 result.append(tool_call['arguments']['message'])
         return result
 

+ 47 - 6
pqai_agent/agents/simple_chat_agent.py

@@ -1,11 +1,13 @@
 import json
 from typing import List, Optional
 
+import pqai_agent.utils
 from pqai_agent.agent import DEFAULT_MAX_RUN_STEPS
 from pqai_agent.chat_service import OpenAICompatible
-from pqai_agent.logging_service import logger
+from pqai_agent.data_models.agent_task_detail import AgentTaskDetail
+from pqai_agent.logging import logger
 from pqai_agent.toolkit.function_tool import FunctionTool
-
+from pqai_agent_server.const.status_enum import AgentTaskDetailStatus
 
 
 class SimpleOpenAICompatibleChatAgent:
@@ -23,6 +25,10 @@ class SimpleOpenAICompatibleChatAgent:
         self.generate_cfg = generate_cfg or {}
         self.max_run_step = max_run_step or DEFAULT_MAX_RUN_STEPS
         self.tool_call_records = []
+        self.agent_task_details: list[AgentTaskDetail] = []
+        self.total_input_tokens = 0
+        self.total_output_tokens = 0
+        logger.debug(self.tool_map)
 
     def add_tool(self, tool: FunctionTool):
         """添加一个工具到Agent中"""
@@ -32,23 +38,32 @@ class SimpleOpenAICompatibleChatAgent:
         self.tool_map[tool.name] = tool
 
     def run(self, user_input: str) -> str:
+        run_id = pqai_agent.utils.random_str()[:12]
         messages = [{"role": "system", "content": self.system_prompt}]
         tools = [tool.get_openai_tool_schema() for tool in self.tools]
         messages.append({"role": "user", "content": user_input})
 
         n_steps = 0
-        logger.debug(f"start agent loop. messages: {messages}")
+        logger.debug(f"run_id[{run_id}] start agent loop. messages: {messages}")
         while n_steps < self.max_run_step:
             response = self.llm_client.chat.completions.create(model=self.model, messages=messages, tools=tools, **self.generate_cfg)
             message = response.choices[0].message
+            self.total_input_tokens += response.usage.prompt_tokens
+            self.total_output_tokens += response.usage.completion_tokens
             messages.append(message)
-            logger.debug(f"current step content: {message.content}")
+            logger.debug(f"run_id[{run_id}] current step content: {message.content}")
 
             if message.tool_calls:
                 for tool_call in message.tool_calls:
                     function_name = tool_call.function.name
                     arguments = json.loads(tool_call.function.arguments)
-                    logger.debug(f"call function[{function_name}], parameter: {arguments}")
+                    logger.debug(f"run_id[{run_id}] call function[{function_name}], parameter: {arguments}")
+
+                    agent_task_detail = AgentTaskDetail()
+                    agent_task_detail.executor_type = 'tool'
+                    agent_task_detail.executor_name = function_name
+                    agent_task_detail.input_data = tool_call.function.arguments
+                    self.agent_task_details.append(agent_task_detail)
 
                     if function_name in self.tool_map:
                         result = self.tool_map[function_name](**arguments)
@@ -62,11 +77,37 @@ class SimpleOpenAICompatibleChatAgent:
                             "arguments": arguments,
                             "result": result
                         })
+                        agent_task_detail.output_data = json.dumps(result, ensure_ascii=False)
+                        agent_task_detail.status = AgentTaskDetailStatus.SUCCESS.value
                     else:
-                        logger.error(f"Function {function_name} not found in tool map.")
+                        agent_task_detail.error_message = f"Function {function_name} not found in tool map."
+                        agent_task_detail.status = AgentTaskDetailStatus.FAILED.value
+                        logger.error(f"run_id[{run_id}] Function {function_name} not found in tool map.")
                         raise Exception(f"Function {function_name} not found in tool map.")
             else:
+                agent_task_detail = AgentTaskDetail()
+                agent_task_detail.executor_type = 'llm'
+                agent_task_detail.executor_name = self.model
+                agent_task_detail.output_data = message.content
+                agent_task_detail.status = AgentTaskDetailStatus.SUCCESS.value
+                self.agent_task_details.append(agent_task_detail)
                 return message.content
             n_steps += 1
 
         raise Exception("Max run steps exceeded")
+
+    # 新增方法:获取步骤记录
+    def get_agent_task_details(self) -> list:
+        """返回代理运行过程中的详细步骤记录"""
+        return self.agent_task_details
+
+    def get_total_input_tokens(self) -> int:
+        """获取总输入token数"""
+        return self.total_input_tokens
+
+    def get_total_output_tokens(self) -> int:
+        """获取总输出token数"""
+        return self.total_output_tokens
+
+    def get_total_cost(self) -> float:
+        return OpenAICompatible.calculate_cost(self.model, self.total_input_tokens, self.total_output_tokens)

+ 123 - 32
pqai_agent/chat_service.py

@@ -11,7 +11,7 @@ from enum import Enum, auto
 import httpx
 
 from pqai_agent import configs
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 import cozepy
 from cozepy import Coze, TokenAuth, Message, ChatStatus, MessageType, JWTOAuthApp, JWTAuth
 import time
@@ -22,9 +22,9 @@ COZE_CN_BASE_URL = 'https://api.coze.cn'
 VOLCENGINE_API_TOKEN = '5e275c38-44fd-415f-abcf-4b59f6377f72'
 VOLCENGINE_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"
 VOLCENGINE_MODEL_DEEPSEEK_V3 = "deepseek-v3-250324"
-VOLCENGINE_MODEL_DOUBAO_PRO_1_5 = 'ep-20250307150409-4blz9'
-VOLCENGINE_MODEL_DOUBAO_PRO_32K = 'ep-20250414202859-6nkz5'
-VOLCENGINE_MODEL_DOUBAO_1_5_VISION_PRO = 'ep-20250421193334-nz5wd'
+VOLCENGINE_MODEL_DOUBAO_PRO_1_5_32K = 'doubao-1-5-pro-32k-250115'
+VOLCENGINE_MODEL_DOUBAO_PRO_32K = 'doubao-pro-32k-241215'
+VOLCENGINE_MODEL_DOUBAO_1_5_VISION_PRO = 'doubao-1-5-vision-pro-32k-250115'
 DEEPSEEK_API_TOKEN = 'sk-67daad8f424f4854bda7f1fed7ef220b'
 DEEPSEEK_BASE_URL = 'https://api.deepseek.com/'
 DEEPSEEK_CHAT_MODEL = 'deepseek-chat'
@@ -34,52 +34,143 @@ OPENAI_API_TOKEN = 'sk-proj-6LsybsZSinbMIUzqttDt8LxmNbi-i6lEq-AUMzBhCr3jS8sme9AG
 OPENAI_BASE_URL = 'https://api.openai.com/v1'
 OPENAI_MODEL_GPT_4o = 'gpt-4o'
 OPENAI_MODEL_GPT_4o_mini = 'gpt-4o-mini'
-OPENROUTER_API_TOKEN = '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'
+OPENROUTER_API_TOKEN = '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'
 OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1/'
 OPENROUTER_MODEL_CLAUDE_3_7_SONNET = 'anthropic/claude-3.7-sonnet'
+OPENROUTER_MODEL_GEMINI_2_5_PRO = 'google/gemini-2.5-pro'
+ALIYUN_API_TOKEN = 'sk-47381479425f4485af7673d3d2fd92b6'
+ALIYUN_BASE_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
+
 
 class ChatServiceType(Enum):
     OPENAI_COMPATIBLE = auto()
     COZE_CHAT = auto()
 
+class ModelPrice:
+    EXCHANGE_RATE_TO_CNY = {
+        "USD": 7.2,  # Example conversion rate, adjust as needed
+    }
+
+    def __init__(self, input_price: float, output_price: float, currency: str = 'CNY'):
+        """
+        :param input_price: input price for per million tokens
+        :param output_price: output price for per million tokens
+        """
+        self.input_price = input_price
+        self.output_price = output_price
+        self.currency = currency
+
+    def get_total_cost(self, input_tokens: int, output_tokens: int, convert_to_cny: bool = True) -> float:
+        """
+        Calculate the total cost based on input and output tokens.
+        :param input_tokens: Number of input tokens
+        :param output_tokens: Number of output tokens
+        :param convert_to_cny: Whether to convert the cost to CNY (default is True)
+        :return: Total cost in the specified currency
+        """
+        total_cost = (self.input_price * input_tokens / 1_000_000) + (self.output_price * output_tokens / 1_000_000)
+        if convert_to_cny and self.currency != 'CNY':
+            conversion_rate = self.EXCHANGE_RATE_TO_CNY.get(self.currency, 1.0)
+            total_cost *= conversion_rate
+        return total_cost
+
+    def get_cny_brief(self) -> str:
+        input_price = self.input_price * self.EXCHANGE_RATE_TO_CNY.get(self.currency, 1.0)
+        output_price = self.output_price * self.EXCHANGE_RATE_TO_CNY.get(self.currency, 1.0)
+        return f"{input_price:.0f}/{output_price:.0f}"
+
+    def __repr__(self):
+        return f"ModelPrice(input_price={self.input_price}, output_price={self.output_price}, currency={self.currency})"
+
 class OpenAICompatible:
+    volcengine_models = [
+        VOLCENGINE_MODEL_DOUBAO_PRO_32K,
+        VOLCENGINE_MODEL_DOUBAO_PRO_1_5_32K,
+        VOLCENGINE_MODEL_DOUBAO_1_5_VISION_PRO,
+        VOLCENGINE_MODEL_DEEPSEEK_V3
+    ]
+    deepseek_models = [
+        DEEPSEEK_CHAT_MODEL,
+    ]
+    openai_models = [
+        OPENAI_MODEL_GPT_4o_mini,
+        OPENAI_MODEL_GPT_4o
+    ]
+    openrouter_models = [
+        OPENROUTER_MODEL_CLAUDE_3_7_SONNET,
+        OPENROUTER_MODEL_GEMINI_2_5_PRO
+    ]
+
+    model_prices = {
+        VOLCENGINE_MODEL_DEEPSEEK_V3: ModelPrice(input_price=2, output_price=8),
+        VOLCENGINE_MODEL_DOUBAO_PRO_32K: ModelPrice(input_price=0.8, output_price=2),
+        VOLCENGINE_MODEL_DOUBAO_PRO_1_5_32K: ModelPrice(input_price=0.8, output_price=2),
+        VOLCENGINE_MODEL_DOUBAO_1_5_VISION_PRO: ModelPrice(input_price=3, output_price=9),
+        DEEPSEEK_CHAT_MODEL: ModelPrice(input_price=2, output_price=8),
+        OPENAI_MODEL_GPT_4o: ModelPrice(input_price=2.5, output_price=10, currency='USD'),
+        OPENAI_MODEL_GPT_4o_mini: ModelPrice(input_price=0.15, output_price=0.6, currency='USD'),
+        OPENROUTER_MODEL_CLAUDE_3_7_SONNET: ModelPrice(input_price=3, output_price=15, currency='USD'),
+        OPENROUTER_MODEL_GEMINI_2_5_PRO: ModelPrice(input_price=1.25, output_price=10, currency='USD'),
+    }
+
     @staticmethod
     def create_client(model_name, **kwargs) -> OpenAI:
-        volcengine_models = [
-            VOLCENGINE_MODEL_DOUBAO_PRO_32K,
-            VOLCENGINE_MODEL_DOUBAO_PRO_1_5,
-            VOLCENGINE_MODEL_DOUBAO_1_5_VISION_PRO,
-            VOLCENGINE_MODEL_DEEPSEEK_V3
-        ]
-        deepseek_models = [
-            DEEPSEEK_CHAT_MODEL,
-        ]
-        openai_models = [
-            OPENAI_MODEL_GPT_4o_mini,
-            OPENAI_MODEL_GPT_4o
-        ]
-        openrouter_models = [
-            OPENROUTER_MODEL_CLAUDE_3_7_SONNET,
-        ]
-        if model_name in volcengine_models:
+        if model_name in OpenAICompatible.volcengine_models:
             llm_client = OpenAI(api_key=VOLCENGINE_API_TOKEN, base_url=VOLCENGINE_BASE_URL, **kwargs)
-        elif model_name in deepseek_models:
+        elif model_name in OpenAICompatible.deepseek_models:
             llm_client = OpenAI(api_key=DEEPSEEK_API_TOKEN, base_url=DEEPSEEK_BASE_URL, **kwargs)
-        elif model_name in openai_models:
-            socks_conf = configs.get().get('system', {}).get('outside_proxy', {}).get('socks5', {})
-            if socks_conf:
-                http_client = httpx.Client(
-                    timeout=httpx.Timeout(600, connect=5.0),
-                    proxy=f"socks5://{socks_conf['hostname']}:{socks_conf['port']}"
-                )
-                kwargs['http_client'] = http_client
+        elif model_name in OpenAICompatible.openai_models:
+            kwargs['http_client'] = OpenAICompatible.create_outside_proxy_http_client()
             llm_client = OpenAI(api_key=OPENAI_API_TOKEN, base_url=OPENAI_BASE_URL, **kwargs)
-        elif model_name in openrouter_models:
+        elif model_name in OpenAICompatible.openrouter_models:
+            # kwargs['http_client'] = OpenAICompatible.create_outside_proxy_http_client()
             llm_client = OpenAI(api_key=OPENROUTER_API_TOKEN, base_url=OPENROUTER_BASE_URL, **kwargs)
         else:
             raise Exception("Unsupported model: %s" % model_name)
         return llm_client
 
+    @staticmethod
+    def create_outside_proxy_http_client() -> httpx.Client:
+        """
+        Create an HTTP client with outside proxy settings.
+        :return: Configured httpx.Client instance
+        """
+        socks_conf = configs.get().get('system', {}).get('outside_proxy', {}).get('socks5', {})
+        if socks_conf:
+            return httpx.Client(
+                timeout=httpx.Timeout(600, connect=5.0),
+                proxy=f"socks5://{socks_conf['hostname']}:{socks_conf['port']}"
+            )
+        # If no proxy is configured, return a standard client
+        logger.error("Outside proxy not configured, using default httpx client.")
+        return httpx.Client(timeout=httpx.Timeout(600, connect=5.0))
+
+    @staticmethod
+    def get_price(model_name: str) -> ModelPrice:
+        """
+        Get the price for a given model.
+        :param model_name: Name of the model
+        :return: ModelPrice object containing input and output prices
+        """
+        if model_name not in OpenAICompatible.model_prices:
+            raise ValueError(f"Model {model_name} not found in price list.")
+        return OpenAICompatible.model_prices[model_name]
+
+    @staticmethod
+    def calculate_cost(model_name: str, input_tokens: int, output_tokens: int, convert_to_cny: bool = True) -> float:
+        """
+        Calculate the cost for a given model based on input and output tokens.
+        :param model_name: Name of the model
+        :param input_tokens: Number of input tokens
+        :param output_tokens: Number of output tokens
+        :param convert_to_cny: Whether to convert the cost to CNY (default is True)
+        :return: Total cost in the model's currency
+        """
+        if model_name not in OpenAICompatible.model_prices:
+            raise ValueError(f"Model {model_name} not found in price list.")
+        price = OpenAICompatible.model_prices[model_name]
+        return price.get_total_cost(input_tokens, output_tokens, convert_to_cny)
+
 class CrossAccountJWTOAuthApp(JWTOAuthApp):
     def __init__(self, account_id: str, client_id: str, private_key: str, public_key_id: str, base_url):
         self.account_id = account_id

+ 22 - 0
pqai_agent/clients/hot_topic_client.py

@@ -0,0 +1,22 @@
+import time
+from pqai_agent.toolkit.hot_topic_toolkit import HotTopicToolkit
+
+class HotTopicClient:
+    def __init__(self):
+        self._cache = None
+        self._cache_time = 0
+        self._cache_ttl = 300  # 5分钟
+        self._toolkit = HotTopicToolkit()
+
+    def get_hot_topics(self):
+        now = time.time()
+        if self._cache and (now - self._cache_time) < self._cache_ttl:
+            return self._cache
+        topics = self._toolkit.get_hot_topics()
+        self._cache = topics
+        self._cache_time = now
+        return topics
+
+# 全局可用实例
+hot_topic_client = HotTopicClient()
+

+ 1 - 1
pqai_agent/clients/relation_stage_client.py

@@ -2,7 +2,7 @@ from typing import Optional
 
 import requests
 
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 
 class RelationStageClient:
     UNKNOWN_RELATION_STAGE = '未知'

+ 11 - 6
pqai_agent/configs/dev.yaml

@@ -23,6 +23,9 @@ storage:
   staff:
     database: ai_agent
     table: qywx_employee
+  agent_user_relation:
+    database: ai_agent
+    table: qywx_employee_customer
   user_relation:
     database: growth
     table:
@@ -51,8 +54,8 @@ chat_api:
     private_key_path: oauth/coze_privkey.pem
     account_id: 649175100044793
   openai_compatible:
-    text_model: ep-20250414202859-6nkz5
-    multimodal_model: ep-20250421193334-nz5wd
+    text_model: doubao-pro-32k-241215
+    multimodal_model: doubao-1-5-vision-pro-32k-250115
 
 system:
   outside_proxy:
@@ -62,11 +65,12 @@ system:
   scheduler_mode: local
   human_intervention_alert_url: https://open.feishu.cn/open-apis/bot/v2/hook/379fcd1a-0fed-4e58-8cd0-40b6d1895721
   max_reply_workers: 2
-  max_push_workers: 1
-  chat_agent_version: 1
+  push_task_workers: 1
+  chat_agent_version: 2
+  log_dir: .
 
 debug_flags:
-  disable_llm_api_call: True
+  disable_llm_api_call: False
   use_local_user_storage: True
   console_input: True
   disable_active_conversation: True
@@ -82,4 +86,5 @@ mq:
   scheduler_topic: agent_scheduler_event_dev
   scheduler_group: agent_scheduler_event_dev
   push_tasks_topic: agent_push_tasks_dev
-  push_tasks_group: agent_push_tasks_dev
+  push_send_task_group: agent_push_tasks_dev
+  push_generate_task_group: agent_push_generate_task_dev

+ 13 - 4
pqai_agent/configs/prod.yaml

@@ -37,7 +37,13 @@ storage:
     table: qywx_chat_history
   push_record:
     database: ai_agent
-    table: agent_push_record_dev
+    table: agent_push_record
+  agent_configuration:
+    table: agent_configuration
+  test_task:
+    table: agent_test_task
+  test_task_conversations:
+    table: agent_test_task_conversations
 
 chat_api:
   coze:
@@ -46,8 +52,8 @@ chat_api:
     private_key_path: oauth/coze_privkey.pem
     account_id: 649175100044793
   openai_compatible:
-    text_model: ep-20250414202859-6nkz5
-    multimodal_model: ep-20250421193334-nz5wd
+    text_model: doubao-pro-32k-241215
+    multimodal_model: doubao-1-5-vision-pro-32k-250115
 
 system:
   outside_proxy:
@@ -57,6 +63,8 @@ system:
   scheduler_mode: mq
   human_intervention_alert_url: https://open.feishu.cn/open-apis/bot/v2/hook/c316b559-1c6a-4c4e-97c9-50b44e4c2a9d
   max_reply_workers: 5
+  push_task_workers: 5
+  log_dir: /var/log/agent_service
 
 agent_behavior:
   message_aggregation_sec: 20
@@ -79,4 +87,5 @@ mq:
   scheduler_topic: agent_scheduler_event
   scheduler_group: agent_scheduler_event
   push_tasks_topic: agent_push_tasks
-  push_tasks_group: agent_push_tasks
+  push_send_task_group: agent_push_tasks
+  push_generate_task_group: agent_push_generate_task

+ 3 - 2
pqai_agent/data_models/agent_configuration.py

@@ -1,7 +1,7 @@
 from enum import Enum
 
 from sqlalchemy import Column, Integer, Text, BigInteger, String, SmallInteger, Boolean, TIMESTAMP
-from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import declarative_base
 
 Base = declarative_base()
 
@@ -16,6 +16,7 @@ class AgentConfiguration(Base):
     id = Column(BigInteger, primary_key=True, autoincrement=True, comment="主键id")
     name = Column(String(64), nullable=False, comment="唯一名称")
     display_name = Column(String(64), nullable=True, comment="可选,显示名")
+    description = Column(Text, nullable=True, comment="可选,Agent功能描述")
     type = Column(SmallInteger, nullable=False, default=0, comment="Agent类型,0-响应式,1-自主规划式")
     execution_model = Column(String(64), nullable=True, comment="执行LLM")
     system_prompt = Column(Text, nullable=True, comment="系统设定prompt模板")
@@ -27,4 +28,4 @@ class AgentConfiguration(Base):
     create_user = Column(String(32), nullable=True, comment="创建用户")
     update_user = Column(String(32), nullable=True, comment="更新用户")
     create_time = Column(TIMESTAMP, nullable=True, server_default="CURRENT_TIMESTAMP", comment="创建时间")
-    update_time = Column(TIMESTAMP, nullable=True, server_default="CURRENT_TIMESTAMP", onupdate="CURRENT_TIMESTAMP", comment="更新时间")
+    update_time = Column(TIMESTAMP, nullable=True, server_default="CURRENT_TIMESTAMP", server_onupdate="CURRENT_TIMESTAMP", comment="更新时间")

+ 2 - 2
pqai_agent/data_models/agent_push_record.py

@@ -1,12 +1,12 @@
 from sqlalchemy import Column, Integer, Text, BigInteger
-from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import declarative_base
 
 from pqai_agent import configs
 
 Base = declarative_base()
 
 class AgentPushRecord(Base):
-    __tablename__ = configs.get().get('storage', {}).get('push_record', {}).get('table_name', 'agent_push_record_dev')
+    __tablename__ = configs.get().get('storage', {}).get('push_record', {}).get('table', 'agent_push_record_dev')
     id = Column(Integer, primary_key=True)
     staff_id = Column(Integer)
     user_id = Column(Integer)

+ 22 - 0
pqai_agent/data_models/agent_task.py

@@ -0,0 +1,22 @@
+from sqlalchemy import Column, Integer, Text, BigInteger, String, SmallInteger, Boolean, TIMESTAMP
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+
+class AgentTask(Base):
+    __tablename__ = "agent_task"
+
+    id = Column(BigInteger, primary_key=True, autoincrement=True, comment="主键id")
+    agent_id = Column(BigInteger, nullable=False, comment="agent主键")
+    status = Column(Integer, nullable=False, default=0, comment="状态(0:未开始, 1:进行中, 2:已完成, 3:失败)")
+    start_time = Column(TIMESTAMP, nullable=True, comment="任务开始执行时间")
+    end_time = Column(TIMESTAMP, nullable=True, comment="任务结束执行时间")
+    create_user = Column(String(32), nullable=True, comment="创建用户")
+    input = Column(Text, nullable=True, comment="任务执行输入")
+    tools = Column(Text, nullable=True, comment="任务使用的工具")
+    output = Column(Text, nullable=True, comment="任务执行输出")
+    error_message = Column(Text, nullable=True, comment="错误详情(失败时记录)")
+    create_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", comment="创建时间")
+    update_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", onupdate="CURRENT_TIMESTAMP",
+                         comment="更新时间")

+ 23 - 0
pqai_agent/data_models/agent_task_detail.py

@@ -0,0 +1,23 @@
+from sqlalchemy import Column, Integer, Text, BigInteger, String, SmallInteger, Boolean, TIMESTAMP
+from sqlalchemy.dialects.mysql import VARCHAR
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+
+class AgentTaskDetail(Base):
+    __tablename__ = "agent_task_detail"
+
+    id = Column(BigInteger, primary_key=True, autoincrement=True, comment="主键id")
+    agent_task_id = Column(BigInteger, nullable=False, comment="agent执行任务id")
+    parent_execution_id = Column(BigInteger, nullable=False, comment="父级执行ID(用于构建调用树)")
+    executor_type = Column(VARCHAR(32), nullable=True, comment="执行体类型(LLM/agent/tool)")
+    status = Column(Integer, nullable=False, default=0, comment="执行状态(0-执行中 1-成功 2-失败)")
+    input_data = Column(Text, nullable=True, comment="执行输入参数(结构化存储)")
+    executor_name = Column(Text, nullable=True, comment="执行模型名/工具名/子Agent名")
+    reasoning = Column(Text, nullable=True, comment="思考过程(仅适用于LLM步骤)")
+    output_data = Column(Text, nullable=True, comment="当前执行体的原始输出")
+    error_message = Column(Text, nullable=True, comment="错误详情(失败时记录)")
+    create_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", comment="创建时间")
+    update_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", onupdate="CURRENT_TIMESTAMP",
+                         comment="更新时间")

+ 20 - 0
pqai_agent/data_models/agent_test_task.py

@@ -0,0 +1,20 @@
+from sqlalchemy import Column, Integer, Text, BigInteger, String, SmallInteger, Boolean, TIMESTAMP
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+
+class AgentTestTask(Base):
+    __tablename__ = "agent_test_task"
+
+    id = Column(BigInteger, primary_key=True, autoincrement=True, comment="主键id")
+    agent_id = Column(BigInteger, nullable=False, comment="agent主键")
+    module_id = Column(BigInteger, nullable=False, comment="model主键")
+    create_user = Column(String(32), nullable=True, comment="创建用户")
+    update_user = Column(String(32), nullable=True, comment="更新用户")
+    dataset_ids = Column(Text, nullable=True, comment="数据集ids")
+    evaluate_type = Column(Integer, nullable=False, default=0, comment="数据集ids")
+    status = Column(Integer, nullable=True, comment="状态(0:未开始, 1:进行中, 2:已完成, 3:已取消)")
+    create_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", comment="创建时间")
+    update_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", onupdate="CURRENT_TIMESTAMP",
+                         comment="更新时间")

+ 22 - 0
pqai_agent/data_models/agent_test_task_conversations.py

@@ -0,0 +1,22 @@
+from sqlalchemy import Column, Integer, Text, BigInteger, String, SmallInteger, Boolean, TIMESTAMP
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+
+class AgentTestTaskConversations(Base):
+    __tablename__ = "agent_test_task_conversations"
+
+    id = Column(BigInteger, primary_key=True, autoincrement=True, comment="主键id")
+    task_id = Column(BigInteger, nullable=False, comment="任务主键")
+    agent_id = Column(BigInteger, nullable=False, comment="agent主键")
+    dataset_id = Column(BigInteger, nullable=False, comment="数据集主键")
+    conversation_id = Column(BigInteger, nullable=False, comment="对话主键")
+    input = Column(Text, nullable=False, comment="输入内容")
+    output = Column(Text, nullable=False, comment="输出内容")
+    score = Column(Text, nullable=False, comment="得分")
+    status = Column(Integer, default=0, nullable=False,
+                    comment="状态(0:待执行, 1:执行中, 2:执行成功, 3:执行失败, 4:已取消, 5:消息失败, 6:打分失败)")
+    create_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", comment="创建时间")
+    update_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", onupdate="CURRENT_TIMESTAMP",
+                         comment="更新时间")

+ 17 - 0
pqai_agent/data_models/dataset_model.py

@@ -0,0 +1,17 @@
+from sqlalchemy import Column, Integer, Text, BigInteger, String, SmallInteger, Boolean, TIMESTAMP
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+
+class DatasetModule(Base):
+    __tablename__ = "dataset_module"
+
+    id = Column(BigInteger, primary_key=True, autoincrement=True, comment="主键id")
+    dataset_id = Column(BigInteger, nullable=False, comment="数据集id")
+    module_id = Column(BigInteger, nullable=False, comment="模型id")
+    is_default = Column(Integer, nullable=False, default=0, comment="是否为该模块的默认数据集")
+    is_delete = Column(Integer, nullable=False, default=0, comment="是否删除 1-删除 0-未删除")
+    create_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", comment="创建时间")
+    update_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", onupdate="CURRENT_TIMESTAMP",
+                         comment="更新时间")

+ 18 - 0
pqai_agent/data_models/datasets.py

@@ -0,0 +1,18 @@
+from sqlalchemy import Column, Integer, Text, BigInteger, String, SmallInteger, Boolean, TIMESTAMP
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+
+class Datasets(Base):
+    __tablename__ = "datasets"
+
+    id = Column(BigInteger, primary_key=True, autoincrement=True, comment="主键id")
+    name = Column(String(64), nullable=True, comment="数据集名称")
+    type = Column(Integer, default=0, nullable=False, comment="数据集类型 0-内部 1-外部")
+    description = Column(String(256), nullable=True, comment="数据集描述")
+    is_delete = Column(Integer, nullable=False, default=False, comment="是否删除 1-删除 0-未删除")
+    create_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", comment="创建时间")
+    update_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", onupdate="CURRENT_TIMESTAMP",
+                         comment="更新时间")
+

+ 23 - 0
pqai_agent/data_models/internal_conversation_data.py

@@ -0,0 +1,23 @@
+from sqlalchemy import Column, Integer, Text, BigInteger, String, SmallInteger, Boolean, TIMESTAMP, Float
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+
+class InternalConversationData(Base):
+    __tablename__ = "internal_conversation_data"
+
+    id = Column(BigInteger, primary_key=True, autoincrement=True, comment="主键")
+    dataset_id = Column(BigInteger, nullable=False, comment="数据集id")
+    staff_id = Column(String(64), nullable=True, comment="员工画像id")
+    user_id = Column(String(64), nullable=True, comment="用户画像id")
+    version_date = Column(String(16), nullable=True, comment="日期版本")
+    conversation = Column(Text, nullable=True, comment="输入内容")
+    content = Column(Text, nullable=True, comment="回复消息内容")
+    send_time = Column(BigInteger, nullable=False, comment="回复时间戳")
+    send_type = Column(Integer, nullable=False, comment="回复类型 0: reply 1: push")
+    user_active_rate = Column(Float, nullable=False, comment="用户活跃度")
+    is_delete = Column(Integer, nullable=False, default=False, comment="是否删除 1-删除 0-未删除")
+    create_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", comment="创建时间")
+    update_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", onupdate="CURRENT_TIMESTAMP",
+                         comment="更新时间")

+ 22 - 0
pqai_agent/data_models/qywx_chat_history.py

@@ -0,0 +1,22 @@
+from sqlalchemy import Column, Integer, Text, BigInteger, String, SmallInteger, Boolean, TIMESTAMP
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+
+class QywxChatHistory(Base):
+    __tablename__ = "qywx_chat_history"
+
+    id = Column(BigInteger, primary_key=True, autoincrement=True, comment="主键id")
+    guid = Column(String(64), nullable=False, comment="设备唯一标识")
+    appinfo = Column(String(128), nullable=True)
+    quote_appinfo = Column(String(128), nullable=True)
+    sender =  Column(String(64), nullable=False, comment="发送者 ID")
+    receiver = Column(String(64), nullable=False, comment="接收者 ID")
+    roomid = Column(String(64), nullable=False,default='0', comment="聊天室id,私聊:private前缀,群聊:group前缀")
+    sendtime = Column(BigInteger, nullable=True, default=0, comment="单位ms")
+    sender_name = Column(String(255), nullable=False, comment="发送者昵称")
+    msg_type = Column(Integer, nullable=False, default=0, comment="消息类型 枚举参考代码")
+    attachment = Column(Text, nullable=True, comment="附件:图片、视频等")
+    origin_msg = Column(Text, nullable=True, comment="原始消息")
+    content = Column(Text, nullable=True)

+ 22 - 0
pqai_agent/data_models/qywx_employee.py

@@ -0,0 +1,22 @@
+from sqlalchemy import Column, Integer, Text, BigInteger, String, SmallInteger, Boolean, TIMESTAMP
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+
+class QywxEmployee(Base):
+    __tablename__ = "qywx_employee"
+
+    id = Column(BigInteger, primary_key=True, autoincrement=True, comment="主键id")
+    third_party_user_id = Column(String(32), nullable=True, comment="员工在三方平台ID,唯一")
+    name = Column(String(50), nullable=False, comment="员工姓名")
+    wxid = Column(String(32), nullable=False, comment="员工在企业微信的ID,唯一")
+    status = Column(Integer, nullable=False, comment="员工状态(0: 离职, 1: 在职)")
+    create_time = Column(BigInteger, nullable=True, default=0, comment="创建时间(时间戳)")
+    update_time = Column(BigInteger, nullable=True, default=0, comment="更新时间(时间戳)")
+    agent_name = Column(String(50), nullable=True, comment="作为服务助手时的名字")
+    agent_gender = Column(SmallInteger, nullable=True, comment="作为服务助手时的性别")
+    agent_age = Column(SmallInteger, nullable=True, comment="作为服务助手时的年龄")
+    agent_region = Column(String(50), nullable=True, comment="作为服务助手时的地区")
+    agent_profile = Column(Text, nullable=True, comment="服务助手的画像,JSON字符串")
+    guid = Column(String(50), nullable=True, comment="设备ID")

+ 2 - 2
pqai_agent/data_models/service_module.py

@@ -1,7 +1,7 @@
 from enum import Enum
 
 from sqlalchemy import Column, Integer, Text, BigInteger, String, SmallInteger, Boolean, TIMESTAMP
-from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import declarative_base
 
 Base = declarative_base()
 
@@ -19,4 +19,4 @@ class ServiceModule(Base):
     default_agent_id = Column(BigInteger, nullable=True, comment="默认Agent ID")
     is_delete = Column(Boolean, nullable=False, default=False, comment="逻辑删除标识")
     create_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", comment="创建时间")
-    update_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", onupdate="CURRENT_TIMESTAMP", comment="更新时间")
+    update_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", server_onupdate="CURRENT_TIMESTAMP", comment="更新时间")

+ 1 - 1
pqai_agent/database.py

@@ -5,7 +5,7 @@
 # Copyright © 2024 StrayWarrior <i@straywarrior.com>
 
 import pymysql
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 
 class MySQLManager:
     def __init__(self, config):

+ 10 - 4
pqai_agent/dialogue_manager.py

@@ -1,6 +1,7 @@
 #! /usr/bin/env python
 # -*- coding: utf-8 -*-
 # vim:fenc=utf-8
+import json
 import random
 from enum import Enum
 from typing import Dict, List, Optional, Tuple, Any
@@ -14,9 +15,10 @@ import cozepy
 from sqlalchemy.orm import sessionmaker, Session
 
 from pqai_agent import configs
+from pqai_agent.clients.hot_topic_client import hot_topic_client
 from pqai_agent.clients.relation_stage_client import RelationStageClient
 from pqai_agent.data_models.agent_push_record import AgentPushRecord
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 from pqai_agent.database import MySQLManager
 from pqai_agent import chat_service, prompt_templates
 from pqai_agent.history_dialogue_service import HistoryDialogueService
@@ -153,7 +155,9 @@ class DialogueManager:
             return TimeContext.NIGHT
 
     def is_valid(self):
-        if not self.staff_profile.get('name', None) and not self.staff_profile.get('agent_name', None):
+        if not self.staff_profile.get('name', None) \
+                and not self.staff_profile.get('agent_name', None) \
+                and not self.staff_profile.get('基础信息', {}).get('昵称', None):
             return False
         return True
 
@@ -358,7 +362,8 @@ class DialogueManager:
 
     def _send_alert(self, alert_type: str, reason: Optional[str] = None) -> None:
         time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
-        staff_info = f"{self.staff_profile.get('name', '未知')}[{self.staff_id}]"
+        name = self.staff_profile.get('name', None) or self.staff_profile.get('基础信息', {}).get('昵称', None)
+        staff_info = f"{name}[{self.staff_id}]"
         user_info = f"{self.user_profile.get('nickname', '未知')}[{self.user_id}]"
         alert_message = f"""
         {alert_type}告警
@@ -567,8 +572,9 @@ class DialogueManager:
             "if_first_interaction": True if self.previous_state == DialogueState.INITIALIZED else False,
             "if_active_greeting": False if user_message else True,
             "relation_stage": self.relation_stage,
-            "formatted_staff_profile": prompt_utils.format_agent_profile(self.staff_profile),
+            "formatted_staff_profile": prompt_utils.format_agent_profile_v2(self.staff_profile),
             "formatted_user_profile": prompt_utils.format_user_profile(self.user_profile),
+            "hot_topics": json.dumps(hot_topic_client.get_hot_topics(), indent=2, ensure_ascii=False),
             **self.user_profile,
             **legacy_staff_profile
         }

+ 1 - 1
pqai_agent/history_dialogue_service.py

@@ -7,7 +7,7 @@ import requests
 from pymysql.cursors import DictCursor
 
 from pqai_agent.database import MySQLManager
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 import time
 
 from pqai_agent import configs

+ 9 - 7
pqai_agent/logging_service.py → pqai_agent/logging.py

@@ -26,25 +26,27 @@ class ColoredFormatter(logging.Formatter):
         return message
 
 def setup_root_logger(level=logging.DEBUG, logfile_name='service.log'):
-    formatter = ColoredFormatter(
-        '%(asctime)s - %(name)s %(funcName)s[%(lineno)d] - %(levelname)s - %(message)s'
-    )
+    logging_format = '%(asctime)s - %(name)s %(funcName)s[%(lineno)d] - %(levelname)s - %(message)s'
+    plain_formatter = logging.Formatter(logging_format)
+    color_formatter = ColoredFormatter(logging_format)
     console_handler = logging.StreamHandler()
     console_handler.setLevel(logging.DEBUG)
-    console_handler.setFormatter(formatter)
+    console_handler.setFormatter(color_formatter)
 
     root_logger = logging.getLogger()
     root_logger.handlers.clear()
     root_logger.addHandler(console_handler)
-    if configs.get_env() == 'dev':
+
+    log_dir = configs.get().get('system', {}).get('log_dir', '')
+    if log_dir:
         file_handler = RotatingFileHandler(
-            f'{logfile_name}',
+            f'{log_dir}/{logfile_name}',
             maxBytes=64 * 1024 * 1024,
             backupCount=5,
             encoding='utf-8'
         )
         file_handler.setLevel(logging.DEBUG)
-        file_handler.setFormatter(formatter)
+        file_handler.setFormatter(plain_formatter)
         root_logger.addHandler(file_handler)
 
     agent_logger = logging.getLogger('agent')

+ 2 - 3
pqai_agent/message_queue_backend.py

@@ -9,8 +9,7 @@ import rocketmq
 from rocketmq import ClientConfiguration, Credentials, SimpleConsumer
 
 from pqai_agent import configs
-from pqai_agent import logging_service
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 from pqai_agent.mq_message import MqMessage, MessageType, MessageChannel
 
 
@@ -87,7 +86,7 @@ class AliyunRocketMQQueueBackend(MessageQueueBackend):
             return None
         rmq_message = messages[0]
         body = rmq_message.body.decode('utf-8')
-        logger.debug("[{}]recv message body: {}".format(self.topic, body))
+        logger.debug(f"[{self.topic}]recv message, group[{rmq_message.message_group}], body: {body}")
         try:
             message = MqMessage.from_json(body)
             message._rmq_message = rmq_message

+ 10 - 3
pqai_agent/mq_message.py

@@ -107,6 +107,14 @@ class MqMessage(BaseModel):
      senderUnionId: Optional[str] = None
      receiver: str
      content: Optional[str] = None
+     desc: Optional[str] = None
+     title: Optional[str] = None
+     appIconUrl: Optional[str] = None
+     pagePath: Optional[str] = None
+     coverImage: Optional[str] = None
+     appOrgId: Optional[str] = None
+     appId: Optional[str] = None
+
      # 由于需要和其它语言如Java进行序列化和反序列化交互,因此使用camelCase命名法
      sendTime: int
      refMsgId: Optional[int] = None
@@ -127,9 +135,8 @@ class MqMessage(BaseModel):
          )
 
      def to_json(self):
-         return self.model_dump_json(include={
-             "msgId", "type", "channel", "sender", "senderUnionId",
-             "receiver", "content", "sendTime", "refMsgId"
+         return self.model_dump_json(exclude={
+             "_rmq_message",
          })
 
      @staticmethod

+ 1 - 0
pqai_agent/prompt_templates.py

@@ -279,6 +279,7 @@ RESPONSE_TYPE_DETECT_PROMPT = """
 * 默认使用文本
 * 如果用户明确提到使用语音形式,尽量选择语音
 * 用户自身偏向于使用语音形式沟通时,可选择语音
+* 如果用户不认字或有阅读障碍,且内容适合语音朗读,可选择语音
 * 注意分析即将发送的消息内容,如果有不适合使用语音朗读的内容,不要选择使用语音
 * 注意对话中包含的时间!注意时间流逝和情境切换!判断合适的回复方式!
 

+ 65 - 38
pqai_agent/push_service.py

@@ -16,7 +16,7 @@ from pqai_agent.abtest.utils import get_abtest_info
 from pqai_agent.agents.message_push_agent import MessagePushAgent, DummyMessagePushAgent
 from pqai_agent.configs import apollo_config
 from pqai_agent.data_models.agent_push_record import AgentPushRecord
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 from pqai_agent.mq_message import MessageType
 from pqai_agent.toolkit import get_tools
 from pqai_agent.utils.agent_abtest_utils import get_agent_abtest_config
@@ -54,7 +54,10 @@ class PushScanThread:
         first_initiate_tags = set(apollo_config.get_json_value('agent_first_initiate_whitelist_tags', []))
         # 合并白名单,减少配置成本
         white_list_tags.update(first_initiate_tags)
-        for staff_user in self.service.user_relation_manager.list_staff_users(staff_id=self.staff_id):
+        all_staff_users = self.service.user_relation_manager.list_staff_users(staff_id=self.staff_id)
+        all_users = list({pair['user_id'] for pair in all_staff_users})
+        all_user_tags = self.service.user_manager.get_user_tags(all_users)
+        for staff_user in all_staff_users:
             staff_id = staff_user['staff_id']
             user_id = staff_user['user_id']
             # 通过AB实验配置控制用户组是否启用push
@@ -62,8 +65,8 @@ class PushScanThread:
             # if abtest_params.get('agent_push_enabled', 'false').lower() != 'true':
             #     logger.debug(f"User {user_id} not enabled agent push, skipping.")
             #     continue
-            user_tags = self.service.user_relation_manager.get_user_tags(user_id)
-            if configs.get_env() != 'dev' and not white_list_tags.intersection(user_tags):
+            user_tags = all_user_tags.get(user_id, list())
+            if not white_list_tags.intersection(user_tags):
                 should_initiate = False
             else:
                 agent = self.service.get_agent_instance(staff_id, user_id)
@@ -78,58 +81,79 @@ class PushScanThread:
 
 class PushTaskWorkerPool:
     def __init__(self, agent_service: 'AgentService', mq_topic: str,
-                 mq_consumer: rocketmq.SimpleConsumer, mq_producer: rocketmq.Producer):
+                 mq_consumer_generate: rocketmq.SimpleConsumer,
+                 mq_consumer_send: rocketmq.SimpleConsumer,
+                 mq_producer: rocketmq.Producer):
         self.agent_service = agent_service
         max_workers = configs.get()['system'].get('push_task_workers', 5)
+        self.max_push_workers = max_workers
         self.generate_executor = ThreadPoolExecutor(max_workers=max_workers)
         self.send_executors = {}
         self.rmq_topic = mq_topic
-        self.consumer = mq_consumer
+        self.generate_consumer = mq_consumer_generate
+        self.send_consumer = mq_consumer_send
         self.producer = mq_producer
-        self.loop_thread = None
+        self.generate_loop_thread = None
+        self.send_loop_thread = None
         self.is_generator_running = True
-        self.generate_send_done = False # set by wait_to_finish
-        self.no_more_generate_task = False # set by self
+        self.generate_send_done = False # set by wait_to_finish,表示所有生成任务均已进入队列
+        self.no_more_generate_task = False # generate_send_done被设置之后队列中未再收到生成任务时设置
 
     def start(self):
-        self.loop_thread = Thread(target=self.process_push_tasks)
-        self.loop_thread.start()
+        self.send_loop_thread = Thread(target=self.process_send_tasks)
+        self.send_loop_thread.start()
+        self.generate_loop_thread = Thread(target=self.process_generate_tasks)
+        self.generate_loop_thread.start()
 
-    def process_push_tasks(self):
-        # RMQ consumer疑似有bug,创建后立即消费可能报NPE
+    def process_send_tasks(self):
         time.sleep(1)
         while True:
-            msgs = self.consumer.receive(1, 300)
+            msgs = self.send_consumer.receive(1, 60)
             if not msgs:
                 # 没有生成任务在执行且没有消息,才可退出
-                if self.generate_send_done:
-                    if not self.no_more_generate_task:
-                        logger.debug("no message received, there should be no more generate task")
-                        self.no_more_generate_task = True
-                        continue
-                    else:
-                        if self.is_generator_running:
-                            logger.debug("Waiting for generator threads to finish")
-                            continue
-                        else:
-                            break
+                if self.no_more_generate_task and not self.is_generator_running:
+                    break
                 else:
                     continue
             msg = msgs[0]
             task = json.loads(msg.body.decode('utf-8'))
             msg_time = datetime.fromtimestamp(task['timestamp'] / 1000).strftime("%Y-%m-%d %H:%M:%S")
             logger.debug(f"recv message:{msg_time} - {task}")
-            if task['task_type'] == TaskType.GENERATE.value:
-                self.generate_executor.submit(self.handle_generate_task, task, msg)
-            elif task['task_type'] == TaskType.SEND.value:
+            if task['task_type'] == TaskType.SEND.value:
                 staff_id = task['staff_id']
                 if staff_id not in self.send_executors:
                     self.send_executors[staff_id] = ThreadPoolExecutor(max_workers=1)
                 self.send_executors[staff_id].submit(self.handle_send_task, task, msg)
             else:
                 logger.error(f"Unknown task type: {task['task_type']}")
-                self.consumer.ack(msg)
-        logger.info("PushGenerateWorkerPool stopped")
+                self.send_consumer.ack(msg)
+        logger.info("PushGenerateWorkerPool send thread stopped")
+
+    def process_generate_tasks(self):
+        time.sleep(1)
+        while True:
+            if self.generate_executor._work_queue.qsize() > self.max_push_workers * 2:
+                logger.warning("Too many generate tasks in queue, consume later")
+                time.sleep(10)
+                continue
+            msgs = self.generate_consumer.receive(1, 300)
+            if not msgs:
+                # 生成任务已经创建完成 且 未收到新任务,才可退出
+                if self.generate_send_done:
+                    logger.debug("no message received, there should be no more generate task")
+                    self.no_more_generate_task = True
+                    break
+                else:
+                    continue
+            msg = msgs[0]
+            task = json.loads(msg.body.decode('utf-8'))
+            msg_time = datetime.fromtimestamp(task['timestamp'] / 1000).strftime("%Y-%m-%d %H:%M:%S")
+            logger.debug(f"recv message:{msg_time} - {task}")
+            if task['task_type'] == TaskType.GENERATE.value:
+                self.generate_executor.submit(self.handle_generate_task, task, msg)
+            else:
+                self.generate_consumer.ack(msg)
+        logger.info("PushGenerateWorkerPool generator thread stopped")
 
     def wait_to_finish(self):
         self.generate_send_done = True
@@ -138,7 +162,8 @@ class PushTaskWorkerPool:
             time.sleep(1)
         self.generate_executor.shutdown(wait=True)
         self.is_generator_running = False
-        self.loop_thread.join()
+        self.generate_loop_thread.join()
+        self.send_loop_thread.join()
 
     def handle_send_task(self, task: Dict, msg: rocketmq.Message):
         try:
@@ -147,13 +172,13 @@ class PushTaskWorkerPool:
             agent = self.agent_service.get_agent_instance(staff_id, user_id)
             # 二次校验是否需要发送
             if not agent.should_initiate_conversation():
-                logger.debug(f"user[{user_id}], do not initiate conversation")
-                self.consumer.ack(msg)
+                logger.debug(f"user[{user_id}], should not initiate, skip sending task")
+                self.send_consumer.ack(msg)
                 return
             contents: List[Dict] = json.loads(task['content'])
             if not contents:
                 logger.debug(f"staff[{staff_id}], user[{user_id}]: empty content, do not send")
-                self.consumer.ack(msg)
+                self.send_consumer.ack(msg)
                 return
             recent_dialogue = agent.dialogue_history[-10:]
             agent_voice_whitelist = set(apollo_config.get_json_value("agent_voice_whitelist", []))
@@ -189,11 +214,11 @@ class PushTaskWorkerPool:
                 agent.update_last_active_interaction_time(current_ts)
             else:
                 logger.debug(f"staff[{staff_id}], user[{user_id}]: generate empty response")
-            self.consumer.ack(msg)
+            self.send_consumer.ack(msg)
         except Exception as e:
             fmt_exc = traceback.format_exc()
             logger.error(f"Error processing message sending: {e}, {fmt_exc}")
-            self.consumer.ack(msg)
+            self.send_consumer.ack(msg)
 
     def handle_generate_task(self, task: Dict, msg: rocketmq.Message):
         try:
@@ -223,15 +248,17 @@ class PushTaskWorkerPool:
                 ),
                 query_prompt_template=query_prompt_template
             )
+            cost = push_agent.get_total_cost()
+            logger.debug(f"staff[{staff_id}], user[{user_id}]: push message generation cost: {cost}")
             if message_to_user:
                 rmq_message = generate_task_rmq_message(
                     self.rmq_topic, staff_id, user_id, TaskType.SEND, json.dumps(message_to_user))
                 self.producer.send(rmq_message)
             else:
                 logger.info(f"staff[{staff_id}], user[{user_id}]: no push message generated")
-            self.consumer.ack(msg)
+            self.generate_consumer.ack(msg)
         except Exception as e:
             fmt_exc = traceback.format_exc()
             logger.error(f"Error processing message generation: {e}, {fmt_exc}")
             # FIXME: 是否需要ACK
-            self.consumer.ack(msg)
+            self.generate_consumer.ack(msg)

+ 1 - 1
pqai_agent/rate_limiter.py

@@ -5,7 +5,7 @@
 import time
 from typing import Optional, Union, Dict
 
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 from pqai_agent.mq_message import MessageType
 
 

+ 2 - 2
pqai_agent/response_type_detector.py

@@ -12,7 +12,7 @@ from pqai_agent import chat_service
 from pqai_agent import configs
 from pqai_agent import prompt_templates
 from pqai_agent.dialogue_manager import DialogueManager
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 from pqai_agent.mq_message import MessageType
 
 
@@ -36,7 +36,7 @@ class ResponseTypeDetector:
             api_key=chat_service.VOLCENGINE_API_TOKEN,
             base_url=chat_service.VOLCENGINE_BASE_URL
         )
-        self.model_name = chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_1_5
+        self.model_name = chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_1_5_32K
 
     def detect_type(self, dialogue_history: List[Dict], next_message: Dict, enable_random=False,
                     random_rate=0.25):

+ 1 - 1
pqai_agent/service_module_manager.py

@@ -1,5 +1,5 @@
 from pqai_agent.data_models.service_module import ServiceModule, ModuleAgentType
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 
 class ServiceModuleManager:
     def __init__(self, session_maker):

+ 3 - 2
pqai_agent/toolkit/__init__.py

@@ -1,12 +1,13 @@
 # 必须要在这里导入模块,以便对应的模块执行register_toolkit
 from typing import Sequence, List
 
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 from pqai_agent.toolkit.tool_registry import ToolRegistry
 from pqai_agent.toolkit.image_describer import ImageDescriber
-from pqai_agent.toolkit.message_notifier import MessageNotifier
+from pqai_agent.toolkit.message_toolkit import MessageToolkit
 from pqai_agent.toolkit.pq_video_searcher import PQVideoSearcher
 from pqai_agent.toolkit.search_toolkit import SearchToolkit
+from pqai_agent.toolkit.hot_topic_toolkit import HotTopicToolkit
 
 global_tool_map = ToolRegistry.tool_map
 

+ 1 - 1
pqai_agent/toolkit/function_tool.py

@@ -8,7 +8,7 @@ from pydantic import BaseModel, create_model
 from pydantic.fields import FieldInfo
 from jsonschema.validators import Draft202012Validator as JSONValidator
 import re
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 
 
 def to_pascal(snake: str) -> str:

+ 40 - 0
pqai_agent/toolkit/hot_topic_toolkit.py

@@ -0,0 +1,40 @@
+from typing import List, Dict, Any
+import requests
+
+from pqai_agent.logging import logger
+from pqai_agent.toolkit.base import BaseToolkit
+from pqai_agent.toolkit.function_tool import FunctionTool
+from pqai_agent.toolkit.tool_registry import register_toolkit
+
+
+@register_toolkit
+class HotTopicToolkit(BaseToolkit):
+    def get_hot_topics(self) -> List[Dict[str, Any]]:
+        """
+        获取热点内容列表
+        Returns:
+            List[Dict[str, Any]]: [{'title': ..., 'url': ...}, ...]
+        """
+        url = "http://ai-wechat-hook-internal.piaoquantv.com/hotTopic/getHotContent"
+        try:
+            resp = requests.get(url, timeout=5)
+            resp.raise_for_status()
+            data = resp.json()
+            if data.get("code") == 0 and data.get("success"):
+                result = []
+                for item in data.get("data", []):
+                    if item.get("title") and item.get("url"):
+                        result.append({
+                            "title": item.get("title"),
+                            "url": item.get("url")
+                        })
+                return result
+            else:
+                logger.error(f"Failed to fetch hot topics: {data.get('msg', 'Unknown error')}")
+                return []
+        except Exception as e:
+            logger.error(f"Error in fetching hot topics: {e}")
+            return []
+
+    def get_tools(self) -> List[FunctionTool]:
+        return [FunctionTool(self.get_hot_topics)]

+ 1 - 1
pqai_agent/toolkit/image_describer.py

@@ -3,7 +3,7 @@ import threading
 
 from pqai_agent import chat_service
 from pqai_agent.chat_service import VOLCENGINE_MODEL_DOUBAO_1_5_VISION_PRO
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 from pqai_agent.toolkit.base import BaseToolkit
 from pqai_agent.toolkit.function_tool import FunctionTool
 from pqai_agent.toolkit.tool_registry import register_toolkit

+ 1 - 1
pqai_agent/toolkit/lark_sheet_record_for_human_intervention.py

@@ -4,7 +4,7 @@ from typing import List
 import requests
 from pqai_agent.toolkit.base import BaseToolkit
 from pqai_agent.toolkit.function_tool import FunctionTool
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 
 class LarkSheetRecordForHumanIntervention(BaseToolkit):
     r"""A toolkit for recording human intervention events into a Feishu spreadsheet."""

+ 0 - 54
pqai_agent/toolkit/message_notifier.py

@@ -1,54 +0,0 @@
-from typing import List, Dict
-
-from pqai_agent.logging_service import logger
-from pqai_agent.toolkit.base import BaseToolkit
-from pqai_agent.toolkit.function_tool import FunctionTool
-from pqai_agent.toolkit.tool_registry import register_toolkit
-
-
-@register_toolkit
-class MessageNotifier(BaseToolkit):
-    def __init__(self):
-        super().__init__()
-
-    def message_notify_user(self, message: str) -> str:
-        """Sends a message to the user.
-
-        Args:
-            message (str): The message to send.
-        Returns:
-            str: A confirmation message.
-        """
-
-        logger.info(f"Message to user: {message}")
-        return 'success'
-
-    def output_multimodal_message(self, message: Dict[str, str]) -> str:
-        """Outputs a multimodal message to the user.
-        Message schema:
-        {
-            "type": "text|image|gif|video|mini_program",
-            "content": "message content",
-            "title": "only needed if type is video or mini_program",
-            "cover_image": "only needed if type is mini_program",
-        }
-        if message type is image, gif, video or mini_program, the content should be a URL.
-
-        Args:
-            message (Dict[str, str]): The message to output.
-        Returns:
-            str: A confirmation message.
-        """
-        if message["type"] not in ["text", "image", "gif", "video", "mini_program"]:
-            return f"Invalid message type: {message['type']}"
-        if message["type"] in ("video", "mini_program") and "title" not in message:
-            return "Title is required for video or mini_program messages."
-        if message["type"] == "mini_program" and "cover_image" not in message:
-            return "Cover image is required for mini_program messages."
-        logger.info(f"Multimodal message to user: {message}")
-        return 'success'
-
-
-    def get_tools(self):
-        return [FunctionTool(self.message_notify_user),
-                FunctionTool(self.output_multimodal_message)]

+ 58 - 0
pqai_agent/toolkit/message_toolkit.py

@@ -0,0 +1,58 @@
+from typing import List, Dict
+
+from pqai_agent.logging import logger
+from pqai_agent.toolkit.base import BaseToolkit
+from pqai_agent.toolkit.function_tool import FunctionTool
+from pqai_agent.toolkit.tool_registry import register_toolkit
+
+
+@register_toolkit
+class MessageToolkit(BaseToolkit):
+    def __init__(self):
+        super().__init__()
+
+    def message_notify_user(self, message: str) -> str:
+        """Sends a message to the user.
+
+        Args:
+            message (str): The message to send.
+        Returns:
+            str: A confirmation message.
+        """
+
+        logger.info(f"Message to user: {message}")
+        return 'success'
+
+    def output_multimodal_message(self, message: Dict[str, str]) -> str:
+        """Outputs a multimodal message to the user.
+        Message schema:
+        {
+            "type": "text|image|gif|video|mini_program|link",
+            "content": "text message content or url of the media",
+            "title": "only needed if type in: video, link, mini_program",
+            "cover_url": "cover image url, only needed if type in: mini_program",
+            "desc": "description, optional if type in: link"
+        }
+        if message type is image, gif, video, link or mini_program, the content should be a URL.
+
+        Args:
+            message (Dict[str, str]): The message to output.
+        Returns:
+            str: A confirmation message.
+        """
+        msg_type = message.get("type", "")
+        if msg_type not in ["text", "image", "gif", "video", "mini_program", "link"]:
+            return f"Invalid message type: {msg_type}"
+        if msg_type in ("video", "mini_program", "link") and "title" not in message:
+            return f"Title is required for [{msg_type}] messages."
+        if msg_type in ("mini_program", ) and "cover_url" not in message:
+            return f"Cover image URL is required for [{msg_type}] messages."
+        # if msg_type in ("link", ) and "desc" not in message:
+        #     return f"Description is required for [link] messages."
+        logger.info(f"Multimodal message to user: {message}")
+        return 'success'
+
+
+    def get_tools(self):
+        return [FunctionTool(self.message_notify_user),
+                FunctionTool(self.output_multimodal_message)]

+ 44 - 0
pqai_agent/toolkit/sub_agent_toolkit.py

@@ -0,0 +1,44 @@
+import json
+import textwrap
+import types
+
+from pqai_agent.data_models.agent_configuration import AgentConfiguration
+from pqai_agent.toolkit import get_tools
+from pqai_agent.toolkit.base import BaseToolkit
+from pqai_agent.toolkit.function_tool import FunctionTool
+from pqai_agent.agents.simple_chat_agent import SimpleOpenAICompatibleChatAgent
+
+class SubAgentToolkit(BaseToolkit):
+    @staticmethod
+    def create_tool_from_agent(agent_config: AgentConfiguration):
+        def agent_execution_func(user_input: str) -> str:
+            extra_params = json.loads(agent_config.extra_params)
+
+            agent = SimpleOpenAICompatibleChatAgent(
+                model=agent_config.execution_model,
+                system_prompt=agent_config.system_prompt,
+                tools=get_tools(json.loads(agent_config.tools)),
+                generate_cfg=extra_params.get('generate_cfg', {}),
+                max_run_step=extra_params.get('max_run_step', 20)
+            )
+            return agent.run(user_input)
+
+        func_doc = f"""
+            {agent_config.description}
+            Args:
+                user_input (str): 用户输入
+            Returns:
+                str: Agent执行结果
+"""
+        func_doc = textwrap.dedent(func_doc).strip()
+
+        dynamic_function = types.FunctionType(
+            agent_execution_func.__code__,
+            globals(),
+            name=agent_config.name,
+            argdefs=agent_execution_func.__defaults__,
+            closure=agent_execution_func.__closure__
+        )
+        dynamic_function.__doc__ = func_doc
+
+        return FunctionTool(dynamic_function)

+ 52 - 4
pqai_agent/user_manager.py

@@ -1,8 +1,9 @@
 #! /usr/bin/env python
 # -*- coding: utf-8 -*-
 # vim:fenc=utf-8
+from abc import abstractmethod
 
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 from typing import Dict, Optional, List
 import json
 import time
@@ -33,6 +34,10 @@ class UserManager(abc.ABC):
         #FIXME(zhoutian): 重新设计用户和员工数据管理模型
         pass
 
+    @abstractmethod
+    def get_user_tags(self, user_ids: List[str], batch_size = 500) -> Dict[str, List[str]]:
+        pass
+
     @staticmethod
     def get_default_profile(**kwargs) -> Dict:
         default_profile = {
@@ -133,6 +138,9 @@ class LocalUserManager(UserManager):
             logger.error("staff profile not found: {}".format(e))
             return {}
 
+    def get_user_tags(self, user_ids: List[str], batch_size = 500) -> Dict[str, List[str]]:
+        return {}
+
     def list_users(self, **kwargs) -> List[Dict]:
         pass
 
@@ -228,7 +236,7 @@ class MySQLUserManager(UserManager):
         return profile
 
     def get_staff_profile_v3(self, staff_id) -> Dict:
-        sql = f"SELECT agent_profile " \
+        sql = f"SELECT agent_profile_v2 " \
               f"FROM {self.staff_table} WHERE third_party_user_id = '{staff_id}'"
         data = self.db.select(sql)
         if not data:
@@ -249,6 +257,37 @@ class MySQLUserManager(UserManager):
         sql = f"UPDATE {self.staff_table} SET agent_profile = %s WHERE third_party_user_id = '{staff_id}'"
         self.db.execute(sql, (json.dumps(profile),))
 
+    def get_user_tags(self, user_ids: List[str], batch_size = 500) -> Dict[str, List[str]]:
+        """
+        获取用户的标签列表
+        :param user_ids: 用户ID
+        :param batch_size: 批量查询的大小
+        :return: 标签名称列表
+        """
+        batches = (len(user_ids) + batch_size - 1) // batch_size
+        ret = {}
+        for i in range(batches):
+            idx_begin = i * batch_size
+            idx_end = min((i + 1) * batch_size, len(user_ids))
+            batch_user_ids = user_ids[idx_begin:idx_end]
+            sql = f"""
+                SELECT a.third_party_user_id, b.tag_id, b.name FROM qywx_user_tag a
+                    JOIN qywx_tag b ON a.tag_id = b.tag_id
+                """
+            if len(batch_user_ids) == 1:
+                sql += f" AND a.third_party_user_id = '{batch_user_ids[0]}'"""
+            else:
+                sql += f" AND a.third_party_user_id IN {str(tuple(batch_user_ids))}"
+            rows = self.db.select(sql, pymysql.cursors.DictCursor)
+            # group by user_id
+            for row in rows:
+                user_id = row['third_party_user_id']
+                tag_name = row['name']
+                if user_id not in ret:
+                    ret[user_id] = []
+                ret[user_id].append(tag_name)
+        return ret
+
     def list_users(self, **kwargs) -> List[Dict]:
         user_union_id = kwargs.get('user_union_id', None)
         user_name = kwargs.get('user_name', None)
@@ -333,6 +372,7 @@ class MySQLUserRelationManager(UserRelationManager):
         self.relation_table = relation_table
         self.agent_user_table = agent_user_table
         self.user_table = user_table
+        self.agent_user_relation_table = 'qywx_employee_customer'
 
     def list_staffs(self):
         sql = f"SELECT third_party_user_id, name, wxid, agent_name FROM {self.agent_staff_table} WHERE status = 1"
@@ -342,7 +382,14 @@ class MySQLUserRelationManager(UserRelationManager):
     def list_users(self, staff_id: str, page: int = 1, page_size: int = 100):
         return []
 
-    def list_staff_users(self, staff_id: str = None, tag_id: int = None):
+    def list_staff_users(self, staff_id: str = None, tag_id: int = None) -> List[Dict]:
+        sql = f"SELECT employee_id as staff_id, customer_id as user_id FROM {self.agent_user_relation_table} WHERE 1 = 1"
+        if staff_id:
+            sql += f" AND employee_id = '{staff_id}'"
+        agent_staff_data = self.agent_db.select(sql, pymysql.cursors.DictCursor)
+        return agent_staff_data
+
+    def list_staff_users_v1(self, staff_id: str = None, tag_id: int = None):
         sql = f"SELECT third_party_user_id, wxid FROM {self.agent_staff_table} WHERE status = 1"
         if staff_id:
             sql += f" AND third_party_user_id = '{staff_id}'"
@@ -382,7 +429,8 @@ class MySQLUserRelationManager(UserRelationManager):
                 sql = f"SELECT third_party_user_id, wxid FROM {self.agent_user_table} WHERE wxid IN {str(batch_union_ids)}"
                 batch_agent_user_data = self.agent_db.select(sql, pymysql.cursors.DictCursor)
                 if len(agent_user_data) != len(batch_union_ids):
-                    # logger.debug(f"staff[{wxid}] some users not found in agent database")
+                    diff_num = len(batch_union_ids) - len(batch_agent_user_data)
+                    logger.debug(f"staff[{staff_id}] {diff_num} users not found in agent database")
                     pass
                 agent_user_data.extend(batch_agent_user_data)
             staff_user_pairs = [

+ 3 - 3
pqai_agent/user_profile_extractor.py

@@ -8,7 +8,7 @@ from typing import Dict, Optional, List
 from pqai_agent import chat_service, configs
 from pqai_agent.prompt_templates import USER_PROFILE_EXTRACT_PROMPT, USER_PROFILE_EXTRACT_PROMPT_V2
 from openai import OpenAI
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 from pqai_agent.utils import prompt_utils
 
 
@@ -198,8 +198,8 @@ class UserProfileExtractor:
 
 if __name__ == '__main__':
     from pqai_agent import configs
-    from pqai_agent import logging_service
-    logging_service.setup_root_logger()
+    from pqai_agent import logging
+    logging.setup_root_logger()
     config = configs.get()
     config['debug_flags']['disable_llm_api_call'] = False
     extractor = UserProfileExtractor()

+ 8 - 0
pqai_agent/utils/__init__.py

@@ -0,0 +1,8 @@
+import hashlib
+import random
+import time
+
+def random_str() -> str:
+    """生成一个随机的MD5字符串"""
+    random_string = str(random.randint(0, 1000000)) + str(time.time())
+    return hashlib.md5(random_string.encode('utf-8')).hexdigest()

+ 24 - 0
pqai_agent/utils/agent_utils.py

@@ -0,0 +1,24 @@
+import json
+
+from pqai_agent.agents.simple_chat_agent import SimpleOpenAICompatibleChatAgent
+from pqai_agent.data_models.agent_configuration import AgentConfiguration
+from pqai_agent.toolkit import get_tools
+from pqai_agent.toolkit.sub_agent_toolkit import SubAgentToolkit
+
+
+def create_agent_from_config(agent_config: AgentConfiguration, session_maker) -> SimpleOpenAICompatibleChatAgent:
+    tools = get_tools(json.loads(agent_config.tools))
+    sub_agent_ids = json.loads(agent_config.sub_agents)
+    if sub_agent_ids:
+        # 查询子Agent配置
+        with session_maker() as session:
+            sub_agent_configs = session.query(AgentConfiguration).filter(
+                AgentConfiguration.id.in_(sub_agent_ids)).all()
+        # 将子Agent配置转换为工具
+        for sub_agent_config in sub_agent_configs:
+            sub_agent_tool = SubAgentToolkit.create_tool_from_agent(sub_agent_config)
+            tools.append(sub_agent_tool)
+    chat_agent = SimpleOpenAICompatibleChatAgent(model=agent_config.execution_model,
+                                                 system_prompt=agent_config.system_prompt,
+                                                 tools=tools)
+    return chat_agent

+ 12 - 3
pqai_agent/utils/prompt_utils.py

@@ -1,5 +1,9 @@
+import json
+from io import StringIO
 from typing import Dict
 
+import yaml
+
 
 def format_agent_profile(profile: Dict) -> str:
     fields = [
@@ -16,12 +20,17 @@ def format_agent_profile(profile: Dict) -> str:
     ]
     strings_to_join = []
     for field in fields:
-        if not profile.get(field[0], None):
+        if profile.get(field[0], None) is None:
             continue
         cur_string = f"- {field[1]}:{profile[field[0]]}"
         strings_to_join.append(cur_string)
     return "\n".join(strings_to_join)
 
+def format_agent_profile_v2(profile: Dict) -> str:
+    str_stream = StringIO()
+    yaml.dump(profile, str_stream, indent=2, allow_unicode=True)
+    return str_stream.getvalue()
+
 def format_user_profile(profile: Dict) -> str:
     """
     :param profile:
@@ -54,8 +63,8 @@ def format_user_profile(profile: Dict) -> str:
     for field in fields:
         value = profile.get(field[0], None)
         if not value:
-            continue
-        if isinstance(value, list):
+            value = '未知'
+        elif isinstance(value, list):
             value = ','.join(value)
         elif isinstance(value, dict):
             value = ';'.join(f"{k}: {v}" for k, v in value.items())

+ 7 - 4
pqai_agent_server/agent_server.py

@@ -2,10 +2,10 @@ import logging
 import sys
 import time
 
-from pqai_agent import configs, logging_service
+from pqai_agent import configs
 from pqai_agent.agent_service import AgentService
 from pqai_agent.chat_service import ChatServiceType
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger, setup_root_logger
 from pqai_agent.mq_message import MessageType, MqMessage, MessageChannel
 from pqai_agent.message_queue_backend import AliyunRocketMQQueueBackend, MemoryQueueBackend
 from pqai_agent.push_service import PushTaskWorkerPool, PushScanThread
@@ -14,7 +14,7 @@ from pqai_agent.user_manager import LocalUserManager, LocalUserRelationManager,
 
 if __name__ == "__main__":
     config = configs.get()
-    logging_service.setup_root_logger()
+    setup_root_logger()
     logger.warning("current env: {}".format(configs.get_env()))
     scheduler_logger = logging.getLogger('apscheduler')
     scheduler_logger.setLevel(logging.WARNING)
@@ -92,13 +92,16 @@ if __name__ == "__main__":
             continue
         message_id += 1
         sender = '7881301903997433'
-        receiver = '1688855931724582'
+        receiver = '1688854974625870'
         if text in (MessageType.AGGREGATION_TRIGGER.name,
                     MessageType.HUMAN_INTERVENTION_END.name):
             message = MqMessage.build(
                 MessageType.__members__.get(text),
                 MessageChannel.CORP_WECHAT,
                 sender, receiver, None, int(time.time() * 1000))
+        elif text == 'S_PUSH':
+            service._check_initiative_conversations()
+            continue
         else:
             message = MqMessage.build(MessageType.TEXT, MessageChannel.CORP_WECHAT,
                                       sender, receiver, text, int(time.time() * 1000)

+ 227 - 0
pqai_agent_server/agent_task_server.py

@@ -0,0 +1,227 @@
+import json
+import threading
+from concurrent.futures import ThreadPoolExecutor
+from datetime import datetime
+from typing import Dict
+
+from sqlalchemy import func, select
+
+from pqai_agent.data_models.agent_configuration import AgentConfiguration
+from pqai_agent.data_models.agent_task import AgentTask
+from pqai_agent.data_models.agent_task_detail import AgentTaskDetail
+from pqai_agent.logging import logger
+from pqai_agent.utils.agent_utils import create_agent_from_config
+from pqai_agent_server.const.status_enum import AgentTaskStatus, get_agent_task_detail_status_desc, \
+    AgentTaskDetailStatus, get_agent_task_status_desc
+
+
+class AgentTaskManager:
+    """任务管理器"""
+
+    def __init__(self, session_maker):
+        self.session_maker = session_maker
+        self.task_events = {}  # 任务ID -> Event (用于取消任务)
+        self.task_locks = {}  # 任务ID -> Lock (用于任务状态同步)
+        self.running_tasks = set()
+        self.executor = ThreadPoolExecutor(max_workers=10, thread_name_prefix='AgentTaskWorker')
+        self.task_futures = {}  # 任务ID -> Future
+
+    def get_agent_task(self, agent_task_id: int):
+        """获取任务信息"""
+        with self.session_maker() as session:
+            return session.query(AgentTask).filter(AgentTask.id == agent_task_id).one()
+
+    def get_agent_config(self, agent_id: int):
+        """获取任务信息"""
+        with self.session_maker() as session:
+            return session.query(AgentConfiguration).filter(AgentConfiguration.id == agent_id).one()
+
+    def get_in_progress_task(self):
+        """获取执行中任务"""
+        with self.session_maker() as session:
+            return session.query(AgentTask).filter(AgentTask.status.in_([
+                AgentTaskStatus.NOT_STARTED.value,
+                AgentTaskStatus.IN_PROGRESS.value
+            ])).all()
+
+    def get_agent_task_details(self, task_id, parent_execution_id):
+        """获取任务详情"""
+        with self.session_maker() as session:
+            # 创建子查询:统计每个父节点的子节点数量
+            subquery = (
+                select(
+                    AgentTaskDetail.parent_execution_id.label('parent_id'),
+                    func.count('*').label('child_count')
+                )
+                .where(AgentTaskDetail.parent_execution_id.isnot(None))
+                .group_by(AgentTaskDetail.parent_execution_id)
+                .subquery()
+            )
+
+            # 主查询:关联子查询,判断是否有子节点
+            # 修正连接条件:使用parent_execution_id关联
+            query = (
+                select(
+                    AgentTaskDetail,
+                    (func.coalesce(subquery.c.child_count, 0) > 0).label('has_children')
+                )
+                .outerjoin(
+                    subquery,
+                    AgentTaskDetail.id == subquery.c.parent_id  # 使用当前记录的id匹配子查询的parent_id
+                )
+            ).where(AgentTaskDetail.agent_task_id == task_id).where(
+                AgentTaskDetail.parent_execution_id == parent_execution_id)
+            # 执行查询
+            return session.execute(query).all()
+
+    def save_agent_task_details_batch(self, agent_task_details: list, agent_task_id: int, message: str):
+        """批量保存子任务到数据库"""
+        try:
+            with self.session_maker() as session:
+                with session.begin():
+                    session.add_all(agent_task_details)
+                    session.query(AgentTask).filter(
+                        AgentTask.id == agent_task_id).update(
+                        {"status": AgentTaskStatus.COMPLETED.value, "output": message, "update_time": datetime.now()})
+                    session.commit()
+        except Exception as e:
+            logger.error(e)
+            raise Exception(e)
+
+    def update_task_failed(self, task_id, error_message: str):
+        """更新任务失败"""
+        with self.session_maker() as session:
+            session.query(AgentTask).filter(AgentTask.id == task_id).update(
+                {"status": AgentTaskStatus.FAILED.value, "error_message": error_message, "update_time": datetime.now()})
+            session.commit()
+
+    def update_task_status(self, task_id, status):
+        """更新任务状态"""
+        with self.session_maker() as session:
+            session.query(AgentTask).filter(AgentTask.id == task_id).update(
+                {"status": status, "update_time": datetime.now()})
+            session.commit()
+
+    def get_agent_task_list(self, page_num: int, page_size: int) -> Dict:
+        with self.session_maker() as session:
+            # 计算偏移量
+            offset = (page_num - 1) * page_size
+            # 查询分页数据
+            result = (session.query(AgentTask, AgentConfiguration)
+                      .outerjoin(AgentConfiguration, AgentTask.agent_id == AgentConfiguration.id)
+                      .limit(page_size).offset(offset).all())
+            # 查询总记录数
+            total = session.query(func.count(AgentTask.id)).scalar()
+
+            total_page = total // page_size + 1 if total % page_size > 0 else total // page_size
+            total_page = 1 if total_page <= 0 else total_page
+            response_data = [
+                {
+                    "id": agent_task.id,
+                    "agentName": agent_configuration.name,
+                    "statusName": get_agent_task_status_desc(agent_task.status),
+                    "startTime": agent_task.start_time.strftime("%Y-%m-%d %H:%M:%S"),
+                    "endTime": agent_task.start_time.strftime(
+                        "%Y-%m-%d %H:%M:%S") if agent_task.start_time is not None else None,
+                    "createUser": agent_task.create_user,
+                    "input": agent_task.input,
+                    "output": agent_task.output,
+                    "createTime": agent_task.create_time.strftime("%Y-%m-%d %H:%M:%S"),
+                    "updateTime": agent_task.update_time.strftime("%Y-%m-%d %H:%M:%S")
+                }
+                for agent_task, agent_configuration in result
+            ]
+            return {
+                "currentPage": page_num,
+                "pageSize": page_size,
+                "totalSize": total_page,
+                "total": total,
+                "list": response_data,
+            }
+
+    def create_task(self, agent_id: int, task_prompt: str):
+        """创建新任务"""
+        with self.session_maker() as session:
+            agent_config = session.get(AgentConfiguration, agent_id)
+            agent_task = AgentTask(agent_id=agent_id,
+                                   status=AgentTaskStatus.NOT_STARTED.value,
+                                   start_time=datetime.now(),
+                                   input=task_prompt,
+                                   tools=agent_config.tools)
+            session.add(agent_task)
+            session.commit()  # 显式提交
+            task_id = agent_task.id
+        # 异步执行创建任务
+        self.executor.submit(self._execute_task, task_id)
+
+    def _process_task(self, task_id: int):
+        try:
+            self.update_task_status(task_id, AgentTaskStatus.IN_PROGRESS.value)
+            agent_task = self.get_agent_task(task_id)
+            agent_config = self.get_agent_config(agent_task.agent_id)
+            chat_agent = create_agent_from_config(agent_config, self.session_maker)
+            message = chat_agent.run(agent_task.input)
+            agent_task_details = chat_agent.get_agent_task_details()
+            for agent_task_detail in agent_task_details:
+                agent_task_detail.agent_task_id = task_id
+                agent_task_detail.status = AgentTaskDetailStatus.SUCCESS.value
+            self.save_agent_task_details_batch(agent_task_details, task_id, message)
+        except Exception as e:
+            logger.error(e)
+            self.update_task_failed(task_id, str(e))
+
+    def recover_tasks(self):
+        """服务启动时恢复未完成的任务"""
+
+        in_progress_tasks = self.get_in_progress_task()
+
+        for task in in_progress_tasks:
+            task_id = task.id
+            # 重新提交任务
+            self._execute_task(task_id)
+
+    def _execute_task(self, task_id: int):
+        """提交任务到线程池执行"""
+        # 确保任务状态一致性
+        if task_id in self.running_tasks:
+            return
+
+        # 创建任务事件和锁
+        if task_id not in self.task_events:
+            self.task_events[task_id] = threading.Event()
+        if task_id not in self.task_locks:
+            self.task_locks[task_id] = threading.Lock()
+
+        # 提交到线程池
+        future = self.executor.submit(self._process_task, task_id)
+        self.task_futures[task_id] = future
+
+        # 标记任务为运行中
+        with self.task_locks[task_id]:
+            self.running_tasks.add(task_id)
+
+    def get_agent_task_detail(self, agent_task_id, parent_execution_id):
+        agent_task = self.get_agent_task(agent_task_id)
+        agent_task_details = self.get_agent_task_details(agent_task_id, parent_execution_id)
+        agent_task_detail_datas = [
+            {
+                "id": agent_task_detail.id,
+                "agentTaskId": agent_task_detail.agent_task_id,
+                "executorType": agent_task_detail.executor_type,
+                "statusName": get_agent_task_detail_status_desc(agent_task_detail.status),
+                "inputData": agent_task_detail.input_data,
+                "executorName": agent_task_detail.executor_name,
+                "reasoning": agent_task_detail.reasoning,
+                "outputData": agent_task_detail.output_data,
+                "errorMessage": agent_task_detail.error_message,
+                "hasChildren": has_children,
+                "createTime": agent_task_detail.create_time.strftime("%Y-%m-%d %H:%M:%S"),
+                "updateTime": agent_task_detail.update_time.strftime("%Y-%m-%d %H:%M:%S")
+            }
+            for agent_task_detail, has_children in agent_task_details
+        ]
+        return {
+            "input": agent_task.input,
+            "tools": agent_task.tools,
+            "agentTaskDetails": agent_task_detail_datas
+        }

+ 418 - 77
pqai_agent_server/api_server.py

@@ -1,37 +1,44 @@
 #! /usr/bin/env python
 # -*- coding: utf-8 -*-
 # vim:fenc=utf-8
-import time
+import json
 import logging
-import werkzeug.exceptions
-from flask import Flask, request, jsonify
 from argparse import ArgumentParser
 
+import werkzeug.exceptions
+from flask import Flask, request, jsonify
 from sqlalchemy.orm import sessionmaker
 
+import pqai_agent_server.utils
+from pqai_agent import chat_service, prompt_templates
 from pqai_agent import configs
-
-from pqai_agent import logging_service, chat_service, prompt_templates
-from pqai_agent.agents.message_reply_agent import MessageReplyAgent
+from pqai_agent.chat_service import OpenAICompatible
 from pqai_agent.data_models.agent_configuration import AgentConfiguration
 from pqai_agent.data_models.service_module import ServiceModule
 from pqai_agent.history_dialogue_service import HistoryDialogueService
+from pqai_agent.logging import logger, setup_root_logger
+from pqai_agent.toolkit import global_tool_map
 from pqai_agent.user_manager import MySQLUserManager, MySQLUserRelationManager
 from pqai_agent.utils.db_utils import create_ai_agent_db_engine
 from pqai_agent.utils.prompt_utils import format_agent_profile, format_user_profile
+from pqai_agent_server.agent_task_server import AgentTaskManager
 from pqai_agent_server.const import AgentApiConst
+from pqai_agent_server.const.status_enum import TestTaskStatus
+from pqai_agent_server.const.type_enum import EvaluateType
+from pqai_agent_server.dataset_service import DatasetService
 from pqai_agent_server.models import MySQLSessionManager
-from pqai_agent_server.utils import wrap_response, quit_human_intervention_status
+from pqai_agent_server.task_server import TaskManager
 from pqai_agent_server.utils import (
     run_extractor_prompt,
     run_chat_prompt,
     run_response_type_prompt,
 )
+from pqai_agent_server.utils import wrap_response
 
 app = Flask('agent_api_server')
-logger = logging_service.logger
 const = AgentApiConst()
 
+
 @app.route('/api/listStaffs', methods=['GET'])
 def list_staffs():
     staff_data = app.user_relation_manager.list_staffs()
@@ -91,34 +98,24 @@ def get_dialogue_history():
 
 @app.route('/api/listModels', methods=['GET'])
 def list_models():
-    models = [
-        {
-            'model_type': 'openai_compatible',
-            'model_name': chat_service.VOLCENGINE_MODEL_DEEPSEEK_V3,
-            'display_name': 'DeepSeek V3 on 火山'
-        },
-        {
-            'model_type': 'openai_compatible',
-            'model_name': chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_32K,
-            'display_name': '豆包Pro 32K'
-        },
-        {
-            'model_type': 'openai_compatible',
-            'model_name': chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_1_5,
-            'display_name': '豆包Pro 1.5'
-        },
-        {
-            'model_type': 'openai_compatible',
-            'model_name': chat_service.VOLCENGINE_BOT_DEEPSEEK_V3_SEARCH,
-            'display_name': 'DeepSeek V3联网 on 火山'
-        },
+    models = {
+        "deepseek-chat": chat_service.VOLCENGINE_MODEL_DEEPSEEK_V3,
+        "gpt-4o": chat_service.OPENAI_MODEL_GPT_4o,
+        "gpt-4o-mini": chat_service.OPENAI_MODEL_GPT_4o_mini,
+        "doubao-pro-32k": chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_32K,
+        "doubao-pro-1.5": chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_1_5_32K,
+        "doubao-1.5-vision-pro": chat_service.VOLCENGINE_MODEL_DOUBAO_1_5_VISION_PRO,
+        "openrouter-gemini-2.5-pro": chat_service.OPENROUTER_MODEL_GEMINI_2_5_PRO,
+    }
+    ret_data = [
         {
             'model_type': 'openai_compatible',
-            'model_name': chat_service.VOLCENGINE_MODEL_DOUBAO_1_5_VISION_PRO,
-            'display_name': '豆包1.5视觉理解Pro'
-        },
+            'model_name': model_name,
+            'display_name': f"{model_display_name} ({OpenAICompatible.get_price(model_name).get_cny_brief()})"
+        }
+        for model_display_name, model_name in models.items()
     ]
-    return wrap_response(200, data=models)
+    return wrap_response(200, data=ret_data)
 
 
 @app.route('/api/listScenes', methods=['GET'])
@@ -146,8 +143,8 @@ def get_base_prompt():
     model_map = {
         'greeting': chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_32K,
         'chitchat': chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_32K,
-        'profile_extractor': chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_1_5,
-        'response_type_detector': chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_1_5,
+        'profile_extractor': chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_1_5_32K,
+        'response_type_detector': chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_1_5_32K,
         'custom_debugging': chat_service.VOLCENGINE_BOT_DEEPSEEK_V3_SEARCH
     }
     if scene not in prompt_map:
@@ -178,6 +175,7 @@ def run_prompt():
         logger.error(e)
         return wrap_response(500, msg='Error: {}'.format(e))
 
+
 @app.route('/api/formatForPrompt', methods=['POST'])
 def format_data_for_prompt():
     try:
@@ -297,8 +295,8 @@ def send_message():
     return wrap_response(200, msg="暂不实现功能")
 
 
-@app.route("/api/quitHumanInterventionStatus", methods=["POST"])
-def quit_human_interventions_status():
+@app.route("/api/quitHumanIntervention", methods=["POST"])
+def quit_human_intervention():
     """
     退出人工介入状态
     :return:
@@ -308,9 +306,28 @@ def quit_human_interventions_status():
     user_id = req_data["user_id"]
     if not user_id or not staff_id:
         return wrap_response(404, msg="user_id and staff_id are required")
-    response = quit_human_intervention_status(user_id, staff_id)
+    if pqai_agent_server.utils.common.quit_human_intervention(user_id, staff_id):
+        return wrap_response(200, msg="success")
+    else:
+        return wrap_response(500, msg="error")
+
+
+@app.route("/api/enterHumanIntervention", methods=["POST"])
+def enter_human_intervention():
+    """
+    进入人工介入状态
+    :return:
+    """
+    req_data = request.json
+    staff_id = req_data["staff_id"]
+    user_id = req_data["user_id"]
+    if not user_id or not staff_id:
+        return wrap_response(404, msg="user_id and staff_id are required")
+    if pqai_agent_server.utils.common.enter_human_intervention(user_id, staff_id):
+        return wrap_response(200, msg="success")
+    else:
+        return wrap_response(500, msg="error")
 
-    return wrap_response(200, data=response)
 
 ## Agent管理接口
 @app.route("/api/getNativeAgentList", methods=["GET"])
@@ -332,22 +349,29 @@ def get_native_agent_list():
             query = query.filter(AgentConfiguration.create_user == create_user)
         if update_user:
             query = query.filter(AgentConfiguration.update_user == update_user)
+        total = query.count()
         query = query.offset(offset).limit(int(page_size))
         data = query.all()
-    ret_data = [
-        {
-            'id': agent.id,
-            'name': agent.name,
-            'display_name': agent.display_name,
-            'type': agent.type,
-            'execution_model': agent.execution_model,
-            'create_time': agent.create_time.strftime('%Y-%m-%d %H:%M:%S'),
-            'update_time': agent.update_time.strftime('%Y-%m-%d %H:%M:%S')
-        }
-        for agent in data
-    ]
+    ret_data = {
+        'total': total,
+        'agent_list': [
+            {
+                'id': agent.id,
+                'name': agent.name,
+                'display_name': agent.display_name,
+                'type': agent.type,
+                'execution_model': agent.execution_model,
+                'create_user': agent.create_user,
+                'update_user': agent.update_user,
+                'create_time': agent.create_time.strftime('%Y-%m-%d %H:%M:%S'),
+                'update_time': agent.update_time.strftime('%Y-%m-%d %H:%M:%S')
+            }
+            for agent in data
+        ]
+    }
     return wrap_response(200, data=ret_data)
 
+
 @app.route("/api/getNativeAgentConfiguration", methods=["GET"])
 def get_native_agent_configuration():
     """
@@ -371,14 +395,15 @@ def get_native_agent_configuration():
             'execution_model': agent.execution_model,
             'system_prompt': agent.system_prompt,
             'task_prompt': agent.task_prompt,
-            'tools': agent.tools,
-            'sub_agents': agent.sub_agents,
-            'extra_params': agent.extra_params,
+            'tools': json.loads(agent.tools),
+            'sub_agents': json.loads(agent.sub_agents),
+            'extra_params': json.loads(agent.extra_params),
             'create_time': agent.create_time.strftime('%Y-%m-%d %H:%M:%S'),
             'update_time': agent.update_time.strftime('%Y-%m-%d %H:%M:%S')
         }
         return wrap_response(200, data=data)
 
+
 @app.route("/api/saveNativeAgentConfiguration", methods=["POST"])
 def save_native_agent_configuration():
     """
@@ -393,9 +418,19 @@ def save_native_agent_configuration():
     execution_model = req_data.get('execution_model', None)
     system_prompt = req_data.get('system_prompt', None)
     task_prompt = req_data.get('task_prompt', None)
-    tools = req_data.get('tools', [])
-    sub_agents = req_data.get('sub_agents', [])
+    tools = json.dumps(req_data.get('tools', []))
+    sub_agents = json.dumps(req_data.get('sub_agents', []))
     extra_params = req_data.get('extra_params', {})
+    operate_user = req_data.get('user', None)
+    if isinstance(extra_params, dict):
+        extra_params = json.dumps(extra_params)
+    elif isinstance(extra_params, str):
+        try:
+            json.loads(extra_params)
+        except json.JSONDecodeError:
+            return wrap_response(400, msg='extra_params should be a valid JSON object or string')
+    if not extra_params:
+        extra_params = '{}'
 
     if not name:
         return wrap_response(400, msg='name is required')
@@ -415,6 +450,7 @@ def save_native_agent_configuration():
             agent.tools = tools
             agent.sub_agents = sub_agents
             agent.extra_params = extra_params
+            agent.update_user = operate_user
         else:
             agent = AgentConfiguration(
                 name=name,
@@ -425,37 +461,88 @@ def save_native_agent_configuration():
                 task_prompt=task_prompt,
                 tools=tools,
                 sub_agents=sub_agents,
-                extra_params=extra_params
+                extra_params=extra_params,
+                create_user=operate_user,
+                update_user=operate_user
             )
             session.add(agent)
 
         session.commit()
         return wrap_response(200, msg='Agent configuration saved successfully', data={'id': agent.id})
 
+
+@app.route("/api/deleteNativeAgentConfiguration", methods=["POST"])
+def delete_native_agent_configuration():
+    """
+    删除指定Agent配置(软删除,设置is_delete=1)
+    :return:
+    """
+    req_data = request.json
+    agent_id = req_data.get('agent_id', None)
+    if not agent_id:
+        return wrap_response(400, msg='agent_id is required')
+    try:
+        agent_id = int(agent_id)
+    except ValueError:
+        return wrap_response(400, msg='agent_id must be an integer')
+
+    with app.session_maker() as session:
+        agent = session.query(AgentConfiguration).filter(
+            AgentConfiguration.id == agent_id,
+            AgentConfiguration.is_delete == 0
+        ).first()
+        if not agent:
+            return wrap_response(404, msg='Agent not found')
+        agent.is_delete = 1
+        session.commit()
+        return wrap_response(200, msg='Agent configuration deleted successfully')
+
+
+
 @app.route("/api/getModuleList", methods=["GET"])
 def get_module_list():
     """
-    获取所有的模块列表
+    获取所有的模块列表,支持分页查询
     :return:
     """
+    page = request.args.get('page', 1)
+    page_size = request.args.get('page_size', 50)
+    try:
+        page = int(page)
+        page_size = int(page_size)
+    except Exception as e:
+        return wrap_response(400, msg="Invalid parameter: {}".format(e))
+
+    offset = (page - 1) * page_size
     with app.session_maker() as session:
-        query = session.query(ServiceModule) \
-            .filter(ServiceModule.is_delete == 0)
-        data = query.all()
-    ret_data = [
-        {
-            'id': module.id,
-            'name': module.name,
-            'display_name': module.display_name,
-            'default_agent_type': module.default_agent_type,
-            'default_agent_id': module.default_agent_id,
-            'create_time': module.create_time.strftime('%Y-%m-%d %H:%M:%S'),
-            'update_time': module.update_time.strftime('%Y-%m-%d %H:%M:%S')
-        }
-        for module in data
-    ]
+        query = session.query(
+            ServiceModule,
+            AgentConfiguration.name.label("default_agent_name")
+        ).outerjoin(
+            AgentConfiguration,
+            ServiceModule.default_agent_id == AgentConfiguration.id
+        ).filter(ServiceModule.is_delete == 0)
+        total = query.count()
+        modules = query.offset(offset).limit(page_size).all()
+    ret_data = {
+        'total': total,
+        'module_list': [
+            {
+                'id': module.id,
+                'name': module.name,
+                'display_name': module.display_name,
+                'default_agent_type': module.default_agent_type,
+                'default_agent_id': module.default_agent_id,
+                'default_agent_name': default_agent_name,
+                'create_time': module.create_time.strftime('%Y-%m-%d %H:%M:%S'),
+                'update_time': module.update_time.strftime('%Y-%m-%d %H:%M:%S')
+            }
+            for module, default_agent_name in modules
+        ]
+    }
     return wrap_response(200, data=ret_data)
 
+
 @app.route("/api/getModuleConfiguration", methods=["GET"])
 def get_module_configuration():
     """
@@ -478,10 +565,11 @@ def get_module_configuration():
             'default_agent_type': module.default_agent_type,
             'default_agent_id': module.default_agent_id,
             'create_time': module.create_time.strftime('%Y-%m-%d %H:%M:%S'),
-            'updated_time': module.updated_time.strftime('%Y-%m-%d %H:%M:%S')
+            'update_time': module.update_time.strftime('%Y-%m-%d %H:%M:%S')
         }
         return wrap_response(200, data=data)
 
+
 @app.route("/api/saveModuleConfiguration", methods=["POST"])
 def save_module_configuration():
     """
@@ -520,6 +608,248 @@ def save_module_configuration():
         session.commit()
         return wrap_response(200, msg='Module configuration saved successfully', data={'id': module.id})
 
+@app.route("/api/deleteModuleConfiguration", methods=["POST"])
+def delete_module_configuration():
+    """
+    删除指定模块配置(软删除,设置is_delete=1)
+    :return:
+    """
+    req_data = request.json
+    module_id = req_data.get('module_id', None)
+    if not module_id:
+        return wrap_response(400, msg='module_id is required')
+    try:
+        module_id = int(module_id)
+    except ValueError:
+        return wrap_response(400, msg='module_id must be an integer')
+
+    with app.session_maker() as session:
+        module = session.query(ServiceModule).filter(
+            ServiceModule.id == module_id,
+            ServiceModule.is_delete == 0
+        ).first()
+        if not module:
+            return wrap_response(404, msg='Module not found')
+        module.is_delete = 1
+        session.commit()
+        return wrap_response(200, msg='Module configuration deleted successfully')
+
+@app.route("/api/getToolList", methods=["GET"])
+def get_tool_list():
+    """
+    获取所有的工具列表
+    :return:
+    """
+    tools = []
+    for tool_name, tool in global_tool_map.items():
+        tools.append({
+            'name': tool_name,
+            'description': tool.get_function_description(),
+            'parameters': tool.parameters if hasattr(tool, 'parameters') else {}
+        })
+    return wrap_response(200, data=tools)
+
+@app.route("/api/getModuleAgentTypes", methods=["GET"])
+def get_agent_types():
+    """
+    获取所有的Agent类型
+    :return:
+    """
+    agent_types = [
+        {'type': 0, 'display_name': '原生'},
+        {'type': 1, 'display_name': 'Coze'}
+    ]
+    return wrap_response(200, data=agent_types)
+
+@app.route("/api/getTestTaskList", methods=["GET"])
+def get_test_task_list():
+    """
+       获取单元测试任务列表
+       :return:
+    """
+    page_num = request.args.get("pageNum", const.DEFAULT_PAGE_ID)
+    page_size = request.args.get("pageSize", const.DEFAULT_PAGE_SIZE)
+    try:
+        page_num = int(page_num)
+        page_size = int(page_size)
+    except Exception as e:
+        return wrap_response(404, msg="Invalid parameter: {}".format(e))
+    response = app.task_manager.get_test_task_list(page_num, page_size)
+    return wrap_response(200, data=response)
+
+
+@app.route("/api/getTestTaskConversations", methods=["GET"])
+def get_test_task_conversations():
+    """
+       获取单元测试对话任务列表
+       :return:
+    """
+    task_id = request.args.get("taskId", None)
+    if not task_id:
+        return wrap_response(404, msg='task_id is required')
+    page_num = request.args.get("pageNum", const.DEFAULT_PAGE_ID)
+    page_size = request.args.get("pageSize", const.DEFAULT_PAGE_SIZE)
+    try:
+        page_num = int(page_num)
+        page_size = int(page_size)
+    except Exception as e:
+        return wrap_response(404, msg="Invalid parameter: {}".format(e))
+    response = app.task_manager.get_test_task_conversations(int(task_id), page_num, page_size)
+    return wrap_response(200, data=response)
+
+
+@app.route("/api/createTestTask", methods=["POST"])
+def create_test_task():
+    """
+       创建单元测试任务
+       :return:
+    """
+    req_data = request.json
+    agent_id = req_data.get('agentId', None)
+    module_id = req_data.get('moduleId', None)
+    evaluate_type = req_data.get('evaluateType', None)
+    if not agent_id:
+        return wrap_response(404, msg='agent id is required')
+    if not module_id:
+        return wrap_response(404, msg='module id is required')
+    if not evaluate_type:
+        return wrap_response(404, msg='evaluate_type id is required')
+    app.task_manager.create_task(agent_id, module_id, evaluate_type)
+    return wrap_response(200)
+
+
+@app.route("/api/stopTestTask", methods=["POST"])
+def stop_test_task():
+    """
+       停止单元测试任务
+       :return:
+    """
+    req_data = request.json
+    task_id = req_data.get('taskId', None)
+    if not task_id:
+        return wrap_response(400, msg='task id is required')
+    task = app.task_manager.get_task(task_id)
+    if task.status not in (TestTaskStatus.NOT_STARTED.value, TestTaskStatus.IN_PROGRESS.value):
+        return wrap_response(400, msg='task status is invalid')
+    app.task_manager.cancel_task(task_id)
+    return wrap_response(200)
+
+
+@app.route("/api/resumeTestTask", methods=["POST"])
+def resume_test_task():
+    """
+       恢复停止的单元测试任务
+       :return:
+    """
+    req_data = request.json
+    task_id = req_data.get('taskId', None)
+    if not task_id:
+        return wrap_response(400, msg='task id is required')
+    task = app.task_manager.get_task(task_id)
+    if task.status != TestTaskStatus.CANCELLED.value:
+        return wrap_response(400, msg='task status is invalid')
+    app.task_manager.resume_task(task_id)
+    return wrap_response(200)
+
+
+@app.route("/api/getEvaluateType", methods=["GET"])
+def get_evaluate_type():
+    """
+       获取评估类型
+       :return:
+    """
+    name_desc_list = [
+        {
+            "type": item.value,
+            "desc": item.description
+        }
+        for item in EvaluateType]
+    return wrap_response(code=200, data=name_desc_list)
+
+
+@app.route("/api/getDatasetList", methods=["GET"])
+def get_dataset_list():
+    """
+       获取数据集列表
+       :return:
+    """
+    page_num = request.args.get("pageNum", const.DEFAULT_PAGE_ID)
+    page_size = request.args.get("pageSize", const.DEFAULT_PAGE_SIZE)
+    try:
+        page_num = int(page_num)
+        page_size = int(page_size)
+    except Exception as e:
+        return wrap_response(404, msg="Invalid parameter: {}".format(e))
+    response = app.dataset_service.get_dataset_list(page_num, page_size)
+    return wrap_response(200, data=response)
+
+
+@app.route("/api/getConversationDataList", methods=["GET"])
+def get_conversation_data_list():
+    """
+       获取对话列表
+       :return:
+    """
+    dataset_id = request.args.get("datasetId", None)
+    if not dataset_id:
+        return wrap_response(404, msg='dataset_id is required')
+    page_num = request.args.get("pageNum", const.DEFAULT_PAGE_ID)
+    page_size = request.args.get("pageSize", const.DEFAULT_PAGE_SIZE)
+    try:
+        page_num = int(page_num)
+        page_size = int(page_size)
+    except Exception as e:
+        return wrap_response(404, msg="Invalid parameter: {}".format(e))
+    response = app.dataset_service.get_conversation_data_list(int(dataset_id), page_num, page_size)
+    return wrap_response(200, data=response)
+
+
+@app.route("/api/createAgentTask", methods=["POST"])
+def create_agent_task():
+    """
+       创建agent执行任务
+       :return:
+    """
+    req_data = request.json
+    agent_id = req_data.get('agentId', None)
+    task_prompt = req_data.get('taskPrompt', None)
+    if not agent_id:
+        return wrap_response(404, msg='agent id is required')
+    if not task_prompt:
+        return wrap_response(404, msg='task_prompt is required')
+    app.agent_task_manager.create_task(agent_id, task_prompt)
+    return wrap_response(200)
+
+@app.route("/api/getAgentTaskList", methods=["GET"])
+def get_agent_task_list():
+    """
+       获取单元测试任务列表
+       :return:
+    """
+    page_num = request.args.get("pageNum", const.DEFAULT_PAGE_ID)
+    page_size = request.args.get("pageSize", const.DEFAULT_PAGE_SIZE)
+    try:
+        page_num = int(page_num)
+        page_size = int(page_size)
+    except Exception as e:
+        return wrap_response(404, msg="Invalid parameter: {}".format(e))
+    response = app.agent_task_manager.get_agent_task_list(page_num, page_size)
+    return wrap_response(200, data=response)
+
+
+@app.route("/api/getAgentTaskDetail", methods=["GET"])
+def get_agent_task_detail():
+    """
+       查询agent执行任务详情
+       :return:
+    """
+    agent_task_id = request.args.get("agentTaskId", None)
+    if not agent_task_id:
+        return wrap_response(404, msg='agent_task_id is required')
+    parent_execution_id = request.args.get("parentExecutionId", None)
+    response = app.agent_task_manager.get_agent_task_detail(int(agent_task_id), parent_execution_id)
+    return wrap_response(200, data=response)
+
 @app.errorhandler(werkzeug.exceptions.BadRequest)
 def handle_bad_request(e):
     logger.error(e)
@@ -536,7 +866,7 @@ if __name__ == '__main__':
 
     config = configs.get()
     logging_level = logging.getLevelName(args.log_level)
-    logging_service.setup_root_logger(level=logging_level, logfile_name='agent_api_server.log')
+    setup_root_logger(level=logging_level, logfile_name='agent_api_server.log')
 
     # set db config
     agent_db_config = config['database']['ai_agent']
@@ -547,7 +877,7 @@ if __name__ == '__main__':
     chat_history_db_config = config['storage']['chat_history']
 
     # init user manager
-    user_manager = MySQLUserManager(agent_db_config, growth_db_config, staff_db_config['table'])
+    user_manager = MySQLUserManager(agent_db_config, user_db_config['table'], staff_db_config['table'])
     app.user_manager = user_manager
 
     # init session manager
@@ -559,9 +889,20 @@ if __name__ == '__main__':
         chat_history_table=chat_history_db_config['table']
     )
     app.session_manager = session_manager
-    agent_db_engine = create_ai_agent_db_engine(config['database']['ai_agent'])
+    agent_db_engine = create_ai_agent_db_engine()
     app.session_maker = sessionmaker(bind=agent_db_engine)
 
+    dataset_service = DatasetService(session_maker=sessionmaker(bind=agent_db_engine))
+    app.dataset_service = dataset_service
+
+    task_manager = TaskManager(session_maker=sessionmaker(bind=agent_db_engine), dataset_service=dataset_service)
+    app.task_manager = task_manager
+    app.task_manager.recover_tasks()
+
+    agent_task_manager = AgentTaskManager(session_maker=sessionmaker(bind=agent_db_engine))
+    app.agent_task_manager = agent_task_manager
+    app.agent_task_manager.recover_tasks()
+
     wecom_db_config = config['storage']['user_relation']
     user_relation_manager = MySQLUserRelationManager(
         agent_db_config, growth_db_config,

+ 115 - 0
pqai_agent_server/const/status_enum.py

@@ -0,0 +1,115 @@
+from enum import Enum
+
+
+class TestTaskStatus(Enum):
+    NOT_STARTED = 0
+    IN_PROGRESS = 1
+    COMPLETED = 2
+    CANCELLED = 3
+    FAILED = 4
+    CREATING = 5
+    CREATED_FAIL = 6
+
+    @property
+    def description(self):
+        descriptions = {
+            self.NOT_STARTED: "未开始",
+            self.IN_PROGRESS: "进行中",
+            self.COMPLETED: "已完成",
+            self.CANCELLED: "已取消",
+            self.FAILED: "已失败",
+            self.CREATING: "生成任务中",
+            self.CREATED_FAIL: "生成任务失败"
+        }
+        return descriptions.get(self)
+
+
+# 使用示例
+def get_test_task_status_desc(status_code):
+    try:
+        status = TestTaskStatus(status_code)
+        return status.description
+    except ValueError:
+        return f"未知状态: {status_code}"
+
+
+class TestTaskConversationsStatus(Enum):
+    """任务状态枚举类"""
+    PENDING = 0  # 待执行
+    RUNNING = 1  # 执行中
+    SUCCESS = 2  # 执行成功
+    FAILED = 3  # 执行失败
+    CANCELLED = 4  # 已取消
+    MESSAGE_FAILED = 5  # 消息失败
+    SCORE_FAILED = 6  # 打分失败
+
+    @property
+    def description(self):
+        descriptions = {
+            self.PENDING: "待执行",
+            self.RUNNING: "执行中",
+            self.SUCCESS: "执行成功",
+            self.FAILED: "执行失败",
+            self.CANCELLED: "已取消",
+            self.MESSAGE_FAILED: "消息失败",
+            self.SCORE_FAILED: "打分失败"
+        }
+        return descriptions.get(self)
+
+
+# 使用示例
+def get_test_task_conversations_status_desc(status_code):
+    try:
+        status = TestTaskConversationsStatus(status_code)
+        return status.description
+    except ValueError:
+        return f"未知状态: {status_code}"
+
+
+class AgentTaskStatus(Enum):
+    NOT_STARTED = 0
+    IN_PROGRESS = 1
+    COMPLETED = 2
+    FAILED = 3
+
+    @property
+    def description(self):
+        descriptions = {
+            self.NOT_STARTED: "未开始",
+            self.IN_PROGRESS: "进行中",
+            self.COMPLETED: "已完成",
+            self.FAILED: "已失败"
+        }
+        return descriptions.get(self)
+
+
+# 使用示例
+def get_agent_task_status_desc(status_code):
+    try:
+        status = AgentTaskStatus(status_code)
+        return status.description
+    except ValueError:
+        return f"未知状态: {status_code}"
+
+class AgentTaskDetailStatus(Enum):
+    IN_PROGRESS = 0
+    SUCCESS = 1
+    FAILED = 2
+
+    @property
+    def description(self):
+        descriptions = {
+            self.IN_PROGRESS: "执行中",
+            self.SUCCESS: "成功",
+            self.FAILED: "失败"
+        }
+        return descriptions.get(self)
+
+
+# 使用示例
+def get_agent_task_detail_status_desc(status_code):
+    try:
+        status = AgentTaskDetailStatus(status_code)
+        return status.description
+    except ValueError:
+        return f"未知状态: {status_code}"

+ 46 - 0
pqai_agent_server/const/type_enum.py

@@ -0,0 +1,46 @@
+from enum import Enum
+
+
+class DatasetType(Enum):
+    INTERNAL = 0
+    EXTERNAL = 1
+
+    @property
+    def description(self):
+        descriptions = {
+            self.INTERNAL: "内部",
+            self.EXTERNAL: "外部"
+        }
+        return descriptions.get(self)
+
+
+# 使用示例
+def get_dataset_type_desc(type_code):
+    try:
+        type = DatasetType(type_code)
+        return type.description
+    except ValueError:
+        return f"未知类型: {type_code}"
+
+
+class EvaluateType(Enum):
+    REPLAY = 0
+    PUSH = 1
+
+    @property
+    def description(self):
+        descriptions = {
+            self.REPLAY: "回复",
+            self.PUSH: "唤起"
+        }
+        return descriptions.get(self)
+
+
+# 使用示例
+def get_evaluate_type_desc(type_code):
+    try:
+        type = EvaluateType(type_code)
+        return type.description
+    except ValueError:
+        return f"未知类型: {type_code}"
+

+ 160 - 0
pqai_agent_server/dataset_service.py

@@ -0,0 +1,160 @@
+import json
+from cgitb import reset
+from typing import List
+
+from sqlalchemy import func
+
+from pqai_agent.data_models.dataset_model import DatasetModule
+from pqai_agent.data_models.datasets import Datasets
+from pqai_agent.data_models.internal_conversation_data import InternalConversationData
+from pqai_agent.data_models.qywx_chat_history import QywxChatHistory
+from pqai_agent.data_models.qywx_employee import QywxEmployee
+from pqai_agent_server.const.type_enum import get_dataset_type_desc
+from pqai_agent_server.utils.odps_utils import ODPSUtils
+
+
+class DatasetService:
+    def __init__(self, session_maker):
+        self.session_maker = session_maker
+        odps_utils = ODPSUtils()
+        self.odps_utils = odps_utils
+
+    def get_user_profile_data(self, third_party_user_id: str, date_version: str):
+        sql = f"""
+           SELECT * FROM third_party_user_date_version
+           WHERE dt >= '20250612' and dt < {date_version}  -- 添加分区条件
+           and third_party_user_id = {third_party_user_id}
+           and profile_data_v1 is not null 
+           order by dt desc 
+           limit 1
+           """
+        result_df = self.odps_utils.execute_sql(sql)
+
+        if not result_df.empty:
+            return result_df.iloc[0].to_dict()  # 获取第一行
+        return None
+
+    def get_dataset_module_list_by_module(self, module_id: int):
+        with self.session_maker() as session:
+            return session.query(DatasetModule).filter(DatasetModule.module_id == module_id).filter(
+                DatasetModule.is_delete == 0).all()
+
+    def get_conversation_data_list_by_dataset(self, dataset_id: int):
+        with self.session_maker() as session:
+            return session.query(InternalConversationData).filter(
+                InternalConversationData.dataset_id == dataset_id).filter(
+                InternalConversationData.is_delete == 0).order_by(
+                InternalConversationData.id.asc()
+            ).all()
+
+    def get_conversation_data_by_id(self, conversation_data_id: int):
+        with self.session_maker() as session:
+            return session.query(InternalConversationData).filter(
+                InternalConversationData.id == conversation_data_id).one()
+
+    def get_staff_profile_data(self, third_party_user_id: str):
+        with self.session_maker() as session:
+            return session.query(QywxEmployee).filter(
+                QywxEmployee.third_party_user_id == third_party_user_id).one()
+
+    def get_conversation_list_by_ids(self, conversation_ids: List[int]):
+        with self.session_maker() as session:
+            conversations = session.query(QywxChatHistory).filter(QywxChatHistory.id.in_(conversation_ids)).order_by(
+                QywxChatHistory.id.asc()).all()
+            result = []
+            for conversation in conversations:
+                data = {}
+                data["id"] = conversation.id
+                data["sender"] = conversation.sender
+                data["receiver"] = conversation.receiver
+                data["roomid"] = conversation.roomid
+                data["sendtime"] = conversation.sendtime
+                data["msg_type"] = conversation.msg_type
+                data["content"] = conversation.content
+                result.append(data)
+        return result
+
+    def get_chat_conversation_list_by_ids(self, conversation_ids: List[int], staff_id):
+        result = self.get_conversation_list_by_ids(conversation_ids)
+        conversations = [
+            {
+                "content": conversation['content'],
+                "role": "assistant" if conversation['sender'] == staff_id else "user",
+                "timestamp": conversation['sendtime']
+            } for conversation in result
+        ]
+        return conversations
+
+
+
+    def get_dataset_list(self, page_num: int, page_size: int):
+        with self.session_maker() as session:
+            # 计算偏移量
+            offset = (page_num - 1) * page_size
+            # 查询分页数据
+            result = (session.query(Datasets)
+                      .filter(Datasets.is_delete == 0)
+                      .limit(page_size).offset(offset).all())
+            # 查询总记录数
+            total = session.query(func.count(Datasets.id)).filter(Datasets.is_delete == 0).scalar()
+
+            total_page = total // page_size + 1 if total % page_size > 0 else total // page_size
+            total_page = 1 if total_page <= 0 else total_page
+            response_data = [
+                {
+                    "id": dataset.id,
+                    "name": dataset.name,
+                    "type": get_dataset_type_desc(dataset.type),
+                    "description": dataset.description,
+                    "createTime": dataset.create_time.strftime("%Y-%m-%d %H:%M:%S"),
+                    "updateTime": dataset.update_time.strftime("%Y-%m-%d %H:%M:%S")
+                }
+                for dataset in result
+            ]
+            return {
+                "currentPage": page_num,
+                "pageSize": page_size,
+                "totalSize": total_page,
+                "total": total,
+                "list": response_data,
+            }
+
+    def get_conversation_data_list(self, dataset_id: int, page_num: int, page_size: int):
+        with self.session_maker() as session:
+            # 计算偏移量
+            offset = (page_num - 1) * page_size
+            # 查询分页数据
+            result = (session.query(InternalConversationData)
+                      .filter(InternalConversationData.dataset_id == dataset_id)
+                      .filter(InternalConversationData.is_delete == 0)
+                      .limit(page_size).offset(offset).all())
+            # 查询总记录数
+            total = session.query(func.count(InternalConversationData.id)).filter(
+                InternalConversationData.is_delete == 0).scalar()
+
+            total_page = total // page_size + 1 if total % page_size > 0 else total // page_size
+            total_page = 1 if total_page <= 0 else total_page
+            response_data = []
+            for conversation_data in result:
+                data = {}
+                data["id"] = conversation_data.id
+                data["datasetId"] = conversation_data.dataset_id
+                data["staff"] = self.get_staff_profile_data(conversation_data.staff_id).agent_profile
+                data["user"] = self.get_user_profile_data(conversation_data.user_id,
+                                                          conversation_data.version_date.replace("-", ""))[
+                    'profile_data_v1']
+                data["conversation"] = self.get_conversation_list_by_ids(json.loads(conversation_data.conversation))
+                data["content"] = conversation_data.content
+                data["sendTime"] = conversation_data.send_time
+                data["sendType"] = conversation_data.send_type
+                data["userActiveRate"] = conversation_data.user_active_rate
+                data["createTime"]: conversation_data.create_time.strftime("%Y-%m-%d %H:%M:%S")
+                data["updateTime"]: conversation_data.update_time.strftime("%Y-%m-%d %H:%M:%S")
+                response_data.append(data)
+            return {
+                "currentPage": page_num,
+                "pageSize": page_size,
+                "totalSize": total_page,
+                "total": total,
+                "list": response_data,
+            }

+ 571 - 0
pqai_agent_server/evaluate_agent.py

@@ -0,0 +1,571 @@
+import json
+
+from enum import IntEnum
+from typing import Dict, List, Any
+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
+
+
+class TaskType(IntEnum):
+    """Evaluation scenario: 0 = reply, 1 = proactive push."""
+
+    REPLY = 0
+    PUSH = 1
+
+
+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.3 客服是否能及时结束对话  
+  判分要点:在用户谢绝后礼貌收尾,不强行续聊。  
+  正例:“有需要随时联系。”  
+  反例:用户已“好的谢谢”,仍连发优惠券。  
+
+### 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 身份的内容
+"""
+
+
+reply_index = {
+    "1.1": "客服是否识别用户核心意图",
+    "1.2": "客服是否识别上文关键信息",
+    "1.3": "客服是否理解歧义词或模糊表达",
+    "1.4": "客服是否理解用户发送的表情 / 图片",
+    "1.5": "客服是否理解用户发送的语音 / 方言(转写内容)",
+    "2.1": "客服的回复是否与用户意图相关",
+    "2.2": "客服的回复是否清晰简洁",
+    "2.3": "客服的回复是否流畅",
+    "2.4": "客服回复的语法是否规范",
+    "2.5": "客服的回复是否具有机械性",
+    "3.1": "客服是否正确理解代词",
+    "3.2": "客服是否延续上文话题",
+    "3.3": "客服是否能及时结束对话",
+    "4.1": "客服回复的消息是否超出客服角色认知范围",
+    "4.2": "客服是否使用错误时代背景或过时词汇",
+    "4.3": "客服回复的消息是否展现出与角色设定一致的知识/经验",
+    "5.1": "客服言行是否体现预设性格",
+    "5.2": "客服价值观与道德是否一致",
+    "6.1": "客服的用词语法是否匹配身份背景",
+    "6.2": "客服的语气是否保持稳定",
+    "6.3": "客服是否保持客服角色表达习惯",
+    "7.1": "客服回复是否体现其核心目标",
+    "8.1": "客服是否遵循角色与用户的互动模式",
+    "8.2": "客服是否正确理解自己身份",
+    "8.3": "客服是否回复超越用户可理解范围",
+}
+
+push_index = {
+    "1.1": "客服是否感知用户情绪",
+    "2.1": "客服是否延续上文话题",
+    "2.2": "客服是否记住上文信息",
+    "3.1": "客服推送的消息是否不超出角色认知范围",
+    "3.2": "客服推送的消息用到的词汇是否符合当前时代",
+    "3.3": "客服推送消息的知识是否知识符合角色设定",
+    "4.1": "客服推送的消息是否符合同一性格",
+    "4.2": "客服推送的消息是否符合正确的价值观、道德观",
+    "5.1": "客服的用词语法是否匹配身份背景学历职业",
+    "5.2": "客服的语气是否保持稳定",
+    "5.3": "客服是否保持角色表达习惯",
+    "5.4": "客服推送消息语言风格是否匹配其年龄 & 性别(禁忌词检测,重点审)",
+    "5.5": "客服推送的消息是否符合其职业典型",
+    "6.1": "客服是否遵循角色与用户互动模式",
+    "6.2": "客服是否自身身份准确",
+    "6.3": "客服推送内容是否不超出用户理解范围",
+    "6.4": "客服是否不透露 AI 身份",
+    "7.1": "客服的唤起消息是否多样、非机械",
+    "7.2": "客服推送消息是否关注用户兴趣 / 地域",
+    "7.3": "客服推送消息是否解决上文遗留的合理需求(如有)",
+    "7.4": "客服推送消息是否明确表现继续聊天意图",
+    "7.5": "客服推送节日祝福时间节点是否合适",
+}
+
+PROMPT_TEMPLATE_MAP: Dict[TaskType, str] = {
+    TaskType.REPLY: REPLY_MESSAGE_EVALUATE_PROMPT,
+    TaskType.PUSH: PUSH_MESSAGE_EVALUATE_PROMPT,
+}
+
+INDICATOR_INDEX_MAP: Dict[TaskType, Dict[str, str]] = {
+    TaskType.REPLY: reply_index,
+    TaskType.PUSH: push_index,
+}
+
+
+def fetch_llm_completion(prompt, output_type="text") -> str | Dict[str, Dict]:
+    """
+    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 _build_prompt(task: Dict[str, Any], task_type: TaskType) -> str:
+    """Assemble the prompt for LLM completion."""
+    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["message"],
+        "send_time": task["send_time"],
+        "agent_profile": format_agent_profile(task["agent_profile"]),
+        "user_profile": format_user_profile(task["user_profile"]),
+    }
+
+    template = PROMPT_TEMPLATE_MAP[task_type]
+    return template.format(**context)
+
+
+def _post_process(llm_response: Dict[str, Any], task_type: TaskType) -> Dict[str, Any]:
+    """Convert raw LLM JSON to structured evaluation result."""
+    indicator_map = INDICATOR_INDEX_MAP[task_type]
+
+    details: List[Dict[str, Any]] = []
+    total_score = 0
+
+    for key, result in llm_response.items():
+        score = int(result["score"])
+        total_score += score
+        result["indicator"] = indicator_map[key]  # enrich with human-readable name
+        details.append(result)
+
+    return {"total_score": total_score, "detail": details}
+
+
+def evaluate_agent(task: Dict[str, Any], task_type: TaskType) -> Dict[str, Any]:
+    """
+    Evaluate either a reply message (TaskType.REPLY) or a proactive push
+    (TaskType.PUSH) and return aggregated scoring information.
+    """
+    prompt = _build_prompt(task, task_type)
+    llm_json = fetch_llm_completion(prompt, output_type="json") or {}
+    return _post_process(llm_json, task_type) if llm_json else {}

+ 527 - 0
pqai_agent_server/task_server.py

@@ -0,0 +1,527 @@
+import concurrent.futures
+import json
+import threading
+from concurrent.futures import ThreadPoolExecutor
+from datetime import datetime
+from typing import Dict
+
+from sqlalchemy import func
+
+from pqai_agent.agents.multimodal_chat_agent import MultiModalChatAgent
+from pqai_agent.data_models.agent_configuration import AgentConfiguration
+from pqai_agent.data_models.agent_test_task import AgentTestTask
+from pqai_agent.data_models.agent_test_task_conversations import AgentTestTaskConversations
+from pqai_agent.data_models.service_module import ServiceModule
+from pqai_agent.logging import logger
+from pqai_agent_server.const.status_enum import TestTaskConversationsStatus, TestTaskStatus, get_test_task_status_desc, \
+    get_test_task_conversations_status_desc
+from pqai_agent_server.evaluate_agent import evaluate_agent
+
+
+class TaskManager:
+    """任务管理器"""
+
+    def __init__(self, session_maker, dataset_service):
+        self.session_maker = session_maker
+        self.dataset_service = dataset_service
+        self.task_events = {}  # 任务ID -> Event (用于取消任务)
+        self.task_locks = {}  # 任务ID -> Lock (用于任务状态同步)
+        self.running_tasks = set()
+        self.executor = ThreadPoolExecutor(max_workers=20, thread_name_prefix='TaskWorker')
+        self.create_task_executor = ThreadPoolExecutor(max_workers=10, thread_name_prefix='CreateTaskWorker')
+        self.task_futures = {}  # 任务ID -> Future
+
+    def get_test_task_list(self, page_num: int, page_size: int) -> Dict:
+        with self.session_maker() as session:
+            # 计算偏移量
+            offset = (page_num - 1) * page_size
+            # 查询分页数据
+            result = (session.query(AgentTestTask, AgentConfiguration)
+                      .outerjoin(AgentConfiguration, AgentTestTask.agent_id == AgentConfiguration.id)
+                      .limit(page_size).offset(offset).all())
+            # 查询总记录数
+            total = session.query(func.count(AgentTestTask.id)).scalar()
+
+            total_page = total // page_size + 1 if total % page_size > 0 else total // page_size
+            total_page = 1 if total_page <= 0 else total_page
+            response_data = [
+                {
+                    "id": agent_test_task.id,
+                    "agentName": agent_configuration.name,
+                    "createUser": agent_test_task.create_user,
+                    "updateUser": agent_test_task.update_user,
+                    "statusName": get_test_task_status_desc(agent_test_task.status),
+                    "createTime": agent_test_task.create_time.strftime("%Y-%m-%d %H:%M:%S"),
+                    "updateTime": agent_test_task.update_time.strftime("%Y-%m-%d %H:%M:%S")
+                }
+                for agent_test_task, agent_configuration in result
+            ]
+            return {
+                "currentPage": page_num,
+                "pageSize": page_size,
+                "totalSize": total_page,
+                "total": total,
+                "list": response_data,
+            }
+
+    def get_test_task_conversations(self, task_id: int, page_num: int, page_size: int) -> Dict:
+        with self.session_maker() as session:
+            # 计算偏移量
+            offset = (page_num - 1) * page_size
+            # 查询分页数据
+            result = (session.query(AgentTestTaskConversations, AgentConfiguration)
+                      .outerjoin(AgentConfiguration, AgentTestTaskConversations.agent_id == AgentConfiguration.id)
+                      .filter(AgentTestTaskConversations.task_id == task_id)
+                      .limit(page_size).offset(offset).all())
+            # 查询总记录数
+            total = session.query(func.count(AgentTestTaskConversations.id)).filter(
+                AgentTestTaskConversations.task_id == task_id).scalar()
+
+            total_page = total // page_size + 1 if total % page_size > 0 else total // page_size
+            total_page = 1 if total_page <= 0 else total_page
+            response_data = [
+                {
+                    "id": agent_test_task_conversation.id,
+                    "agentName": agent_configuration.name,
+                    "input": MultiModalChatAgent.compose_dialogue(json.loads(agent_test_task_conversation.input))
+                    if agent_test_task_conversation.input and agent_test_task_conversation.input.strip()
+                    else None,
+                    "output": agent_test_task_conversation.output,
+                    "score": agent_test_task_conversation.score,
+                    "statusName": get_test_task_conversations_status_desc(agent_test_task_conversation.status),
+                    "createTime": agent_test_task_conversation.create_time.strftime("%Y-%m-%d %H:%M:%S"),
+                    "updateTime": agent_test_task_conversation.update_time.strftime("%Y-%m-%d %H:%M:%S")
+                }
+                for agent_test_task_conversation, agent_configuration in result
+            ]
+            return {
+                "currentPage": page_num,
+                "pageSize": page_size,
+                "totalSize": total_page,
+                "total": total,
+                "list": response_data,
+            }
+
+    def create_task(self, agent_id: int, module_id: int, evaluate_type: int) -> Dict:
+        """创建新任务"""
+        with self.session_maker() as session:
+            agent_test_task = AgentTestTask(agent_id=agent_id, module_id=module_id, evaluate_type=evaluate_type,
+                                            status=TestTaskStatus.CREATING.value)
+            session.add(agent_test_task)
+            session.commit()  # 显式提交
+            task_id = agent_test_task.id
+        # 异步执行创建任务
+        self.create_task_executor.submit(self._generate_agent_test_task_conversation_batch, task_id, agent_id,
+                                         module_id)
+        return self.get_task(task_id)
+
+    def _generate_agent_test_task_conversation_batch(self, task_id: int, agent_id: int, module_id: int):
+        """异步生成子任务"""
+        try:
+            # 获取数据集列表
+            dataset_module_list = self.dataset_service.get_dataset_module_list_by_module(module_id)
+
+            # 批量处理数据集 - 减少数据库交互
+            batch_size = 100  # 每批处理100个子任务
+            agent_test_task_conversation_batch = []
+
+            for dataset_module in dataset_module_list:
+                # 获取对话数据列表
+                conversation_datas = self.dataset_service.get_conversation_data_list_by_dataset(
+                    dataset_module.dataset_id)
+
+                for conversation_data in conversation_datas:
+                    # 创建子任务对象
+                    agent_test_task_conversation = AgentTestTaskConversations(
+                        task_id=task_id,
+                        agent_id=agent_id,
+                        dataset_id=dataset_module.dataset_id,
+                        conversation_id=conversation_data.id,
+                        status=TestTaskConversationsStatus.PENDING.value
+                    )
+                    agent_test_task_conversation_batch.append(agent_test_task_conversation)
+
+                    # 批量提交
+                    if len(agent_test_task_conversation_batch) >= batch_size:
+                        self.save_agent_test_task_conversation_batch(agent_test_task_conversation_batch)
+                        agent_test_task_conversation_batch = []
+
+            # 提交剩余的子任务
+            if agent_test_task_conversation_batch:
+                self.save_agent_test_task_conversation_batch(agent_test_task_conversation_batch)
+
+            # 更新主任务状态为未开始
+            self.update_task_status(task_id, TestTaskStatus.NOT_STARTED.value)
+
+            # 自动提交任务执行
+            self._execute_task(task_id)
+
+        except Exception as e:
+            logger.error(f"生成子任务失败: {str(e)}")
+            # 更新任务状态为失败
+            self.update_task_status(task_id, TestTaskStatus.CREATED_FAIL.value)
+
+    def save_agent_test_task_conversation_batch(self, agent_test_task_conversation_batch: list):
+        """批量保存子任务到数据库"""
+        try:
+            with self.session_maker() as session:
+                with session.begin():
+                    session.add_all(agent_test_task_conversation_batch)
+        except Exception as e:
+            logger.error(e)
+
+    def get_agent_configuration_by_task_id(self, task_id: int):
+        """获取指定任务ID对应的Agent配置信息"""
+        with self.session_maker() as session:
+            return session.query(AgentConfiguration) \
+                .join(AgentTestTask, AgentTestTask.agent_id == AgentConfiguration.id) \
+                .filter(AgentTestTask.id == task_id) \
+                .one_or_none()  # 返回单个对象或None(如果未找到)
+
+    def get_service_module_by_task_id(self, task_id: int):
+        """获取指定任务ID对应的Agent配置信息"""
+        with self.session_maker() as session:
+            return session.query(ServiceModule) \
+                .join(AgentTestTask, AgentTestTask.module_id == ServiceModule.id) \
+                .filter(AgentTestTask.id == task_id) \
+                .one_or_none()  # 返回单个对象或None(如果未找到)
+
+    def get_task(self, task_id: int):
+        """获取任务信息"""
+        with self.session_maker() as session:
+            return session.query(AgentTestTask).filter(AgentTestTask.id == task_id).one()
+
+    def get_in_progress_task(self):
+        """获取执行中任务"""
+        with self.session_maker() as session:
+            return session.query(AgentTestTask).filter(AgentTestTask.status.in_([
+                TestTaskStatus.NOT_STARTED.value,
+                TestTaskStatus.IN_PROGRESS.value
+            ])).all()
+
+    def get_creating_task(self):
+        """获取执行中任务"""
+        with self.session_maker() as session:
+            return session.query(AgentTestTask).filter(AgentTestTask.status == TestTaskStatus.CREATING.value).all()
+
+    def get_task_conversations(self, task_id: int):
+        """获取任务的所有子任务"""
+        with self.session_maker() as session:
+            return session.query(AgentTestTaskConversations).filter(AgentTestTaskConversations.task_id == task_id).all()
+
+    def del_task_conversations(self, task_id: int):
+        with self.session_maker() as session:
+            session.query(AgentTestTaskConversations).filter(AgentTestTaskConversations.task_id == task_id).delete()
+            # 提交事务生效
+            session.commit()
+
+    def get_pending_task_conversations(self, task_id: int):
+        """获取待处理的子任务"""
+        with self.session_maker() as session:
+            return session.query(AgentTestTaskConversations).filter(
+                AgentTestTaskConversations.task_id == task_id).filter(
+                AgentTestTaskConversations.status.in_([
+                    TestTaskConversationsStatus.PENDING.value,
+                    TestTaskConversationsStatus.RUNNING.value
+                ])).all()
+
+    def update_task_status(self, task_id: int, status: int):
+        """更新任务状态"""
+        with self.session_maker() as session:
+            session.query(AgentTestTask).filter(AgentTestTask.id == task_id).update(
+                {"status": status, "update_time": datetime.now()})
+            session.commit()
+
+    def update_task_conversations_status(self, task_conversations_id: int, status: int):
+        """更新子任务状态"""
+        with self.session_maker() as session:
+            session.query(AgentTestTaskConversations).filter(
+                AgentTestTaskConversations.id == task_conversations_id).update(
+                {"status": status, "update_time": datetime.now()})
+            session.commit()
+
+    def update_task_conversations_res(self, task_conversations_id: int, status: int, input: str, output: str,
+                                      score: str):
+        """更新子任务结果"""
+        with self.session_maker() as session:
+            session.query(AgentTestTaskConversations).filter(
+                AgentTestTaskConversations.id == task_conversations_id).update(
+                {"status": status, "input": input, "output": output, "score": score, "update_time": datetime.now()})
+            session.commit()
+
+    def cancel_task(self, task_id: int):
+        """取消任务(带事务支持)"""
+        # 设置取消事件
+        if task_id in self.task_events:
+            self.task_events[task_id].set()
+        # 如果任务正在执行,尝试取消Future
+        if task_id in self.task_futures:
+            self.task_futures[task_id].cancel()
+
+        with self.session_maker() as session:
+            with session.begin():
+                session.query(AgentTestTask).filter(AgentTestTask.id == task_id).update(
+                    {"status": TestTaskStatus.CANCELLED.value})
+                session.query(AgentTestTaskConversations).filter(AgentTestTaskConversations.task_id == task_id).filter(
+                    AgentTestTaskConversations.status == TestTaskConversationsStatus.PENDING.value).update(
+                    {"status": TestTaskConversationsStatus.CANCELLED.value})
+                session.commit()
+
+    def resume_task(self, task_id: int) -> bool:
+        """恢复已取消的任务"""
+        task = self.get_task(task_id)
+        if not task or task.status != TestTaskStatus.CANCELLED.value:
+            return False
+
+        with self.session_maker() as session:
+            with session.begin():
+                session.query(AgentTestTask).filter(AgentTestTask.id == task_id).update(
+                    {"status": TestTaskStatus.NOT_STARTED.value})
+                session.query(AgentTestTaskConversations).filter(AgentTestTaskConversations.task_id == task_id).filter(
+                    AgentTestTaskConversations.status == TestTaskConversationsStatus.CANCELLED.value).update(
+                    {"status": TestTaskConversationsStatus.PENDING.value})
+                session.commit()
+
+        # 重新执行任务
+        self._execute_task(task_id)
+        logger.info(f"Resumed task {task_id}")
+        return True
+
+    def recover_tasks(self):
+        """服务启动时恢复未完成的任务"""
+
+        creating = self.get_creating_task()
+        for task in creating:
+            task_id = task.id
+            agent_id = task.agent_id
+            module_id = task.module_id
+            self.del_task_conversations(task_id)
+            # 重新提交任务
+            # 异步执行创建任务
+            self.create_task_executor.submit(self._generate_agent_test_task_conversation_batch, task_id, agent_id,
+                                             module_id)
+
+        # 获取所有进行中的任务ID(根据实际状态定义查询)
+        in_progress_tasks = self.get_in_progress_task()
+
+        for task in in_progress_tasks:
+            task_id = task.id
+            # 重新提交任务
+            self._execute_task(task_id)
+
+    def _execute_task(self, task_id: int):
+        """提交任务到线程池执行"""
+        # 确保任务状态一致性
+        if task_id in self.running_tasks:
+            return
+
+        # 创建任务事件和锁
+        if task_id not in self.task_events:
+            self.task_events[task_id] = threading.Event()
+        if task_id not in self.task_locks:
+            self.task_locks[task_id] = threading.Lock()
+
+        # 提交到线程池
+        future = self.executor.submit(self._process_task, task_id)
+        self.task_futures[task_id] = future
+
+        # 标记任务为运行中
+        with self.task_locks[task_id]:
+            self.running_tasks.add(task_id)
+
+    def _process_task(self, task_id: int):
+        """处理任务的所有子任务(并发执行)"""
+        try:
+            self.update_task_status(task_id, TestTaskStatus.IN_PROGRESS.value)
+            task_conversations = self.get_pending_task_conversations(task_id)
+
+            if not task_conversations:
+                self.update_task_status(task_id, TestTaskStatus.COMPLETED.value)
+                return
+
+            agent_configuration = self.get_agent_configuration_by_task_id(task_id)
+            query_prompt_template = agent_configuration.task_prompt
+
+            task = self.get_task(task_id)
+
+            # 使用线程池执行子任务
+            with ThreadPoolExecutor(max_workers=8) as executor:  # 可根据需要调整并发数
+                futures = {}
+                for task_conversation in task_conversations:
+                    if self.task_events[task_id].is_set():
+                        break  # 检查任务取消事件
+
+                    # 提交子任务到线程池
+                    future = executor.submit(
+                        self._process_single_conversation,
+                        task_id,
+                        task,
+                        task_conversation,
+                        query_prompt_template,
+                        agent_configuration
+                    )
+                    futures[future] = task_conversation.id
+
+                # 等待所有子任务完成或取消
+                for future in concurrent.futures.as_completed(futures):
+                    conv_id = futures[future]
+                    try:
+                        future.result()  # 获取结果(如有异常会在此抛出)
+                    except Exception as e:
+                        logger.error(f"Subtask {conv_id} failed: {str(e)}")
+                        self.update_task_conversations_status(
+                            conv_id,
+                            TestTaskConversationsStatus.FAILED.value
+                        )
+
+            # 检查最终任务状态
+            self._update_final_task_status(task_id)
+
+        except Exception as e:
+            logger.error(f"Error processing task {task_id}: {str(e)}")
+            self.update_task_status(task_id, TestTaskStatus.FAILED.value)
+        finally:
+            self._cleanup_task_resources(task_id)
+
+    def _process_single_conversation(self, task_id, task, task_conversation, query_prompt_template,
+                                     agent_configuration):
+        """处理单个对话子任务(线程安全)"""
+        # 检查任务是否被取消
+        if self.task_events[task_id].is_set():
+            return
+
+        # 更新子任务状态
+        if task_conversation.status == TestTaskConversationsStatus.PENDING.value:
+            self.update_task_conversations_status(
+                task_conversation.id,
+                TestTaskConversationsStatus.RUNNING.value
+            )
+
+        try:
+            # 创建独立的agent实例(确保线程安全)
+            agent = MultiModalChatAgent(
+                model=agent_configuration.execution_model,
+                system_prompt=agent_configuration.system_prompt,
+                tools=json.loads(agent_configuration.tools)
+            )
+
+            # 获取对话数据
+            conversation_data = self.dataset_service.get_conversation_data_by_id(
+                task_conversation.conversation_id)
+            user_profile_data = self.dataset_service.get_user_profile_data(
+                conversation_data.user_id,
+                conversation_data.version_date.replace("-", ""))
+            user_profile = json.loads(user_profile_data['profile_data_v1'])
+            avatar = user_profile_data['iconurl']
+            staff_profile = self.dataset_service.get_staff_profile_data(
+                conversation_data.staff_id).agent_profile
+            conversations = self.dataset_service.get_chat_conversation_list_by_ids(
+                json.loads(conversation_data.conversation),
+                conversation_data.staff_id
+            )
+            conversations = sorted(conversations, key=lambda i: i['timestamp'], reverse=False)
+
+            # 生成推送消息
+            last_timestamp = int(conversations[-1]["timestamp"])
+            match task.evaluate_type:
+                case 0:
+                    send_timestamp = int(last_timestamp / 1000) + 10
+                case 1:
+                    send_timestamp = int(last_timestamp / 1000) + 24 * 3600
+                case _:
+                    raise ValueError("evaluate_type must be 0 or 1")
+            send_time = datetime.fromtimestamp(send_timestamp).strftime('%Y-%m-%d %H:%M:%S')
+            message = agent._generate_message(
+                context={
+                    "formatted_staff_profile": staff_profile,
+                    "nickname": user_profile['nickname'],
+                    "name": user_profile['name'],
+                    "avatar": avatar,
+                    "preferred_nickname": user_profile['preferred_nickname'],
+                    "gender": user_profile['gender'],
+                    "age": user_profile['age'],
+                    "region": user_profile['region'],
+                    "health_conditions": user_profile['health_conditions'],
+                    "medications": user_profile['medications'],
+                    "interests": user_profile['interests'],
+                    "current_datetime": send_time
+                },
+                dialogue_history=conversations,
+                query_prompt_template=query_prompt_template
+            )
+
+            if not message:
+                self.update_task_conversations_status(
+                    task_conversation.id,
+                    TestTaskConversationsStatus.MESSAGE_FAILED.value
+                )
+                return
+
+            param = {}
+            param["dialogue_history"] = conversations
+            param["message"] = message
+            param["send_time"] = send_time
+            param["agent_profile"] = json.loads(staff_profile)
+            param["user_profile"] = user_profile
+            score = evaluate_agent(param, task.evaluate_type)
+
+            if not score:
+                self.update_task_conversations_status(
+                    task_conversation.id,
+                    TestTaskConversationsStatus.SCORE_FAILED.value
+                )
+                return
+
+            # 更新子任务结果
+            self.update_task_conversations_res(
+                task_conversation.id,
+                TestTaskConversationsStatus.SUCCESS.value,
+                json.dumps(conversations, ensure_ascii=False),
+                json.dumps(message, ensure_ascii=False),
+                json.dumps(score)
+            )
+
+        except Exception as e:
+            logger.error(f"Subtask {task_conversation.id} failed: {str(e)}")
+            self.update_task_conversations_status(
+                task_conversation.id,
+                TestTaskConversationsStatus.FAILED.value
+            )
+            raise  # 重新抛出异常以便主线程捕获
+
+    def _update_final_task_status(self, task_id):
+        """更新任务的最终状态"""
+        task_conversations = self.get_task_conversations(task_id)
+        all_completed = all(
+            conv.status in (TestTaskConversationsStatus.SUCCESS.value,
+                            TestTaskConversationsStatus.FAILED.value)
+            for conv in task_conversations
+        )
+
+        if all_completed:
+            self.update_task_status(task_id, TestTaskStatus.COMPLETED.value)
+            logger.info(f"Task {task_id} completed")
+        elif not any(
+                conv.status in (TestTaskConversationsStatus.PENDING.value,
+                                TestTaskConversationsStatus.RUNNING.value)
+                for conv in task_conversations
+        ):
+            current_status = self.get_task(task_id).status
+            if current_status != TestTaskStatus.CANCELLED.value:
+                new_status = TestTaskStatus.COMPLETED.value if all_completed else TestTaskStatus.CANCELLED.value
+                self.update_task_status(task_id, new_status)
+
+    def _cleanup_task_resources(self, task_id):
+        """清理任务资源(线程安全)"""
+        with self.task_locks[task_id]:
+            if task_id in self.running_tasks:
+                self.running_tasks.remove(task_id)
+            if task_id in self.task_events:
+                del self.task_events[task_id]
+            if task_id in self.task_futures:
+                del self.task_futures[task_id]
+
+    def shutdown(self):
+        """关闭执行器"""
+        self.executor.shutdown(wait=False)
+        logger.info("Task executor shutdown")

+ 0 - 1
pqai_agent_server/utils/__init__.py

@@ -1,5 +1,4 @@
 from .common import wrap_response
-from .common import quit_human_intervention_status
 
 from .prompt_util import (
     run_openai_chat,

+ 14 - 3
pqai_agent_server/utils/common.py

@@ -11,7 +11,18 @@ def wrap_response(code, msg=None, data=None):
     return jsonify(resp)
 
 
-def quit_human_intervention_status(user_id, staff_id):
+def quit_human_intervention(user_id, staff_id) -> bool:
     url = f"http://ai-wechat-hook-internal.piaoquantv.com/manage/insertEvent?sender={user_id}&receiver={staff_id}&type=103&content=SYSTEM"
-    response = requests.get(url, timeout=20)
-    return response.json()
+    response = requests.post(url, timeout=20, headers={"Content-Type": "application/json"})
+    if response.status_code == 200 and response.json().get("code") == 0:
+        return True
+    else:
+        return False
+
+def enter_human_intervention(user_id, staff_id) -> bool:
+    url = f"http://ai-wechat-hook-internal.piaoquantv.com/manage/insertEvent?sender={user_id}&receiver={staff_id}&type=104&content=SYSTEM"
+    response = requests.post(url, timeout=20, headers={"Content-Type": "application/json"})
+    if response.status_code == 200 and response.json().get("code") == 0:
+        return True
+    else:
+        return False

+ 86 - 0
pqai_agent_server/utils/odps_utils.py

@@ -0,0 +1,86 @@
+import logging
+
+import pandas as pd
+from odps import ODPS
+
+
+class ODPSUtils:
+    """ODPS操作工具类,封装常用的ODPS操作"""
+
+    # 默认配置
+    DEFAULT_ACCESS_ID = 'LTAIWYUujJAm7CbH'
+    DEFAULT_ACCESS_KEY = 'RfSjdiWwED1sGFlsjXv0DlfTnZTG1P'
+    DEFAULT_PROJECT = 'loghubods'
+    DEFAULT_ENDPOINT = 'http://service.cn.maxcompute.aliyun.com/api'
+    DEFAULT_LOG_LEVEL = logging.INFO
+    DEFAULT_LOG_FILE = None
+
+    def __init__(self,
+                 access_id='LTAIWYUujJAm7CbH',
+                 access_key='RfSjdiWwED1sGFlsjXv0DlfTnZTG1P',
+                 project='loghubods',
+                 endpoint='http://service.cn.maxcompute.aliyun.com/api'):
+        """
+        初始化ODPS连接
+
+        参数:
+            access_id: ODPS访问ID
+            access_key: ODPS访问密钥
+            project: ODPS项目名
+            endpoint: ODPS服务地址
+            log_level: 日志级别,默认为INFO
+            log_file: 日志文件路径,默认为None(不写入文件)
+        """
+        # 使用默认值或用户提供的值
+        self.access_id = access_id
+        self.access_key = access_key
+        self.project = project
+        self.endpoint = endpoint
+
+        # 初始化ODPS连接
+        self.odps = None
+        self.connect()
+
+    def connect(self):
+        """建立ODPS连接"""
+        try:
+            self.odps = ODPS(self.access_id, self.access_key,
+                             project=self.project, endpoint=self.endpoint)
+            return True
+        except Exception as e:
+            return False
+
+    def execute_sql(self, sql, max_wait_time=3600, tunnel=True):
+        """
+        执行SQL查询并返回结果
+
+        参数:
+            sql: SQL查询语句
+            max_wait_time: 最大等待时间(秒)
+            tunnel: 是否使用Tunnel下载结果,默认为True
+
+        返回:
+            pandas DataFrame包含查询结果
+        """
+        if not self.odps:
+            return None
+
+        try:
+            with self.odps.execute_sql(sql).open_reader(tunnel=tunnel) as reader:
+                # 转换结果为DataFrame
+                records = []
+                for record in reader:
+                    records.append(dict(record))
+
+                if records:
+                    df = pd.DataFrame(records)
+                    return df
+                else:
+                    return pd.DataFrame()
+        except Exception as e:
+            return None
+
+
+
+
+

+ 3 - 4
pqai_agent_server/utils/prompt_util.py

@@ -5,15 +5,14 @@ from typing import List, Dict
 
 from openai import OpenAI
 
-from pqai_agent import logging_service, chat_service
+from pqai_agent import chat_service
+from pqai_agent.logging import logger
 from pqai_agent.response_type_detector import ResponseTypeDetector
 from pqai_agent.user_profile_extractor import UserProfileExtractor
 from pqai_agent.dialogue_manager import DialogueManager
 from pqai_agent.mq_message import MessageType
 from pqai_agent.utils.prompt_utils import format_agent_profile
 
-logger = logging_service.logger
-
 
 def compose_openai_chat_messages_no_time(dialogue_history, multimodal=False):
     messages = []
@@ -44,7 +43,7 @@ def compose_openai_chat_messages_no_time(dialogue_history, multimodal=False):
 def create_llm_client(model_name):
     volcengine_models = [
         chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_32K,
-        chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_1_5,
+        chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_1_5_32K,
         chat_service.VOLCENGINE_MODEL_DOUBAO_1_5_VISION_PRO,
         chat_service.VOLCENGINE_MODEL_DEEPSEEK_V3,
     ]

+ 3 - 1
requirements.txt

@@ -60,4 +60,6 @@ pillow~=11.2.1
 json5~=0.12.0
 beautifulsoup4~=4.13.4
 diskcache~=5.6.3
-SQLAlchemy~=2.0.40
+SQLAlchemy~=2.0.40
+pandas==2.3.0
+odps==3.5.1

+ 43 - 0
scripts/extract_push_action_logs.py

@@ -0,0 +1,43 @@
+import re
+
+def extract_agent_run_steps(log_path):
+    pattern = re.compile(
+        r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3} - agent run\[\d+\] - DEBUG - current step content:'
+    )
+    timestamp_pattern = re.compile(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3} - ')
+    results = []
+    current = []
+    collecting = False
+
+    with open(log_path, 'r', encoding='utf-8') as f:
+        for line in f:
+            if pattern.match(line):
+                if collecting and current:
+                    results.append(''.join(current).rstrip())
+                    current = []
+                collecting = True
+                current.append(line)
+            elif collecting:
+                if timestamp_pattern.match(line):
+                    results.append(''.join(current).rstrip())
+                    current = []
+                    collecting = False
+                else:
+                    current.append(line)
+        # 文件结尾处理
+        if collecting and current:
+            results.append(''.join(current).rstrip())
+    return results
+
+if __name__ == "__main__":
+    import sys
+    if len(sys.argv) != 2:
+        print("Usage: python extract_agent_run_step.py <logfile>")
+        sys.exit(1)
+    log_file = sys.argv[1]
+    steps = extract_agent_run_steps(log_file)
+    for i, step in enumerate(steps, 1):
+        print(f"--- Step {i} ---")
+        print(step)
+        print()
+

+ 30 - 0
scripts/mq_consumer.py

@@ -0,0 +1,30 @@
+import time
+
+import rocketmq
+
+from pqai_agent import configs
+
+if __name__ == '__main__':
+    credentials = rocketmq.Credentials()
+    mq_conf = configs.get()['mq']
+    rmq_client_conf = rocketmq.ClientConfiguration(mq_conf['endpoints'], credentials, mq_conf['instance_id'])
+    print(rmq_client_conf)
+    rmq_topic = 'agent_push_tasks'
+    rmq_group = 'agent_push_generate_task'
+    consumer = rocketmq.SimpleConsumer(rmq_client_conf, rmq_group, await_duration=5)
+    consumer.startup()
+    time.sleep(1)
+    consumer.subscribe(rmq_topic)
+    time.sleep(1)
+    while True:
+        t1 = time.time()
+        msgs = consumer.receive(1, 10)
+        if not msgs:
+            break
+        msg = msgs[0]
+        for msg in msgs:
+            msg_body = msg.body.decode('utf-8')
+            print(f"received message: {msg_body}")
+            consumer.ack(msg)
+        time.sleep(1)
+

+ 40 - 3
tests/unit_test.py

@@ -4,8 +4,12 @@
 
 import pytest
 from unittest.mock import Mock, MagicMock
-from pqai_agent.agent_service import AgentService, MemoryQueueBackend
+
+import pqai_agent.abtest.client
+import pqai_agent.configs
+from pqai_agent.agent_service import AgentService
 from pqai_agent.dialogue_manager import DialogueState, TimeContext
+from pqai_agent.message_queue_backend import MemoryQueueBackend
 from pqai_agent.mq_message import MessageType, MqMessage, MessageChannel
 from pqai_agent.response_type_detector import ResponseTypeDetector
 from pqai_agent.user_manager import LocalUserManager
@@ -44,11 +48,16 @@ def test_env():
         user_relation_manager=user_relation_manager
     )
     service.user_profile_extractor.extract_profile_info = Mock(return_value=None)
+    service.can_send_to_user = Mock(return_value=True)
+    service.start()
 
     # 替换LLM调用为模拟响应
     service._call_chat_api = Mock(return_value="模拟响应")
 
-    return service, queues
+    yield service, queues
+
+    service.shutdown(sync=True)
+    pqai_agent.abtest.client.get_client().shutdown()
 
 def test_agent_state_change(test_env):
     service, _ = test_env
@@ -220,10 +229,38 @@ def test_initiative_conversation(test_env):
 def test_response_type_detector(test_env):
     case1 = '大哥,那可得提前了解下天气,以便安排行程~我帮您查查明天北京天气?'
     assert ResponseTypeDetector.is_chinese_only(case1) == True
-    assert ResponseTypeDetector.if_message_suitable_for_voice(case1) == False
+    assert ResponseTypeDetector.if_message_suitable_for_voice(case1) == True
     case2 = 'hi'
     assert ResponseTypeDetector.is_chinese_only(case2) == False
     case3 = '这是链接:http://domain.com'
     assert ResponseTypeDetector.is_chinese_only(case3) == False
     case4 = '大哥,那可得提前了解下天气'
     assert ResponseTypeDetector.if_message_suitable_for_voice(case4) == True
+
+    global_config = pqai_agent.configs.get()
+    global_config.get('debug_flags', {}).update({'disable_llm_api_call': False})
+
+    response_detector = ResponseTypeDetector()
+    dialogue1 = [
+        {'role': 'user', 'content': '你好', 'timestamp': 1744979571000, 'type': MessageType.TEXT},
+        {'role': 'assistant', 'content': '你好呀', 'timestamp': 1744979581000},
+    ]
+    assert response_detector.detect_type(dialogue1[:-1], dialogue1[-1]) == MessageType.TEXT
+
+    dialogue2 = [
+        {'role': 'user', 'content': '你可以读一个故事给我听吗', 'timestamp': 1744979591000},
+        {'role': 'assistant', 'content': '当然可以啦!想听什么?', 'timestamp': 1744979601000},
+        {'role': 'user', 'content': '我想听小王子', 'timestamp': 1744979611000},
+        {'role': 'assistant', 'content': '《小王子》讲述了一位年轻王子离开自己的小世界去探索宇宙的冒险经历。 在旅途中,他遇到了各种各样的人,包括被困的飞行员、狐狸和聪明的蛇。 王子通过这些遭遇学到了关于爱情、友谊和超越表面的必要性的重要教训。', 'timestamp': 1744979611000},
+    ]
+    assert response_detector.detect_type(dialogue2[:-1], dialogue2[-1]) == MessageType.VOICE
+
+    dialogue3 = [
+        {'role': 'user', 'content': '他说的是西洋参呢,晓不得到底是不是西洋参。那个样,那个茶是抽的真空的紧包包。我泡他两包,两包泡到十几盒,13盒,我还拿回来的。', 'timestamp': 1744979591000},
+        {'role': 'assistant', 'content': '咋啦?是突然想到啥啦,还是有其他事想和我分享分享?', 'timestamp': 1744979601000},
+        {'role': 'user', 'content': '不要打字,还不要打。听不到。不要打字,不要打字,打字我认不到。打字我认不到,不要打字不要打字,打字我认不到。', 'timestamp': 1744979611000},
+        {'role': 'assistant', 'content': '真是不好意思', 'timestamp': 1744979611000},
+    ]
+    assert response_detector.detect_type(dialogue3[:-1], dialogue3[-1]) == MessageType.VOICE
+
+    global_config.get('debug_flags', {}).update({'disable_llm_api_call': True})