Selaa lähdekoodia

Merge branch 'master' into dev-xym-add-test-task

# Conflicts:
#	pqai_agent_server/api_server.py
xueyiming 1 päivä sitten
vanhempi
commit
99644d1140

+ 9 - 3
pqai_agent/agent_service.py

@@ -358,16 +358,22 @@ class AgentService:
     def send_multimodal_response(self, staff_id, user_id, response: Dict, skip_check=False):
         message_type = response["type"]
         logger.warning(f"staff[{staff_id}] user[{user_id}]: response[{message_type}] {response}")
-        if message_type not in (MessageType.TEXT, MessageType.IMAGE_QW, MessageType.VOICE):
+        if message_type not in (MessageType.TEXT, MessageType.IMAGE_QW, MessageType.VOICE,
+                                MessageType.LINK, MessageType.MINI_PROGRAM):
             logger.error(f"staff[{staff_id}] user[{user_id}]: unsupported message type {message_type}")
             return
         if not skip_check and not self.can_send_to_user(staff_id, user_id):
             return
         current_ts = int(time.time() * 1000)
         self.send_rate_limiter.wait_for_sending(staff_id, response)
+        # FIXME: 小程序相关的字段
         self.send_queue.produce(
-            MqMessage.build(message_type, MessageChannel.CORP_WECHAT,
-                            staff_id, user_id, response["content"], current_ts)
+            MqMessage(type=message_type, channel=MessageChannel.CORP_WECHAT,
+                      sender=staff_id, receiver=user_id, content=response["content"], sendTime=current_ts,
+                      desc=response.get("desc"), title=response.get("title"),
+                      appIconUrl=None, pagePath=response.get("content"),
+                      coverImage=response.get("cover_url"), appOrgId=None,
+                      appId=None)
         )
 
     def _route_to_human_intervention(self, user_id: str, origin_message: MqMessage):

+ 1 - 1
pqai_agent/agents/message_push_agent.py

@@ -5,7 +5,7 @@ from pqai_agent.chat_service import VOLCENGINE_MODEL_DEEPSEEK_V3
 from pqai_agent.logging import logger
 from pqai_agent.toolkit.function_tool import FunctionTool
 from pqai_agent.toolkit.image_describer import ImageDescriber
-from pqai_agent.toolkit.message_toolit import MessageToolkit
+from pqai_agent.toolkit.message_toolkit import MessageToolkit
 
 DEFAULT_SYSTEM_PROMPT = '''
 <基本设定>

+ 1 - 1
pqai_agent/agents/message_reply_agent.py

@@ -5,7 +5,7 @@ from pqai_agent.chat_service import VOLCENGINE_MODEL_DEEPSEEK_V3
 from pqai_agent.logging import logger
 from pqai_agent.toolkit.function_tool import FunctionTool
 from pqai_agent.toolkit.image_describer import ImageDescriber
-from pqai_agent.toolkit.message_toolit import MessageToolkit
+from pqai_agent.toolkit.message_toolkit import MessageToolkit
 
 DEFAULT_SYSTEM_PROMPT = '''
 <基本设定>

+ 1 - 1
pqai_agent/agents/multimodal_chat_agent.py

@@ -8,7 +8,7 @@ from pqai_agent.logging import logger
 from pqai_agent.mq_message import MessageType
 from pqai_agent.toolkit import get_tool
 from pqai_agent.toolkit.function_tool import FunctionTool
-from pqai_agent.toolkit.message_toolit import MessageToolkit
+from pqai_agent.toolkit.message_toolkit import MessageToolkit
 
 
 class MultiModalChatAgent(SimpleOpenAICompatibleChatAgent):

+ 27 - 8
pqai_agent/chat_service.py

@@ -34,9 +34,10 @@ OPENAI_API_TOKEN = 'sk-proj-6LsybsZSinbMIUzqttDt8LxmNbi-i6lEq-AUMzBhCr3jS8sme9AG
 OPENAI_BASE_URL = 'https://api.openai.com/v1'
 OPENAI_MODEL_GPT_4o = 'gpt-4o'
 OPENAI_MODEL_GPT_4o_mini = 'gpt-4o-mini'
-OPENROUTER_API_TOKEN = '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'
+OPENROUTER_API_TOKEN = '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'
 OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1/'
 OPENROUTER_MODEL_CLAUDE_3_7_SONNET = 'anthropic/claude-3.7-sonnet'
+OPENROUTER_MODEL_GEMINI_2_5_PRO = 'google/gemini-2.5-pro'
 ALIYUN_API_TOKEN = 'sk-47381479425f4485af7673d3d2fd92b6'
 ALIYUN_BASE_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
 
@@ -73,6 +74,11 @@ class ModelPrice:
             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})"
 
@@ -92,6 +98,7 @@ class OpenAICompatible:
     ]
     openrouter_models = [
         OPENROUTER_MODEL_CLAUDE_3_7_SONNET,
+        OPENROUTER_MODEL_GEMINI_2_5_PRO
     ]
 
     model_prices = {
@@ -103,6 +110,7 @@ class OpenAICompatible:
         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
@@ -112,20 +120,31 @@ class OpenAICompatible:
         elif model_name in OpenAICompatible.deepseek_models:
             llm_client = OpenAI(api_key=DEEPSEEK_API_TOKEN, base_url=DEEPSEEK_BASE_URL, **kwargs)
         elif model_name in OpenAICompatible.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
+            kwargs['http_client'] = OpenAICompatible.create_outside_proxy_http_client()
             llm_client = OpenAI(api_key=OPENAI_API_TOKEN, base_url=OPENAI_BASE_URL, **kwargs)
         elif model_name in OpenAICompatible.openrouter_models:
+            # kwargs['http_client'] = OpenAICompatible.create_outside_proxy_http_client()
             llm_client = OpenAI(api_key=OPENROUTER_API_TOKEN, base_url=OPENROUTER_BASE_URL, **kwargs)
         else:
             raise Exception("Unsupported model: %s" % model_name)
         return llm_client
 
+    @staticmethod
+    def create_outside_proxy_http_client() -> httpx.Client:
+        """
+        Create an HTTP client with outside proxy settings.
+        :return: Configured httpx.Client instance
+        """
+        socks_conf = configs.get().get('system', {}).get('outside_proxy', {}).get('socks5', {})
+        if socks_conf:
+            return httpx.Client(
+                timeout=httpx.Timeout(600, connect=5.0),
+                proxy=f"socks5://{socks_conf['hostname']}:{socks_conf['port']}"
+            )
+        # If no proxy is configured, return a standard client
+        logger.error("Outside proxy not configured, using default httpx client.")
+        return httpx.Client(timeout=httpx.Timeout(600, connect=5.0))
+
     @staticmethod
     def get_price(model_name: str) -> ModelPrice:
         """

+ 1 - 1
pqai_agent/data_models/service_module.py

@@ -19,4 +19,4 @@ class ServiceModule(Base):
     default_agent_id = Column(BigInteger, nullable=True, comment="默认Agent ID")
     is_delete = Column(Boolean, nullable=False, default=False, comment="逻辑删除标识")
     create_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", comment="创建时间")
-    update_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", onupdate="CURRENT_TIMESTAMP", comment="更新时间")
+    update_time = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP", server_onupdate="CURRENT_TIMESTAMP", comment="更新时间")

+ 10 - 3
pqai_agent/mq_message.py

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

+ 1 - 1
pqai_agent/toolkit/__init__.py

@@ -4,7 +4,7 @@ from typing import Sequence, List
 from pqai_agent.logging import logger
 from pqai_agent.toolkit.tool_registry import ToolRegistry
 from pqai_agent.toolkit.image_describer import ImageDescriber
-from pqai_agent.toolkit.message_toolit import MessageToolkit
+from pqai_agent.toolkit.message_toolkit import MessageToolkit
 from pqai_agent.toolkit.pq_video_searcher import PQVideoSearcher
 from pqai_agent.toolkit.search_toolkit import SearchToolkit
 from pqai_agent.toolkit.hot_topic_toolkit import HotTopicToolkit

+ 15 - 11
pqai_agent/toolkit/message_toolit.py → pqai_agent/toolkit/message_toolkit.py

@@ -27,24 +27,28 @@ class MessageToolkit(BaseToolkit):
         """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",
+            "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 or mini_program, the content should be a URL.
+        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.
         """
-        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."
+        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'
 

+ 13 - 5
pqai_agent_server/api_server.py

@@ -9,9 +9,11 @@ import werkzeug.exceptions
 from flask import Flask, request, jsonify
 from sqlalchemy.orm import sessionmaker
 
+import pqai_agent_server
 import pqai_agent_server.utils
 from pqai_agent import chat_service, prompt_templates
 from pqai_agent import configs
+from pqai_agent.chat_service import OpenAICompatible
 from pqai_agent.data_models.agent_configuration import AgentConfiguration
 from pqai_agent.data_models.service_module import ServiceModule
 from pqai_agent.history_dialogue_service import HistoryDialogueService
@@ -36,7 +38,6 @@ from pqai_agent_server.utils import wrap_response
 app = Flask('agent_api_server')
 const = AgentApiConst()
 
-
 @app.route('/api/listStaffs', methods=['GET'])
 def list_staffs():
     staff_data = app.user_relation_manager.list_staffs()
@@ -103,12 +104,13 @@ def list_models():
         "doubao-pro-32k": chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_32K,
         "doubao-pro-1.5": chat_service.VOLCENGINE_MODEL_DOUBAO_PRO_1_5_32K,
         "doubao-1.5-vision-pro": chat_service.VOLCENGINE_MODEL_DOUBAO_1_5_VISION_PRO,
+        "openrouter-gemini-2.5-pro": chat_service.OPENROUTER_MODEL_GEMINI_2_5_PRO,
     }
     ret_data = [
         {
             'model_type': 'openai_compatible',
             'model_name': model_name,
-            'display_name': model_display_name
+            'display_name': f"{model_display_name} ({OpenAICompatible.get_price(model_name).get_cny_brief()})"
         }
         for model_display_name, model_name in models.items()
     ]
@@ -492,7 +494,6 @@ def delete_native_agent_configuration():
         return wrap_response(200, msg='Agent configuration deleted successfully')
 
 
-
 @app.route("/api/getModuleList", methods=["GET"])
 def get_module_list():
     """
@@ -509,7 +510,13 @@ def get_module_list():
 
     offset = (page - 1) * page_size
     with app.session_maker() as session:
-        query = session.query(ServiceModule).filter(ServiceModule.is_delete == 0)
+        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 = {
@@ -521,10 +528,11 @@ def get_module_list():
                 '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 in modules
+            for module, default_agent_name in modules
         ]
     }
     return wrap_response(200, data=ret_data)

+ 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)
+