Browse Source

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

# Conflicts:
#	pqai_agent/logging.py
xueyiming 4 months ago
parent
commit
fa562de09e
66 changed files with 3112 additions and 310 deletions
  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
     ExperimentContext, ExperimentResult
 from alibabacloud_paiabtest20240119.models import ListProjectsRequest, ListProjectsResponseBodyProjects, \
 from alibabacloud_paiabtest20240119.models import ListProjectsRequest, ListProjectsResponseBodyProjects, \
     ListDomainsRequest, ListFeaturesRequest, ListLayersRequest, ListExperimentsRequest, ListExperimentVersionsRequest
     ListDomainsRequest, ListFeaturesRequest, ListLayersRequest, ListExperimentsRequest, ListExperimentVersionsRequest
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 
 
 class ExperimentClient:
 class ExperimentClient:
     def __init__(self, client: Client):
     def __init__(self, client: Client):
@@ -267,7 +267,7 @@ def get_client():
     return g_client
     return g_client
 
 
 if __name__ == '__main__':
 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')
     setup_root_logger(level='DEBUG')
     experiment_client = get_client()
     experiment_client = get_client()
 
 

+ 1 - 1
pqai_agent/abtest/models.py

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

+ 1 - 3
pqai_agent/agent.py

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

+ 1 - 1
pqai_agent/agent_config_manager.py

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

+ 40 - 15
pqai_agent/agent_service.py

@@ -1,7 +1,7 @@
 #! /usr/bin/env python
 #! /usr/bin/env python
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 # vim:fenc=utf-8
 # vim:fenc=utf-8
-
+import json
 import re
 import re
 import signal
 import signal
 import sys
 import sys
@@ -15,15 +15,16 @@ import traceback
 import apscheduler.triggers.cron
 import apscheduler.triggers.cron
 import rocketmq
 import rocketmq
 from apscheduler.schedulers.background import BackgroundScheduler
 from apscheduler.schedulers.background import BackgroundScheduler
+from rocketmq import FilterExpression
 from sqlalchemy.orm import sessionmaker
 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.abtest.utils import get_abtest_info
 from pqai_agent.agent_config_manager import AgentConfigManager
 from pqai_agent.agent_config_manager import AgentConfigManager
 from pqai_agent.agents.message_reply_agent import MessageReplyAgent
 from pqai_agent.agents.message_reply_agent import MessageReplyAgent
 from pqai_agent.configs import apollo_config
 from pqai_agent.configs import apollo_config
 from pqai_agent.exceptions import NoRetryException
 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 import chat_service
 from pqai_agent.chat_service import CozeChat, ChatServiceType
 from pqai_agent.chat_service import CozeChat, ChatServiceType
 from pqai_agent.dialogue_manager import DialogueManager, DialogueState, DialogueStateCache
 from pqai_agent.dialogue_manager import DialogueManager, DialogueState, DialogueStateCache
@@ -96,7 +97,8 @@ class AgentService:
 
 
         # Push相关
         # Push相关
         self.push_task_producer = None
         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._init_push_task_queue()
         self.next_push_disabled = True
         self.next_push_disabled = True
         self._resume_unfinished_push_task()
         self._resume_unfinished_push_task()
@@ -344,7 +346,7 @@ class AgentService:
             logger.debug(f"staff[{staff_id}], user[{user_id}]: no messages to send")
             logger.debug(f"staff[{staff_id}], user[{user_id}]: no messages to send")
 
 
     def can_send_to_user(self, staff_id, user_id) -> bool:
     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", []))
         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
         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", []))
         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):
     def send_multimodal_response(self, staff_id, user_id, response: Dict, skip_check=False):
         message_type = response["type"]
         message_type = response["type"]
         logger.warning(f"staff[{staff_id}] user[{user_id}]: response[{message_type}] {response}")
         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}")
             logger.error(f"staff[{staff_id}] user[{user_id}]: unsupported message type {message_type}")
             return
             return
         if not skip_check and not self.can_send_to_user(staff_id, user_id):
         if not skip_check and not self.can_send_to_user(staff_id, user_id):
             return
             return
         current_ts = int(time.time() * 1000)
         current_ts = int(time.time() * 1000)
         self.send_rate_limiter.wait_for_sending(staff_id, response)
         self.send_rate_limiter.wait_for_sending(staff_id, response)
+        # FIXME: 小程序相关的字段
         self.send_queue.produce(
         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):
     def _route_to_human_intervention(self, user_id: str, origin_message: MqMessage):
@@ -384,20 +392,31 @@ class AgentService:
         mq_conf = configs.get()['mq']
         mq_conf = configs.get()['mq']
         rmq_client_conf = rocketmq.ClientConfiguration(mq_conf['endpoints'], credentials, mq_conf['instance_id'])
         rmq_client_conf = rocketmq.ClientConfiguration(mq_conf['endpoints'], credentials, mq_conf['instance_id'])
         rmq_topic = mq_conf['push_tasks_topic']
         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_rmq_topic = rmq_topic
         self.push_task_producer = rocketmq.Producer(rmq_client_conf, (rmq_topic,))
         self.push_task_producer = rocketmq.Producer(rmq_client_conf, (rmq_topic,))
         self.push_task_producer.startup()
         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 _resume_unfinished_push_task(self):
         def run_unfinished_push_task():
         def run_unfinished_push_task():
             logger.info("start to resume unfinished push task")
             logger.info("start to resume unfinished push task")
             push_task_worker_pool = PushTaskWorkerPool(
             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.start()
             push_task_worker_pool.wait_to_finish()
             push_task_worker_pool.wait_to_finish()
             self.next_push_disabled = False
             self.next_push_disabled = False
@@ -427,7 +446,8 @@ class AgentService:
             push_scan_threads.append(scan_thread)
             push_scan_threads.append(scan_thread)
 
 
         push_task_worker_pool = PushTaskWorkerPool(
         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.start()
         for thread in push_scan_threads:
         for thread in push_scan_threads:
             thread.join()
             thread.join()
@@ -461,9 +481,14 @@ class AgentService:
         agent_config = get_agent_abtest_config('chat', main_agent.user_id,
         agent_config = get_agent_abtest_config('chat', main_agent.user_id,
                                                self.service_module_manager, self.agent_config_manager)
                                                self.service_module_manager, self.agent_config_manager)
         if agent_config:
         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,
             chat_agent = MessageReplyAgent(model=agent_config.execution_model,
                                            system_prompt=agent_config.system_prompt,
                                            system_prompt=agent_config.system_prompt,
-                                           tools=get_tools(agent_config.tools))
+                                           tools=get_tools(tool_names))
         else:
         else:
             chat_agent = MessageReplyAgent()
             chat_agent = MessageReplyAgent()
         chat_responses = chat_agent.generate_message(
         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.agents.multimodal_chat_agent import MultiModalChatAgent
 from pqai_agent.chat_service import VOLCENGINE_MODEL_DEEPSEEK_V3
 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.function_tool import FunctionTool
 from pqai_agent.toolkit.image_describer import ImageDescriber
 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 = '''
 DEFAULT_SYSTEM_PROMPT = '''
 <基本设定>
 <基本设定>
@@ -128,7 +128,7 @@ class MessagePushAgent(MultiModalChatAgent):
         if tools is None:
         if tools is None:
             tools = [
             tools = [
                 *ImageDescriber().get_tools(),
                 *ImageDescriber().get_tools(),
-                *MessageNotifier().get_tools()
+                *MessageToolkit().get_tools()
             ]
             ]
         super().__init__(model, system_prompt, tools, generate_cfg, max_run_step)
         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.agents.multimodal_chat_agent import MultiModalChatAgent
 from pqai_agent.chat_service import VOLCENGINE_MODEL_DEEPSEEK_V3
 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.function_tool import FunctionTool
 from pqai_agent.toolkit.image_describer import ImageDescriber
 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 = '''
 DEFAULT_SYSTEM_PROMPT = '''
 <基本设定>
 <基本设定>
@@ -94,7 +94,7 @@ class MessageReplyAgent(MultiModalChatAgent):
         if tools is None:
         if tools is None:
             tools = [
             tools = [
                 *ImageDescriber().get_tools(),
                 *ImageDescriber().get_tools(),
-                *MessageNotifier().get_tools()
+                *MessageToolkit().get_tools()
             ]
             ]
         super().__init__(model, system_prompt, tools, generate_cfg, max_run_step)
         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 abc import abstractmethod
 from typing import Optional, List, Dict
 from typing import Optional, List, Dict
 
 
+from pqai_agent import configs
 from pqai_agent.agents.simple_chat_agent import SimpleOpenAICompatibleChatAgent
 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.mq_message import MessageType
 from pqai_agent.toolkit import get_tool
 from pqai_agent.toolkit import get_tool
 from pqai_agent.toolkit.function_tool import FunctionTool
 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):
 class MultiModalChatAgent(SimpleOpenAICompatibleChatAgent):
@@ -28,13 +29,16 @@ class MultiModalChatAgent(SimpleOpenAICompatibleChatAgent):
         pass
         pass
 
 
     def _generate_message(self, context: Dict, dialogue_history: List[Dict],
     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)
         formatted_dialogue = MultiModalChatAgent.compose_dialogue(dialogue_history)
         query = query_prompt_template.format(**context, dialogue_history=formatted_dialogue)
         query = query_prompt_template.format(**context, dialogue_history=formatted_dialogue)
         self.run(query)
         self.run(query)
         result = []
         result = []
         for tool_call in self.tool_call_records:
         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'])
                 result.append(tool_call['arguments']['message'])
         return result
         return result
 
 

+ 47 - 6
pqai_agent/agents/simple_chat_agent.py

@@ -1,11 +1,13 @@
 import json
 import json
 from typing import List, Optional
 from typing import List, Optional
 
 
+import pqai_agent.utils
 from pqai_agent.agent import DEFAULT_MAX_RUN_STEPS
 from pqai_agent.agent import DEFAULT_MAX_RUN_STEPS
 from pqai_agent.chat_service import OpenAICompatible
 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.toolkit.function_tool import FunctionTool
-
+from pqai_agent_server.const.status_enum import AgentTaskDetailStatus
 
 
 
 
 class SimpleOpenAICompatibleChatAgent:
 class SimpleOpenAICompatibleChatAgent:
@@ -23,6 +25,10 @@ class SimpleOpenAICompatibleChatAgent:
         self.generate_cfg = generate_cfg or {}
         self.generate_cfg = generate_cfg or {}
         self.max_run_step = max_run_step or DEFAULT_MAX_RUN_STEPS
         self.max_run_step = max_run_step or DEFAULT_MAX_RUN_STEPS
         self.tool_call_records = []
         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):
     def add_tool(self, tool: FunctionTool):
         """添加一个工具到Agent中"""
         """添加一个工具到Agent中"""
@@ -32,23 +38,32 @@ class SimpleOpenAICompatibleChatAgent:
         self.tool_map[tool.name] = tool
         self.tool_map[tool.name] = tool
 
 
     def run(self, user_input: str) -> str:
     def run(self, user_input: str) -> str:
+        run_id = pqai_agent.utils.random_str()[:12]
         messages = [{"role": "system", "content": self.system_prompt}]
         messages = [{"role": "system", "content": self.system_prompt}]
         tools = [tool.get_openai_tool_schema() for tool in self.tools]
         tools = [tool.get_openai_tool_schema() for tool in self.tools]
         messages.append({"role": "user", "content": user_input})
         messages.append({"role": "user", "content": user_input})
 
 
         n_steps = 0
         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:
         while n_steps < self.max_run_step:
             response = self.llm_client.chat.completions.create(model=self.model, messages=messages, tools=tools, **self.generate_cfg)
             response = self.llm_client.chat.completions.create(model=self.model, messages=messages, tools=tools, **self.generate_cfg)
             message = response.choices[0].message
             message = response.choices[0].message
+            self.total_input_tokens += response.usage.prompt_tokens
+            self.total_output_tokens += response.usage.completion_tokens
             messages.append(message)
             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:
             if message.tool_calls:
                 for tool_call in message.tool_calls:
                 for tool_call in message.tool_calls:
                     function_name = tool_call.function.name
                     function_name = tool_call.function.name
                     arguments = json.loads(tool_call.function.arguments)
                     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:
                     if function_name in self.tool_map:
                         result = self.tool_map[function_name](**arguments)
                         result = self.tool_map[function_name](**arguments)
@@ -62,11 +77,37 @@ class SimpleOpenAICompatibleChatAgent:
                             "arguments": arguments,
                             "arguments": arguments,
                             "result": result
                             "result": result
                         })
                         })
+                        agent_task_detail.output_data = json.dumps(result, ensure_ascii=False)
+                        agent_task_detail.status = AgentTaskDetailStatus.SUCCESS.value
                     else:
                     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.")
                         raise Exception(f"Function {function_name} not found in tool map.")
             else:
             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
                 return message.content
             n_steps += 1
             n_steps += 1
 
 
         raise Exception("Max run steps exceeded")
         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
 import httpx
 
 
 from pqai_agent import configs
 from pqai_agent import configs
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 import cozepy
 import cozepy
 from cozepy import Coze, TokenAuth, Message, ChatStatus, MessageType, JWTOAuthApp, JWTAuth
 from cozepy import Coze, TokenAuth, Message, ChatStatus, MessageType, JWTOAuthApp, JWTAuth
 import time
 import time
@@ -22,9 +22,9 @@ COZE_CN_BASE_URL = 'https://api.coze.cn'
 VOLCENGINE_API_TOKEN = '5e275c38-44fd-415f-abcf-4b59f6377f72'
 VOLCENGINE_API_TOKEN = '5e275c38-44fd-415f-abcf-4b59f6377f72'
 VOLCENGINE_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"
 VOLCENGINE_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"
 VOLCENGINE_MODEL_DEEPSEEK_V3 = "deepseek-v3-250324"
 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_API_TOKEN = 'sk-67daad8f424f4854bda7f1fed7ef220b'
 DEEPSEEK_BASE_URL = 'https://api.deepseek.com/'
 DEEPSEEK_BASE_URL = 'https://api.deepseek.com/'
 DEEPSEEK_CHAT_MODEL = 'deepseek-chat'
 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_BASE_URL = 'https://api.openai.com/v1'
 OPENAI_MODEL_GPT_4o = 'gpt-4o'
 OPENAI_MODEL_GPT_4o = 'gpt-4o'
 OPENAI_MODEL_GPT_4o_mini = 'gpt-4o-mini'
 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_BASE_URL = 'https://openrouter.ai/api/v1/'
 OPENROUTER_MODEL_CLAUDE_3_7_SONNET = 'anthropic/claude-3.7-sonnet'
 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):
 class ChatServiceType(Enum):
     OPENAI_COMPATIBLE = auto()
     OPENAI_COMPATIBLE = auto()
     COZE_CHAT = 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:
 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
     @staticmethod
     def create_client(model_name, **kwargs) -> OpenAI:
     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)
             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)
             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)
             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)
             llm_client = OpenAI(api_key=OPENROUTER_API_TOKEN, base_url=OPENROUTER_BASE_URL, **kwargs)
         else:
         else:
             raise Exception("Unsupported model: %s" % model_name)
             raise Exception("Unsupported model: %s" % model_name)
         return llm_client
         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):
 class CrossAccountJWTOAuthApp(JWTOAuthApp):
     def __init__(self, account_id: str, client_id: str, private_key: str, public_key_id: str, base_url):
     def __init__(self, account_id: str, client_id: str, private_key: str, public_key_id: str, base_url):
         self.account_id = account_id
         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
 import requests
 
 
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 
 
 class RelationStageClient:
 class RelationStageClient:
     UNKNOWN_RELATION_STAGE = '未知'
     UNKNOWN_RELATION_STAGE = '未知'

+ 11 - 6
pqai_agent/configs/dev.yaml

@@ -23,6 +23,9 @@ storage:
   staff:
   staff:
     database: ai_agent
     database: ai_agent
     table: qywx_employee
     table: qywx_employee
+  agent_user_relation:
+    database: ai_agent
+    table: qywx_employee_customer
   user_relation:
   user_relation:
     database: growth
     database: growth
     table:
     table:
@@ -51,8 +54,8 @@ chat_api:
     private_key_path: oauth/coze_privkey.pem
     private_key_path: oauth/coze_privkey.pem
     account_id: 649175100044793
     account_id: 649175100044793
   openai_compatible:
   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:
 system:
   outside_proxy:
   outside_proxy:
@@ -62,11 +65,12 @@ system:
   scheduler_mode: local
   scheduler_mode: local
   human_intervention_alert_url: https://open.feishu.cn/open-apis/bot/v2/hook/379fcd1a-0fed-4e58-8cd0-40b6d1895721
   human_intervention_alert_url: https://open.feishu.cn/open-apis/bot/v2/hook/379fcd1a-0fed-4e58-8cd0-40b6d1895721
   max_reply_workers: 2
   max_reply_workers: 2
-  max_push_workers: 1
-  chat_agent_version: 1
+  push_task_workers: 1
+  chat_agent_version: 2
+  log_dir: .
 
 
 debug_flags:
 debug_flags:
-  disable_llm_api_call: True
+  disable_llm_api_call: False
   use_local_user_storage: True
   use_local_user_storage: True
   console_input: True
   console_input: True
   disable_active_conversation: True
   disable_active_conversation: True
@@ -82,4 +86,5 @@ mq:
   scheduler_topic: agent_scheduler_event_dev
   scheduler_topic: agent_scheduler_event_dev
   scheduler_group: agent_scheduler_event_dev
   scheduler_group: agent_scheduler_event_dev
   push_tasks_topic: agent_push_tasks_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
     table: qywx_chat_history
   push_record:
   push_record:
     database: ai_agent
     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:
 chat_api:
   coze:
   coze:
@@ -46,8 +52,8 @@ chat_api:
     private_key_path: oauth/coze_privkey.pem
     private_key_path: oauth/coze_privkey.pem
     account_id: 649175100044793
     account_id: 649175100044793
   openai_compatible:
   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:
 system:
   outside_proxy:
   outside_proxy:
@@ -57,6 +63,8 @@ system:
   scheduler_mode: mq
   scheduler_mode: mq
   human_intervention_alert_url: https://open.feishu.cn/open-apis/bot/v2/hook/c316b559-1c6a-4c4e-97c9-50b44e4c2a9d
   human_intervention_alert_url: https://open.feishu.cn/open-apis/bot/v2/hook/c316b559-1c6a-4c4e-97c9-50b44e4c2a9d
   max_reply_workers: 5
   max_reply_workers: 5
+  push_task_workers: 5
+  log_dir: /var/log/agent_service
 
 
 agent_behavior:
 agent_behavior:
   message_aggregation_sec: 20
   message_aggregation_sec: 20
@@ -79,4 +87,5 @@ mq:
   scheduler_topic: agent_scheduler_event
   scheduler_topic: agent_scheduler_event
   scheduler_group: agent_scheduler_event
   scheduler_group: agent_scheduler_event
   push_tasks_topic: agent_push_tasks
   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 enum import Enum
 
 
 from sqlalchemy import Column, Integer, Text, BigInteger, String, SmallInteger, Boolean, TIMESTAMP
 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()
 Base = declarative_base()
 
 
@@ -16,6 +16,7 @@ class AgentConfiguration(Base):
     id = Column(BigInteger, primary_key=True, autoincrement=True, comment="主键id")
     id = Column(BigInteger, primary_key=True, autoincrement=True, comment="主键id")
     name = Column(String(64), nullable=False, comment="唯一名称")
     name = Column(String(64), nullable=False, comment="唯一名称")
     display_name = Column(String(64), nullable=True, 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-自主规划式")
     type = Column(SmallInteger, nullable=False, default=0, comment="Agent类型,0-响应式,1-自主规划式")
     execution_model = Column(String(64), nullable=True, comment="执行LLM")
     execution_model = Column(String(64), nullable=True, comment="执行LLM")
     system_prompt = Column(Text, nullable=True, comment="系统设定prompt模板")
     system_prompt = Column(Text, nullable=True, comment="系统设定prompt模板")
@@ -27,4 +28,4 @@ class AgentConfiguration(Base):
     create_user = Column(String(32), nullable=True, comment="创建用户")
     create_user = Column(String(32), nullable=True, comment="创建用户")
     update_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="创建时间")
     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 import Column, Integer, Text, BigInteger
-from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import declarative_base
 
 
 from pqai_agent import configs
 from pqai_agent import configs
 
 
 Base = declarative_base()
 Base = declarative_base()
 
 
 class AgentPushRecord(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)
     id = Column(Integer, primary_key=True)
     staff_id = Column(Integer)
     staff_id = Column(Integer)
     user_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 enum import Enum
 
 
 from sqlalchemy import Column, Integer, Text, BigInteger, String, SmallInteger, Boolean, TIMESTAMP
 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()
 Base = declarative_base()
 
 
@@ -19,4 +19,4 @@ class ServiceModule(Base):
     default_agent_id = Column(BigInteger, nullable=True, comment="默认Agent ID")
     default_agent_id = Column(BigInteger, nullable=True, comment="默认Agent ID")
     is_delete = Column(Boolean, nullable=False, default=False, comment="逻辑删除标识")
     is_delete = Column(Boolean, nullable=False, default=False, comment="逻辑删除标识")
     create_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", 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>
 # Copyright © 2024 StrayWarrior <i@straywarrior.com>
 
 
 import pymysql
 import pymysql
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 
 
 class MySQLManager:
 class MySQLManager:
     def __init__(self, config):
     def __init__(self, config):

+ 10 - 4
pqai_agent/dialogue_manager.py

@@ -1,6 +1,7 @@
 #! /usr/bin/env python
 #! /usr/bin/env python
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 # vim:fenc=utf-8
 # vim:fenc=utf-8
+import json
 import random
 import random
 from enum import Enum
 from enum import Enum
 from typing import Dict, List, Optional, Tuple, Any
 from typing import Dict, List, Optional, Tuple, Any
@@ -14,9 +15,10 @@ import cozepy
 from sqlalchemy.orm import sessionmaker, Session
 from sqlalchemy.orm import sessionmaker, Session
 
 
 from pqai_agent import configs
 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.clients.relation_stage_client import RelationStageClient
 from pqai_agent.data_models.agent_push_record import AgentPushRecord
 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.database import MySQLManager
 from pqai_agent import chat_service, prompt_templates
 from pqai_agent import chat_service, prompt_templates
 from pqai_agent.history_dialogue_service import HistoryDialogueService
 from pqai_agent.history_dialogue_service import HistoryDialogueService
@@ -153,7 +155,9 @@ class DialogueManager:
             return TimeContext.NIGHT
             return TimeContext.NIGHT
 
 
     def is_valid(self):
     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 False
         return True
         return True
 
 
@@ -358,7 +362,8 @@ class DialogueManager:
 
 
     def _send_alert(self, alert_type: str, reason: Optional[str] = None) -> None:
     def _send_alert(self, alert_type: str, reason: Optional[str] = None) -> None:
         time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
         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}]"
         user_info = f"{self.user_profile.get('nickname', '未知')}[{self.user_id}]"
         alert_message = f"""
         alert_message = f"""
         {alert_type}告警
         {alert_type}告警
@@ -567,8 +572,9 @@ class DialogueManager:
             "if_first_interaction": True if self.previous_state == DialogueState.INITIALIZED else False,
             "if_first_interaction": True if self.previous_state == DialogueState.INITIALIZED else False,
             "if_active_greeting": False if user_message else True,
             "if_active_greeting": False if user_message else True,
             "relation_stage": self.relation_stage,
             "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),
             "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,
             **self.user_profile,
             **legacy_staff_profile
             **legacy_staff_profile
         }
         }

+ 1 - 1
pqai_agent/history_dialogue_service.py

@@ -7,7 +7,7 @@ import requests
 from pymysql.cursors import DictCursor
 from pymysql.cursors import DictCursor
 
 
 from pqai_agent.database import MySQLManager
 from pqai_agent.database import MySQLManager
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 import time
 import time
 
 
 from pqai_agent import configs
 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
         return message
 
 
 def setup_root_logger(level=logging.DEBUG, logfile_name='service.log'):
 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 = logging.StreamHandler()
     console_handler.setLevel(logging.DEBUG)
     console_handler.setLevel(logging.DEBUG)
-    console_handler.setFormatter(formatter)
+    console_handler.setFormatter(color_formatter)
 
 
     root_logger = logging.getLogger()
     root_logger = logging.getLogger()
     root_logger.handlers.clear()
     root_logger.handlers.clear()
     root_logger.addHandler(console_handler)
     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(
         file_handler = RotatingFileHandler(
-            f'{logfile_name}',
+            f'{log_dir}/{logfile_name}',
             maxBytes=64 * 1024 * 1024,
             maxBytes=64 * 1024 * 1024,
             backupCount=5,
             backupCount=5,
             encoding='utf-8'
             encoding='utf-8'
         )
         )
         file_handler.setLevel(logging.DEBUG)
         file_handler.setLevel(logging.DEBUG)
-        file_handler.setFormatter(formatter)
+        file_handler.setFormatter(plain_formatter)
         root_logger.addHandler(file_handler)
         root_logger.addHandler(file_handler)
 
 
     agent_logger = logging.getLogger('agent')
     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 rocketmq import ClientConfiguration, Credentials, SimpleConsumer
 
 
 from pqai_agent import configs
 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
 from pqai_agent.mq_message import MqMessage, MessageType, MessageChannel
 
 
 
 
@@ -87,7 +86,7 @@ class AliyunRocketMQQueueBackend(MessageQueueBackend):
             return None
             return None
         rmq_message = messages[0]
         rmq_message = messages[0]
         body = rmq_message.body.decode('utf-8')
         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:
         try:
             message = MqMessage.from_json(body)
             message = MqMessage.from_json(body)
             message._rmq_message = rmq_message
             message._rmq_message = rmq_message

+ 10 - 3
pqai_agent/mq_message.py

@@ -107,6 +107,14 @@ class MqMessage(BaseModel):
      senderUnionId: Optional[str] = None
      senderUnionId: Optional[str] = None
      receiver: str
      receiver: str
      content: Optional[str] = None
      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命名法
      # 由于需要和其它语言如Java进行序列化和反序列化交互,因此使用camelCase命名法
      sendTime: int
      sendTime: int
      refMsgId: Optional[int] = None
      refMsgId: Optional[int] = None
@@ -127,9 +135,8 @@ class MqMessage(BaseModel):
          )
          )
 
 
      def to_json(self):
      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
      @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.agents.message_push_agent import MessagePushAgent, DummyMessagePushAgent
 from pqai_agent.configs import apollo_config
 from pqai_agent.configs import apollo_config
 from pqai_agent.data_models.agent_push_record import AgentPushRecord
 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.mq_message import MessageType
 from pqai_agent.toolkit import get_tools
 from pqai_agent.toolkit import get_tools
 from pqai_agent.utils.agent_abtest_utils import get_agent_abtest_config
 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', []))
         first_initiate_tags = set(apollo_config.get_json_value('agent_first_initiate_whitelist_tags', []))
         # 合并白名单,减少配置成本
         # 合并白名单,减少配置成本
         white_list_tags.update(first_initiate_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']
             staff_id = staff_user['staff_id']
             user_id = staff_user['user_id']
             user_id = staff_user['user_id']
             # 通过AB实验配置控制用户组是否启用push
             # 通过AB实验配置控制用户组是否启用push
@@ -62,8 +65,8 @@ class PushScanThread:
             # if abtest_params.get('agent_push_enabled', 'false').lower() != 'true':
             # if abtest_params.get('agent_push_enabled', 'false').lower() != 'true':
             #     logger.debug(f"User {user_id} not enabled agent push, skipping.")
             #     logger.debug(f"User {user_id} not enabled agent push, skipping.")
             #     continue
             #     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
                 should_initiate = False
             else:
             else:
                 agent = self.service.get_agent_instance(staff_id, user_id)
                 agent = self.service.get_agent_instance(staff_id, user_id)
@@ -78,58 +81,79 @@ class PushScanThread:
 
 
 class PushTaskWorkerPool:
 class PushTaskWorkerPool:
     def __init__(self, agent_service: 'AgentService', mq_topic: str,
     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
         self.agent_service = agent_service
         max_workers = configs.get()['system'].get('push_task_workers', 5)
         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.generate_executor = ThreadPoolExecutor(max_workers=max_workers)
         self.send_executors = {}
         self.send_executors = {}
         self.rmq_topic = mq_topic
         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.producer = mq_producer
-        self.loop_thread = None
+        self.generate_loop_thread = None
+        self.send_loop_thread = None
         self.is_generator_running = True
         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):
     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)
         time.sleep(1)
         while True:
         while True:
-            msgs = self.consumer.receive(1, 300)
+            msgs = self.send_consumer.receive(1, 60)
             if not msgs:
             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:
                 else:
                     continue
                     continue
             msg = msgs[0]
             msg = msgs[0]
             task = json.loads(msg.body.decode('utf-8'))
             task = json.loads(msg.body.decode('utf-8'))
             msg_time = datetime.fromtimestamp(task['timestamp'] / 1000).strftime("%Y-%m-%d %H:%M:%S")
             msg_time = datetime.fromtimestamp(task['timestamp'] / 1000).strftime("%Y-%m-%d %H:%M:%S")
             logger.debug(f"recv message:{msg_time} - {task}")
             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']
                 staff_id = task['staff_id']
                 if staff_id not in self.send_executors:
                 if staff_id not in self.send_executors:
                     self.send_executors[staff_id] = ThreadPoolExecutor(max_workers=1)
                     self.send_executors[staff_id] = ThreadPoolExecutor(max_workers=1)
                 self.send_executors[staff_id].submit(self.handle_send_task, task, msg)
                 self.send_executors[staff_id].submit(self.handle_send_task, task, msg)
             else:
             else:
                 logger.error(f"Unknown task type: {task['task_type']}")
                 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):
     def wait_to_finish(self):
         self.generate_send_done = True
         self.generate_send_done = True
@@ -138,7 +162,8 @@ class PushTaskWorkerPool:
             time.sleep(1)
             time.sleep(1)
         self.generate_executor.shutdown(wait=True)
         self.generate_executor.shutdown(wait=True)
         self.is_generator_running = False
         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):
     def handle_send_task(self, task: Dict, msg: rocketmq.Message):
         try:
         try:
@@ -147,13 +172,13 @@ class PushTaskWorkerPool:
             agent = self.agent_service.get_agent_instance(staff_id, user_id)
             agent = self.agent_service.get_agent_instance(staff_id, user_id)
             # 二次校验是否需要发送
             # 二次校验是否需要发送
             if not agent.should_initiate_conversation():
             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
                 return
             contents: List[Dict] = json.loads(task['content'])
             contents: List[Dict] = json.loads(task['content'])
             if not contents:
             if not contents:
                 logger.debug(f"staff[{staff_id}], user[{user_id}]: empty content, do not send")
                 logger.debug(f"staff[{staff_id}], user[{user_id}]: empty content, do not send")
-                self.consumer.ack(msg)
+                self.send_consumer.ack(msg)
                 return
                 return
             recent_dialogue = agent.dialogue_history[-10:]
             recent_dialogue = agent.dialogue_history[-10:]
             agent_voice_whitelist = set(apollo_config.get_json_value("agent_voice_whitelist", []))
             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)
                 agent.update_last_active_interaction_time(current_ts)
             else:
             else:
                 logger.debug(f"staff[{staff_id}], user[{user_id}]: generate empty response")
                 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:
         except Exception as e:
             fmt_exc = traceback.format_exc()
             fmt_exc = traceback.format_exc()
             logger.error(f"Error processing message sending: {e}, {fmt_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):
     def handle_generate_task(self, task: Dict, msg: rocketmq.Message):
         try:
         try:
@@ -223,15 +248,17 @@ class PushTaskWorkerPool:
                 ),
                 ),
                 query_prompt_template=query_prompt_template
                 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:
             if message_to_user:
                 rmq_message = generate_task_rmq_message(
                 rmq_message = generate_task_rmq_message(
                     self.rmq_topic, staff_id, user_id, TaskType.SEND, json.dumps(message_to_user))
                     self.rmq_topic, staff_id, user_id, TaskType.SEND, json.dumps(message_to_user))
                 self.producer.send(rmq_message)
                 self.producer.send(rmq_message)
             else:
             else:
                 logger.info(f"staff[{staff_id}], user[{user_id}]: no push message generated")
                 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:
         except Exception as e:
             fmt_exc = traceback.format_exc()
             fmt_exc = traceback.format_exc()
             logger.error(f"Error processing message generation: {e}, {fmt_exc}")
             logger.error(f"Error processing message generation: {e}, {fmt_exc}")
             # FIXME: 是否需要ACK
             # FIXME: 是否需要ACK
-            self.consumer.ack(msg)
+            self.generate_consumer.ack(msg)

+ 1 - 1
pqai_agent/rate_limiter.py

@@ -5,7 +5,7 @@
 import time
 import time
 from typing import Optional, Union, Dict
 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
 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 configs
 from pqai_agent import prompt_templates
 from pqai_agent import prompt_templates
 from pqai_agent.dialogue_manager import DialogueManager
 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
 from pqai_agent.mq_message import MessageType
 
 
 
 
@@ -36,7 +36,7 @@ class ResponseTypeDetector:
             api_key=chat_service.VOLCENGINE_API_TOKEN,
             api_key=chat_service.VOLCENGINE_API_TOKEN,
             base_url=chat_service.VOLCENGINE_BASE_URL
             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,
     def detect_type(self, dialogue_history: List[Dict], next_message: Dict, enable_random=False,
                     random_rate=0.25):
                     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.data_models.service_module import ServiceModule, ModuleAgentType
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 
 
 class ServiceModuleManager:
 class ServiceModuleManager:
     def __init__(self, session_maker):
     def __init__(self, session_maker):

+ 3 - 2
pqai_agent/toolkit/__init__.py

@@ -1,12 +1,13 @@
 # 必须要在这里导入模块,以便对应的模块执行register_toolkit
 # 必须要在这里导入模块,以便对应的模块执行register_toolkit
 from typing import Sequence, List
 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.tool_registry import ToolRegistry
 from pqai_agent.toolkit.image_describer import ImageDescriber
 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.pq_video_searcher import PQVideoSearcher
 from pqai_agent.toolkit.search_toolkit import SearchToolkit
 from pqai_agent.toolkit.search_toolkit import SearchToolkit
+from pqai_agent.toolkit.hot_topic_toolkit import HotTopicToolkit
 
 
 global_tool_map = ToolRegistry.tool_map
 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 pydantic.fields import FieldInfo
 from jsonschema.validators import Draft202012Validator as JSONValidator
 from jsonschema.validators import Draft202012Validator as JSONValidator
 import re
 import re
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 
 
 
 
 def to_pascal(snake: str) -> str:
 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 import chat_service
 from pqai_agent.chat_service import VOLCENGINE_MODEL_DOUBAO_1_5_VISION_PRO
 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.base import BaseToolkit
 from pqai_agent.toolkit.function_tool import FunctionTool
 from pqai_agent.toolkit.function_tool import FunctionTool
 from pqai_agent.toolkit.tool_registry import register_toolkit
 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
 import requests
 from pqai_agent.toolkit.base import BaseToolkit
 from pqai_agent.toolkit.base import BaseToolkit
 from pqai_agent.toolkit.function_tool import FunctionTool
 from pqai_agent.toolkit.function_tool import FunctionTool
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 
 
 class LarkSheetRecordForHumanIntervention(BaseToolkit):
 class LarkSheetRecordForHumanIntervention(BaseToolkit):
     r"""A toolkit for recording human intervention events into a Feishu spreadsheet."""
     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
 #! /usr/bin/env python
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 # vim:fenc=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
 from typing import Dict, Optional, List
 import json
 import json
 import time
 import time
@@ -33,6 +34,10 @@ class UserManager(abc.ABC):
         #FIXME(zhoutian): 重新设计用户和员工数据管理模型
         #FIXME(zhoutian): 重新设计用户和员工数据管理模型
         pass
         pass
 
 
+    @abstractmethod
+    def get_user_tags(self, user_ids: List[str], batch_size = 500) -> Dict[str, List[str]]:
+        pass
+
     @staticmethod
     @staticmethod
     def get_default_profile(**kwargs) -> Dict:
     def get_default_profile(**kwargs) -> Dict:
         default_profile = {
         default_profile = {
@@ -133,6 +138,9 @@ class LocalUserManager(UserManager):
             logger.error("staff profile not found: {}".format(e))
             logger.error("staff profile not found: {}".format(e))
             return {}
             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]:
     def list_users(self, **kwargs) -> List[Dict]:
         pass
         pass
 
 
@@ -228,7 +236,7 @@ class MySQLUserManager(UserManager):
         return profile
         return profile
 
 
     def get_staff_profile_v3(self, staff_id) -> Dict:
     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}'"
               f"FROM {self.staff_table} WHERE third_party_user_id = '{staff_id}'"
         data = self.db.select(sql)
         data = self.db.select(sql)
         if not data:
         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}'"
         sql = f"UPDATE {self.staff_table} SET agent_profile = %s WHERE third_party_user_id = '{staff_id}'"
         self.db.execute(sql, (json.dumps(profile),))
         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]:
     def list_users(self, **kwargs) -> List[Dict]:
         user_union_id = kwargs.get('user_union_id', None)
         user_union_id = kwargs.get('user_union_id', None)
         user_name = kwargs.get('user_name', None)
         user_name = kwargs.get('user_name', None)
@@ -333,6 +372,7 @@ class MySQLUserRelationManager(UserRelationManager):
         self.relation_table = relation_table
         self.relation_table = relation_table
         self.agent_user_table = agent_user_table
         self.agent_user_table = agent_user_table
         self.user_table = user_table
         self.user_table = user_table
+        self.agent_user_relation_table = 'qywx_employee_customer'
 
 
     def list_staffs(self):
     def list_staffs(self):
         sql = f"SELECT third_party_user_id, name, wxid, agent_name FROM {self.agent_staff_table} WHERE status = 1"
         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):
     def list_users(self, staff_id: str, page: int = 1, page_size: int = 100):
         return []
         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"
         sql = f"SELECT third_party_user_id, wxid FROM {self.agent_staff_table} WHERE status = 1"
         if staff_id:
         if staff_id:
             sql += f" AND third_party_user_id = '{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)}"
                 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)
                 batch_agent_user_data = self.agent_db.select(sql, pymysql.cursors.DictCursor)
                 if len(agent_user_data) != len(batch_union_ids):
                 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
                     pass
                 agent_user_data.extend(batch_agent_user_data)
                 agent_user_data.extend(batch_agent_user_data)
             staff_user_pairs = [
             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 import chat_service, configs
 from pqai_agent.prompt_templates import USER_PROFILE_EXTRACT_PROMPT, USER_PROFILE_EXTRACT_PROMPT_V2
 from pqai_agent.prompt_templates import USER_PROFILE_EXTRACT_PROMPT, USER_PROFILE_EXTRACT_PROMPT_V2
 from openai import OpenAI
 from openai import OpenAI
-from pqai_agent.logging_service import logger
+from pqai_agent.logging import logger
 from pqai_agent.utils import prompt_utils
 from pqai_agent.utils import prompt_utils
 
 
 
 
@@ -198,8 +198,8 @@ class UserProfileExtractor:
 
 
 if __name__ == '__main__':
 if __name__ == '__main__':
     from pqai_agent import configs
     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 = configs.get()
     config['debug_flags']['disable_llm_api_call'] = False
     config['debug_flags']['disable_llm_api_call'] = False
     extractor = UserProfileExtractor()
     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
 from typing import Dict
 
 
+import yaml
+
 
 
 def format_agent_profile(profile: Dict) -> str:
 def format_agent_profile(profile: Dict) -> str:
     fields = [
     fields = [
@@ -16,12 +20,17 @@ def format_agent_profile(profile: Dict) -> str:
     ]
     ]
     strings_to_join = []
     strings_to_join = []
     for field in fields:
     for field in fields:
-        if not profile.get(field[0], None):
+        if profile.get(field[0], None) is None:
             continue
             continue
         cur_string = f"- {field[1]}:{profile[field[0]]}"
         cur_string = f"- {field[1]}:{profile[field[0]]}"
         strings_to_join.append(cur_string)
         strings_to_join.append(cur_string)
     return "\n".join(strings_to_join)
     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:
 def format_user_profile(profile: Dict) -> str:
     """
     """
     :param profile:
     :param profile:
@@ -54,8 +63,8 @@ def format_user_profile(profile: Dict) -> str:
     for field in fields:
     for field in fields:
         value = profile.get(field[0], None)
         value = profile.get(field[0], None)
         if not value:
         if not value:
-            continue
-        if isinstance(value, list):
+            value = '未知'
+        elif isinstance(value, list):
             value = ','.join(value)
             value = ','.join(value)
         elif isinstance(value, dict):
         elif isinstance(value, dict):
             value = ';'.join(f"{k}: {v}" for k, v in value.items())
             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 sys
 import time
 import time
 
 
-from pqai_agent import configs, logging_service
+from pqai_agent import configs
 from pqai_agent.agent_service import AgentService
 from pqai_agent.agent_service import AgentService
 from pqai_agent.chat_service import ChatServiceType
 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.mq_message import MessageType, MqMessage, MessageChannel
 from pqai_agent.message_queue_backend import AliyunRocketMQQueueBackend, MemoryQueueBackend
 from pqai_agent.message_queue_backend import AliyunRocketMQQueueBackend, MemoryQueueBackend
 from pqai_agent.push_service import PushTaskWorkerPool, PushScanThread
 from pqai_agent.push_service import PushTaskWorkerPool, PushScanThread
@@ -14,7 +14,7 @@ from pqai_agent.user_manager import LocalUserManager, LocalUserRelationManager,
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
     config = configs.get()
     config = configs.get()
-    logging_service.setup_root_logger()
+    setup_root_logger()
     logger.warning("current env: {}".format(configs.get_env()))
     logger.warning("current env: {}".format(configs.get_env()))
     scheduler_logger = logging.getLogger('apscheduler')
     scheduler_logger = logging.getLogger('apscheduler')
     scheduler_logger.setLevel(logging.WARNING)
     scheduler_logger.setLevel(logging.WARNING)
@@ -92,13 +92,16 @@ if __name__ == "__main__":
             continue
             continue
         message_id += 1
         message_id += 1
         sender = '7881301903997433'
         sender = '7881301903997433'
-        receiver = '1688855931724582'
+        receiver = '1688854974625870'
         if text in (MessageType.AGGREGATION_TRIGGER.name,
         if text in (MessageType.AGGREGATION_TRIGGER.name,
                     MessageType.HUMAN_INTERVENTION_END.name):
                     MessageType.HUMAN_INTERVENTION_END.name):
             message = MqMessage.build(
             message = MqMessage.build(
                 MessageType.__members__.get(text),
                 MessageType.__members__.get(text),
                 MessageChannel.CORP_WECHAT,
                 MessageChannel.CORP_WECHAT,
                 sender, receiver, None, int(time.time() * 1000))
                 sender, receiver, None, int(time.time() * 1000))
+        elif text == 'S_PUSH':
+            service._check_initiative_conversations()
+            continue
         else:
         else:
             message = MqMessage.build(MessageType.TEXT, MessageChannel.CORP_WECHAT,
             message = MqMessage.build(MessageType.TEXT, MessageChannel.CORP_WECHAT,
                                       sender, receiver, text, int(time.time() * 1000)
                                       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
 #! /usr/bin/env python
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 # vim:fenc=utf-8
 # vim:fenc=utf-8
-import time
+import json
 import logging
 import logging
-import werkzeug.exceptions
-from flask import Flask, request, jsonify
 from argparse import ArgumentParser
 from argparse import ArgumentParser
 
 
+import werkzeug.exceptions
+from flask import Flask, request, jsonify
 from sqlalchemy.orm import sessionmaker
 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 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.agent_configuration import AgentConfiguration
 from pqai_agent.data_models.service_module import ServiceModule
 from pqai_agent.data_models.service_module import ServiceModule
 from pqai_agent.history_dialogue_service import HistoryDialogueService
 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.user_manager import MySQLUserManager, MySQLUserRelationManager
 from pqai_agent.utils.db_utils import create_ai_agent_db_engine
 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.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 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.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 (
 from pqai_agent_server.utils import (
     run_extractor_prompt,
     run_extractor_prompt,
     run_chat_prompt,
     run_chat_prompt,
     run_response_type_prompt,
     run_response_type_prompt,
 )
 )
+from pqai_agent_server.utils import wrap_response
 
 
 app = Flask('agent_api_server')
 app = Flask('agent_api_server')
-logger = logging_service.logger
 const = AgentApiConst()
 const = AgentApiConst()
 
 
+
 @app.route('/api/listStaffs', methods=['GET'])
 @app.route('/api/listStaffs', methods=['GET'])
 def list_staffs():
 def list_staffs():
     staff_data = app.user_relation_manager.list_staffs()
     staff_data = app.user_relation_manager.list_staffs()
@@ -91,34 +98,24 @@ def get_dialogue_history():
 
 
 @app.route('/api/listModels', methods=['GET'])
 @app.route('/api/listModels', methods=['GET'])
 def list_models():
 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_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'])
 @app.route('/api/listScenes', methods=['GET'])
@@ -146,8 +143,8 @@ def get_base_prompt():
     model_map = {
     model_map = {
         'greeting': chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_32K,
         'greeting': chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_32K,
         'chitchat': 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
         'custom_debugging': chat_service.VOLCENGINE_BOT_DEEPSEEK_V3_SEARCH
     }
     }
     if scene not in prompt_map:
     if scene not in prompt_map:
@@ -178,6 +175,7 @@ def run_prompt():
         logger.error(e)
         logger.error(e)
         return wrap_response(500, msg='Error: {}'.format(e))
         return wrap_response(500, msg='Error: {}'.format(e))
 
 
+
 @app.route('/api/formatForPrompt', methods=['POST'])
 @app.route('/api/formatForPrompt', methods=['POST'])
 def format_data_for_prompt():
 def format_data_for_prompt():
     try:
     try:
@@ -297,8 +295,8 @@ def send_message():
     return wrap_response(200, msg="暂不实现功能")
     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:
     :return:
@@ -308,9 +306,28 @@ def quit_human_interventions_status():
     user_id = req_data["user_id"]
     user_id = req_data["user_id"]
     if not user_id or not staff_id:
     if not user_id or not staff_id:
         return wrap_response(404, msg="user_id and staff_id are required")
         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管理接口
 ## Agent管理接口
 @app.route("/api/getNativeAgentList", methods=["GET"])
 @app.route("/api/getNativeAgentList", methods=["GET"])
@@ -332,22 +349,29 @@ def get_native_agent_list():
             query = query.filter(AgentConfiguration.create_user == create_user)
             query = query.filter(AgentConfiguration.create_user == create_user)
         if update_user:
         if update_user:
             query = query.filter(AgentConfiguration.update_user == update_user)
             query = query.filter(AgentConfiguration.update_user == update_user)
+        total = query.count()
         query = query.offset(offset).limit(int(page_size))
         query = query.offset(offset).limit(int(page_size))
         data = query.all()
         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)
     return wrap_response(200, data=ret_data)
 
 
+
 @app.route("/api/getNativeAgentConfiguration", methods=["GET"])
 @app.route("/api/getNativeAgentConfiguration", methods=["GET"])
 def get_native_agent_configuration():
 def get_native_agent_configuration():
     """
     """
@@ -371,14 +395,15 @@ def get_native_agent_configuration():
             'execution_model': agent.execution_model,
             'execution_model': agent.execution_model,
             'system_prompt': agent.system_prompt,
             'system_prompt': agent.system_prompt,
             'task_prompt': agent.task_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'),
             'create_time': agent.create_time.strftime('%Y-%m-%d %H:%M:%S'),
             'update_time': agent.update_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)
         return wrap_response(200, data=data)
 
 
+
 @app.route("/api/saveNativeAgentConfiguration", methods=["POST"])
 @app.route("/api/saveNativeAgentConfiguration", methods=["POST"])
 def save_native_agent_configuration():
 def save_native_agent_configuration():
     """
     """
@@ -393,9 +418,19 @@ def save_native_agent_configuration():
     execution_model = req_data.get('execution_model', None)
     execution_model = req_data.get('execution_model', None)
     system_prompt = req_data.get('system_prompt', None)
     system_prompt = req_data.get('system_prompt', None)
     task_prompt = req_data.get('task_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', {})
     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:
     if not name:
         return wrap_response(400, msg='name is required')
         return wrap_response(400, msg='name is required')
@@ -415,6 +450,7 @@ def save_native_agent_configuration():
             agent.tools = tools
             agent.tools = tools
             agent.sub_agents = sub_agents
             agent.sub_agents = sub_agents
             agent.extra_params = extra_params
             agent.extra_params = extra_params
+            agent.update_user = operate_user
         else:
         else:
             agent = AgentConfiguration(
             agent = AgentConfiguration(
                 name=name,
                 name=name,
@@ -425,37 +461,88 @@ def save_native_agent_configuration():
                 task_prompt=task_prompt,
                 task_prompt=task_prompt,
                 tools=tools,
                 tools=tools,
                 sub_agents=sub_agents,
                 sub_agents=sub_agents,
-                extra_params=extra_params
+                extra_params=extra_params,
+                create_user=operate_user,
+                update_user=operate_user
             )
             )
             session.add(agent)
             session.add(agent)
 
 
         session.commit()
         session.commit()
         return wrap_response(200, msg='Agent configuration saved successfully', data={'id': agent.id})
         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"])
 @app.route("/api/getModuleList", methods=["GET"])
 def get_module_list():
 def get_module_list():
     """
     """
-    获取所有的模块列表
+    获取所有的模块列表,支持分页查询
     :return:
     :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:
     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)
     return wrap_response(200, data=ret_data)
 
 
+
 @app.route("/api/getModuleConfiguration", methods=["GET"])
 @app.route("/api/getModuleConfiguration", methods=["GET"])
 def get_module_configuration():
 def get_module_configuration():
     """
     """
@@ -478,10 +565,11 @@ def get_module_configuration():
             'default_agent_type': module.default_agent_type,
             'default_agent_type': module.default_agent_type,
             'default_agent_id': module.default_agent_id,
             'default_agent_id': module.default_agent_id,
             'create_time': module.create_time.strftime('%Y-%m-%d %H:%M:%S'),
             '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)
         return wrap_response(200, data=data)
 
 
+
 @app.route("/api/saveModuleConfiguration", methods=["POST"])
 @app.route("/api/saveModuleConfiguration", methods=["POST"])
 def save_module_configuration():
 def save_module_configuration():
     """
     """
@@ -520,6 +608,248 @@ def save_module_configuration():
         session.commit()
         session.commit()
         return wrap_response(200, msg='Module configuration saved successfully', data={'id': module.id})
         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)
 @app.errorhandler(werkzeug.exceptions.BadRequest)
 def handle_bad_request(e):
 def handle_bad_request(e):
     logger.error(e)
     logger.error(e)
@@ -536,7 +866,7 @@ if __name__ == '__main__':
 
 
     config = configs.get()
     config = configs.get()
     logging_level = logging.getLevelName(args.log_level)
     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
     # set db config
     agent_db_config = config['database']['ai_agent']
     agent_db_config = config['database']['ai_agent']
@@ -547,7 +877,7 @@ if __name__ == '__main__':
     chat_history_db_config = config['storage']['chat_history']
     chat_history_db_config = config['storage']['chat_history']
 
 
     # init user manager
     # 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
     app.user_manager = user_manager
 
 
     # init session manager
     # init session manager
@@ -559,9 +889,20 @@ if __name__ == '__main__':
         chat_history_table=chat_history_db_config['table']
         chat_history_table=chat_history_db_config['table']
     )
     )
     app.session_manager = session_manager
     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)
     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']
     wecom_db_config = config['storage']['user_relation']
     user_relation_manager = MySQLUserRelationManager(
     user_relation_manager = MySQLUserRelationManager(
         agent_db_config, growth_db_config,
         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 wrap_response
-from .common import quit_human_intervention_status
 
 
 from .prompt_util import (
 from .prompt_util import (
     run_openai_chat,
     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)
     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"
     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 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.response_type_detector import ResponseTypeDetector
 from pqai_agent.user_profile_extractor import UserProfileExtractor
 from pqai_agent.user_profile_extractor import UserProfileExtractor
 from pqai_agent.dialogue_manager import DialogueManager
 from pqai_agent.dialogue_manager import DialogueManager
 from pqai_agent.mq_message import MessageType
 from pqai_agent.mq_message import MessageType
 from pqai_agent.utils.prompt_utils import format_agent_profile
 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):
 def compose_openai_chat_messages_no_time(dialogue_history, multimodal=False):
     messages = []
     messages = []
@@ -44,7 +43,7 @@ def compose_openai_chat_messages_no_time(dialogue_history, multimodal=False):
 def create_llm_client(model_name):
 def create_llm_client(model_name):
     volcengine_models = [
     volcengine_models = [
         chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_32K,
         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_DOUBAO_1_5_VISION_PRO,
         chat_service.VOLCENGINE_MODEL_DEEPSEEK_V3,
         chat_service.VOLCENGINE_MODEL_DEEPSEEK_V3,
     ]
     ]

+ 3 - 1
requirements.txt

@@ -60,4 +60,6 @@ pillow~=11.2.1
 json5~=0.12.0
 json5~=0.12.0
 beautifulsoup4~=4.13.4
 beautifulsoup4~=4.13.4
 diskcache~=5.6.3
 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
 import pytest
 from unittest.mock import Mock, MagicMock
 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.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.mq_message import MessageType, MqMessage, MessageChannel
 from pqai_agent.response_type_detector import ResponseTypeDetector
 from pqai_agent.response_type_detector import ResponseTypeDetector
 from pqai_agent.user_manager import LocalUserManager
 from pqai_agent.user_manager import LocalUserManager
@@ -44,11 +48,16 @@ def test_env():
         user_relation_manager=user_relation_manager
         user_relation_manager=user_relation_manager
     )
     )
     service.user_profile_extractor.extract_profile_info = Mock(return_value=None)
     service.user_profile_extractor.extract_profile_info = Mock(return_value=None)
+    service.can_send_to_user = Mock(return_value=True)
+    service.start()
 
 
     # 替换LLM调用为模拟响应
     # 替换LLM调用为模拟响应
     service._call_chat_api = Mock(return_value="模拟响应")
     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):
 def test_agent_state_change(test_env):
     service, _ = test_env
     service, _ = test_env
@@ -220,10 +229,38 @@ def test_initiative_conversation(test_env):
 def test_response_type_detector(test_env):
 def test_response_type_detector(test_env):
     case1 = '大哥,那可得提前了解下天气,以便安排行程~我帮您查查明天北京天气?'
     case1 = '大哥,那可得提前了解下天气,以便安排行程~我帮您查查明天北京天气?'
     assert ResponseTypeDetector.is_chinese_only(case1) == True
     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'
     case2 = 'hi'
     assert ResponseTypeDetector.is_chinese_only(case2) == False
     assert ResponseTypeDetector.is_chinese_only(case2) == False
     case3 = '这是链接:http://domain.com'
     case3 = '这是链接:http://domain.com'
     assert ResponseTypeDetector.is_chinese_only(case3) == False
     assert ResponseTypeDetector.is_chinese_only(case3) == False
     case4 = '大哥,那可得提前了解下天气'
     case4 = '大哥,那可得提前了解下天气'
     assert ResponseTypeDetector.if_message_suitable_for_voice(case4) == True
     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})