Преглед на файлове

Merge branch 'master' of ssh://gitlab.fuxicarbon.com:1111/client_service/voice-gateway-service

刘威 преди 6 месеца
родител
ревизия
86fdcf559e
променени са 2 файла, в които са добавени 267 реда и са изтрити 19 реда
  1. 126 0
      src/core/callcenter/model.py
  2. 141 19
      src/core/voip/bot.py

+ 126 - 0
src/core/callcenter/model.py

@@ -461,3 +461,129 @@ class RouteGateway:
     #             f"called_prefix={self.called_prefix}, profile={self.profile}, "
     #             f"sip_header1={self.sip_header1}, sip_header2={self.sip_header2}, "
     #             f"sip_header3={self.sip_header3}, status={self.status})")
+
+
+
+
+
+
+
+#机器人外呼
+class BotChatRequest:
+    def __init__(self, node_id=None, user_id=None, session_id=None, record_id=None,
+                 task_id=None, event_type=None, asr_text=None, key_input=None):
+        self.node_id = node_id  # 节点id
+        self.user_id = user_id  # 用户id
+        self.session_id = session_id  # 会话id
+        self.record_id = record_id  # 唯一标识
+        self.task_id = task_id  # 机器人任务id
+        self.event_type = event_type  # 1:电话接通,2:用户语音asr结果上传,3:用户按键输入,4:用户挂断电话,5:读取外呼结果(可在外呼过程中调用,不一定在结束之后),6:用户不说话超时,7:TTS或录音文件播放完毕或被打断,8:ASR错误导致挂断电话,9:TTS错误导致挂断电话,10:其它系统错误导致挂断电话
+        self.asr_text = asr_text  # asr识别的文本
+
+    def to_json_string(self):
+        return json.dumps(self.__dict__, ensure_ascii=False)
+
+    @classmethod
+    def from_json(cls, json_string):
+        data = json.loads(json_string)
+        return cls(**data)
+
+    # def __repr__(self):
+    #     return (f"OutboundRobotRequestParams(node_id={self.node_id}, user_id={self.user_id}, "
+    #             f"session_id={self.session_id}, record_id={self.record_id}, task_id={self.task_id}, "
+    #             f"event_type={self.event_type}, asr_text={self.asr_text}, key_input={self.key_input})")
+
+
+
+class ChatAction:
+    def __init__(self, action_code=None, action_content=None):
+        self.action_code = action_code         # normal:正常通话;hang:挂断;transfer:转人工
+        self.action_content = action_content   # 动作内容
+
+    def to_json_string(self):
+        return json.dumps(self.__dict__, ensure_ascii=False)
+
+    @classmethod
+    def from_json(cls, json_data):
+        return cls(
+            action_code=json_data.get("action_code"),
+            action_content=json_data.get("action_content")
+        )
+
+
+class ChatContent:
+    def __init__(self, content_type=None, content=None, voice_url=None, voice_content=None):
+        self.content_type = content_type   # 播放类型
+        self.content = content             # 播放内容
+        self.voice_url = voice_url         # 语音地址
+        self.voice_content = voice_content # 语音文本
+
+    def to_json_string(self):
+        return json.dumps(self.__dict__, ensure_ascii=False)
+
+    @classmethod
+    def from_json(cls, json_data):
+        return cls(
+            content_type=json_data.get("content_type"),
+            content=json_data.get("content"),
+            voice_url=json_data.get("voice_url"),
+            voice_content=json_data.get("voice_content")
+        )
+
+
+class ChatMessage:
+    def __init__(self, node_id=None, contents=None, interruptable=None, wait_time=None,
+                 action=None, talk_time_out=None):
+        self.node_id = node_id   # 节点id
+        self.contents = contents if contents is not None else []   # 内容列表
+        self.interruptable = interruptable   # 是否可打断
+        self.wait_time = wait_time   # 用户静默时长
+        self.action = action        # 动作代码
+        self.talk_time_out = talk_time_out     # 用户通话时长
+
+    def to_json_string(self):
+        return json.dumps({
+            "node_id": self.node_id,
+            "contents": [content.__dict__ for content in self.contents],
+            "interruptable": self.interruptable,
+            "wait_time": self.wait_time,
+            "action": self.action.__dict__ if self.action else None,
+            "talk_time_out": self.talk_time_out
+        }, ensure_ascii=False)
+
+    @classmethod
+    def from_json(cls, json_data):
+        contents = [ChatContent.from_json(item) for item in json_data.get("contents", [])]
+        action = ChatAction.from_json(json_data.get("action", {})) if json_data.get("action") else None
+        return cls(
+            node_id=json_data.get("node_id"),
+            contents=contents,
+            interruptable=json_data.get("interruptable"),
+            wait_time=json_data.get("wait_time"),
+            action=action,
+            talk_time_out=json_data.get("talk_time_out")
+        )
+
+
+class ChatResponse:
+    def __init__(self, data=None, message=None, code=None):
+        self.data = data if data is not None else ChatMessage()
+        self.message = message
+        self.code = code
+
+    def to_json_string(self):
+        return json.dumps({
+            "data": json.loads(self.data.to_json_string()),
+            "message": self.message,
+            "code": self.code
+        }, ensure_ascii=False)
+
+    @classmethod
+    def from_json(cls, json_string):
+        data = json.loads(json_string)
+        response_data = ChatMessage.from_json(data.get("data", {}))
+        return cls(
+            data=response_data,
+            message=data.get("message"),
+            code=data.get("code")
+        )

+ 141 - 19
src/core/voip/bot.py

@@ -12,10 +12,14 @@ import pjsua2 as pj
 from enum import Enum
 from src.core.voip.constant import *
 
+import requests
+from src.core.callcenter.model import BotChatRequest, ChatResponse
+
+
 calls = {}
 # recording_file = '/code/src/core/voip/incoming_call.wav'
-# player_file = '/code/src/core/voip/test222.wav'
-
+player_file1 = '/code/src/core/voip/test111.wav'
+player_file2 = '/code/src/core/voip/test222.wav'
 
 class BotStatus(Enum):
     # 等待用户讲话,未超时
@@ -68,10 +72,11 @@ class MyAudioMediaPort(pj.AudioMediaPort):
             if asr_text and not play_complete:
                 self.user_asr_texts.append(asr_text)
             if asr_text and play_complete:
+                self.cur_player_file = None
                 self.user_asr_texts.append(asr_text)
                 user_asr_text = asr_text if len(self.user_asr_texts) == 1 else '###'.join(self.user_asr_texts)
                 self.user_asr_texts.clear()
-                self.call.chat(user_asr_text)
+                self.call.chat("2",user_asr_text)
 
             player_queue_size = self.call.player_queue.qsize()
             if (player_queue_size > 0 and not self.cur_player_file) or (player_queue_size > 0 and play_complete):
@@ -154,6 +159,7 @@ class MyCall(pj.Call):
         self.player = None
         self.asr = None
 
+
         self.scripts = build_demo_script()
         self.user_asr_text_queue = queue.Queue(maxsize=100)
         self.player_queue = queue.Queue(maxsize=3)
@@ -181,9 +187,9 @@ class MyCall(pj.Call):
         # pj.PJSIP_INV_STATE_CONFIRMED
         # pj.PJSIP_INV_STATE_DISCONNECTED
 
-        # if call_info.state == pj.PJSIP_INV_STATE_CONFIRMED:
-        #     # 当呼叫状态为已确认(即接通)
-        #     self.bot_say_hello()
+        if call_info.state == pj.PJSIP_INV_STATE_CONFIRMED:
+            # 当呼叫状态为已确认(即接通)
+            self.bot_say_hello()
 
         if call_info.state == pj.PJSIP_INV_STATE_DISCONNECTED:
             print("通话结束")
@@ -201,7 +207,7 @@ class MyCall(pj.Call):
                 try:
                     # 建立双向通道
                     self.receive_user_speaker()
-                    self.bot_say_hello()
+                    # self.bot_say_hello()
                 except Exception as e:
                     traceback.print_exc()
 
@@ -220,7 +226,7 @@ class MyCall(pj.Call):
         self.player.startTransmit(self.aud_med)
 
     def on_receiver_asr_result(self, message, *args):
-        print(message)
+        print('asr返回内容:',message)
         self.user_asr_text_queue.put(message)
 
     def on_media_player_complete(self, player_id):
@@ -229,15 +235,131 @@ class MyCall(pj.Call):
 
     def bot_say_hello(self):
         print('bot_say_hello, come in ')
-        self.chat(user_asr_text="SAY_HELLO")
+        self.chat('1', user_asr_text="SAY_HELLO")
 
-    def chat(self, user_asr_text=None):
+    def chat(self,event_type, user_asr_text=None):
         # TODO 调用文本机器人接口
-        message = {'player_file': self.scripts.get()}
-        player = message.get('player_file')
-        print('chat::player_file=', player)
-        self.player_queue.put(player)
+        # message = {'player_file': self.scripts.get()}
+        # player = message.get('player_file')
+        # print('chat::player_file=', player)
+        # self.player_queue.put(player)
+        # 调用文本机器人接口
+        ToTextBotAgent(event_type,user_asr_text,self)
+
+class ToTextBotAgent:
+    def __init__(self, event_type, user_asr_text, call_agent):
+        if not user_asr_text:
+            print("ASR文本为空,终止执行。")
+            return
 
+        self.call_agent = call_agent
+        self.request_data = BotChatRequest(
+            node_id="1",
+            user_id="139311",
+            session_id="1",
+            record_id="2",
+            task_id="ceshi",
+            event_type=event_type,  # 传入不同的 event_type 来测试不同的响应
+            asr_text=user_asr_text
+        )
+        # 发送请求并处理响应
+        self.test_request(self.request_data)
+
+    def to_hang(self, action_content):
+        user_part = self.call_agent.user_part_pool.get()
+        self.call_agent.hangup(user_part, action_content)
+
+    def transfer_people(self):
+        print('todo 转人工')
+
+    def handling_release(self, response: ChatResponse):
+        print('handling_release', response.data)
+        if response:
+            data = response.data
+            action = data.action
+            action_code = action.action_code
+            print(f"ActionCode: {action_code}", data)
+            if action_code == 'hang':  # 挂断
+                action_content = action.action_content
+                self.to_hang(action_content)
+            elif action_code == 'transfer':  # 转人工
+                self.transfer_people()
+            elif action_code == 'normal':  # 正常通话
+                contents = data.contents
+                for content in contents:
+                    if content.content_type == 'voice' and content.voice_url:
+                        self.call_agent.player_queue.put(content.voice_url)
+                # 等待几秒以后重新调用文本机器人接口
+                wait_time = data.wait_time
+                print(f"成功获取响应: ActionCode: {action_code}, WaitTime: {wait_time}")
+        else:
+            print("文本机器人接口调用失败")
+
+    def to_quest(self, request: BotChatRequest):
+        # 将实体类转换为JSON字符串
+        headers = {'Content-Type': 'application/json'}
+        request_json = request.to_json_string()
+        # 发送POST请求
+        try:
+            response = requests.post('http://example.com/api', data=request_json, headers=headers)  # 使用占位URL
+            if response.status_code == 200:
+                # 成功,解析响应JSON
+                response_data = response.json()
+                return ChatResponse.from_json(json.dumps(response_data))  # 转换为JSON字符串并解析
+            else:
+                # 错误处理
+                print(f"请求失败,状态码: {response.status_code}, 响应内容: {response.text}")
+                return None
+        except requests.RequestException as e:
+            traceback.print_exc()
+            return None
+
+# 模拟接口请求返回
+    def test_request(self, params: BotChatRequest):
+        print("test_request::params=", params)
+        response_data = {
+            "node_id": "1.0",
+            "contents": [],
+            "interruptable": False,
+            "wait_time": "6",
+            "action": {
+                "action_code": "normal",
+                "action_content": "正常通话"
+            },
+            "talk_time_out": ""
+        }
+
+        print("params.event_type:", params.event_type)
+        if params.event_type == '1':
+            response_data['contents'].append({
+                "content_type": "voice",
+                "content": "",
+                "voice_url": '/code/src/core/voip/test111.wav',
+                "voice_content": "五一北京到上海的高铁票还有吗?"
+            })
+        elif params.event_type == '2':
+            response_data['contents'].append({
+                "content_type": "voice",
+                "content": "",
+                "voice_url": '/code/src/core/voip/test222.wav',
+                "voice_content": "测试第二个录音文件"
+            })
+
+        response = {
+            "data": response_data,
+            "message": "success",
+            "code": 200
+        }
+        try:
+            # 转换为JSON字符串并解析
+            response_json = json.dumps(response)
+            parsed_response = ChatResponse.from_json(response_json)
+            print(f"Response in test_request: {parsed_response}")  # 确认response
+            self.handling_release(parsed_response)
+        except Exception as e:
+            print(f"Error in test_request: {e}")
+            traceback.print_exc()  # 打印完整的错误信息
+            return None
 
 class BotAgent:
 
@@ -259,8 +381,8 @@ class BotAgent:
         ep_cfg.uaConfig.maxCalls = 10
         ep_cfg.uaConfig.maxAccounts = 10
         ep_cfg.medConfig.noVad = True
-        ep_cfg.logConfig.level = 5
-        ep_cfg.logConfig.consoleLevel = 5
+        ep_cfg.logConfig.level = 4
+        ep_cfg.logConfig.consoleLevel = 4
         self.ep.libCreate()
         self.ep.libInit(ep_cfg)
 
@@ -285,9 +407,9 @@ class BotAgent:
             acfg.regConfig.retryIntervalSec = 10  # 重试间隔时间(秒)
             acfg.regConfig.firstRetryIntervalSec = 10  # 首次重试间隔时间(秒)
 
-            # acfg.natConfig.iceEnabled = True
-            # acfg.natConfig.turnEnabled = True
-            # acfg.natConfig.turnServer = "stun:192.168.100.159:3478"
+            acfg.natConfig.iceEnabled = True
+            acfg.natConfig.turnEnabled = True
+            acfg.natConfig.turnServer = "stun:192.168.100.159:3478"
             # acfg.natConfig.turnUsername = "username"
             # acfg.natConfig.turnPassword = "password"