RobotChat.java 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980
  1. package com.telerobot.fs.robot;
  2. import com.alibaba.fastjson.JSON;
  3. import com.alibaba.fastjson.JSONObject;
  4. import com.telerobot.fs.acd.AcdSqlQueue;
  5. import com.telerobot.fs.config.AppContextProvider;
  6. import com.telerobot.fs.config.SystemConfig;
  7. import com.telerobot.fs.config.ThreadLocalTraceId;
  8. import com.telerobot.fs.config.UuidGenerator;
  9. import com.telerobot.fs.entity.bo.InboundDetail;
  10. import com.telerobot.fs.entity.bo.LlmConsumer;
  11. import com.telerobot.fs.entity.dao.LlmKb;
  12. import com.telerobot.fs.entity.dto.AlibabaTokenEntity;
  13. import com.telerobot.fs.entity.dto.LlmAiphoneRes;
  14. import com.telerobot.fs.entity.dto.llm.AccountBaseEntity;
  15. import com.telerobot.fs.entity.po.CdrDetail;
  16. import com.telerobot.fs.entity.po.HangupCause;
  17. import com.telerobot.fs.entity.pojo.AsrProvider;
  18. import com.telerobot.fs.entity.pojo.LlmToolRequest;
  19. import com.telerobot.fs.entity.pojo.SpeechResultEntity;
  20. import com.telerobot.fs.entity.pojo.TtsProvider;
  21. import com.telerobot.fs.global.CdrPush;
  22. import com.telerobot.fs.service.InboundDetailService;
  23. import com.telerobot.fs.service.SysService;
  24. import com.telerobot.fs.tts.aliyun.AliyunTTSWebApi;
  25. import com.telerobot.fs.utils.CommonUtils;
  26. import com.telerobot.fs.utils.RegExp;
  27. import com.telerobot.fs.utils.ThreadUtil;
  28. import io.netty.util.internal.StringUtil;
  29. import link.thingscloud.freeswitch.esl.EslConnectionUtil;
  30. import link.thingscloud.freeswitch.esl.constant.EventNames;
  31. import link.thingscloud.freeswitch.esl.constant.UuidKeys;
  32. import link.thingscloud.freeswitch.esl.transport.event.EslEvent;
  33. import link.thingscloud.freeswitch.esl.transport.message.EslMessage;
  34. import org.apache.commons.lang.StringUtils;
  35. import java.io.UnsupportedEncodingException;
  36. import java.net.URLDecoder;
  37. import java.util.ArrayList;
  38. import java.util.List;
  39. import java.util.Map;
  40. /**
  41. * @author easycallcenter365@126.com
  42. */
  43. public class RobotChat extends RobotBase {
  44. private static int initApp = init();
  45. private static int init() {
  46. int maxRobotNumber = Integer.parseInt(SystemConfig.getValue("max-call-concurrency")) ;
  47. startRobotMainThreadPool(maxRobotNumber);
  48. startFsEsNotifyThreadPool(maxRobotNumber);
  49. startRobotStatThread();
  50. return 1;
  51. }
  52. public static int getInit() {
  53. return initApp;
  54. }
  55. public RobotChat(InboundDetail callDetail, AccountBaseEntity llmAccountInfo) {
  56. this.uuid = callDetail.getUuid();
  57. ThreadLocalTraceId.getInstance().setTraceId(uuid);
  58. this.setAsrModelType(SystemConfig.getValue("robot-asr-type", ASR_TYPE_MRCP));
  59. logger.info("{} current robot_asr_type={}.",
  60. getTraceId(), this.getAsrModelType()
  61. );
  62. logger.info("{} robot allow interrupt={}, interrupt_keywords={}, interrupt_ignore_keywords={}",
  63. getTraceId(), llmAccountInfo.interruptFlag == 1, llmAccountInfo.interruptKeywords, llmAccountInfo.interruptIgnoreKeywords
  64. );
  65. this.callDetail = callDetail;
  66. getEslConnectionPool(
  67. uuid,
  68. SystemConfig.getValue("event-socket-ip"),
  69. Integer.parseInt(SystemConfig.getValue("event-socket-port"))
  70. );
  71. callTaskList.put(uuid, this);
  72. createChatBot(llmAccountInfo.provider);
  73. chatRobot.setAccount(llmAccountInfo);
  74. chatRobot.setCallDetail(callDetail);
  75. chatRobot.setTtsProvider(llmAccountInfo.voiceSource);
  76. chatRobot.setTtsVoiceName(llmAccountInfo.voiceCode);
  77. if(getAllowInterrupt() && ASR_TYPE_MRCP.equalsIgnoreCase(this.getAsrModelType())){
  78. logger.error("{} `robot-speech-interrupt-allowed` is not effective in the mrcp speech recognition mode.", uuid);
  79. }
  80. EslConnectionUtil.sendExecuteCommand(
  81. "playback",
  82. "$${sounds_dir}/" + LLM_WAIT_WAV_FILE ,
  83. uuid
  84. );
  85. waitForPlayBackStartSignalForLlmConcurrency();
  86. long startTime = System.currentTimeMillis();
  87. logger.info("{} Try to acquire llm permit, llmAccountInfo.id={}, llmAccountInfo.concurrentNum={}.",
  88. getTraceId(), llmAccountInfo.id, llmAccountInfo.concurrentNum);
  89. llmConsumer = new LlmConsumer(uuid, llmAccountInfo.id, llmAccountInfo.concurrentNum);
  90. LlmThreadManager.acquire(llmConsumer);
  91. logger.info("{} Acquire llm permit successfully, took {} milliseconds. ", getTraceId(),
  92. System.currentTimeMillis() - startTime
  93. );
  94. int maxTry = 3;
  95. int tryCounter = 0;
  96. while(!checkSignalForLlmConcurrency() && tryCounter <= maxTry) {
  97. interruptRobotSpeech();
  98. waitForPlayBackStoppedSignalForLlmConcurrency();
  99. tryCounter++;
  100. }
  101. callDetail.setAnsweredTime(System.currentTimeMillis());
  102. AcdSqlQueue.addToSqlQueue(callDetail);
  103. }
  104. public void startProcess(String uniqueID) {
  105. if(callDetail.getOutboundPhoneInfo() != null) {
  106. // In the outbound call scenario, solve the problem that the first few words of the first sentence
  107. // cannot be heard clearly, because it takes about 2 seconds for the customer to transfer from
  108. // the receiver to the headphones after answering the call.
  109. ThreadUtil.sleep(200);
  110. if (isHangup) {
  111. return;
  112. }
  113. }
  114. String ttsProvider = chatRobot.getAccount().voiceSource;
  115. String asrProvider = chatRobot.getAccount().asrProvider;
  116. if(ttsProvider.equalsIgnoreCase(TtsProvider.ALIYUN) || asrProvider.equalsIgnoreCase(AsrProvider.ALIYUN)) {
  117. if((!AliyunTTSWebApi.setAliyunTokenToFreeSWITCH(uuid))) {
  118. String errMsg = "AliyunTTSWebApi getToken error!";
  119. logger.error("{} {} ", getTraceId(), errMsg);
  120. CommonUtils.setHangupCauseDetail(
  121. callDetail,
  122. HangupCause.TTS_ACCOUNT_INFO_INCORRECT,
  123. "error msg:" + errMsg
  124. );
  125. hangupAndCloseConn("AliyunTTSWebApi-getToken-error");
  126. return;
  127. }
  128. }
  129. if(ttsProvider.equalsIgnoreCase(TtsProvider.DOUBAO)) {
  130. logger.info("{} Current tts provider is doubao!", getTraceId());
  131. }
  132. if(ttsProvider.equalsIgnoreCase(TtsProvider.MICROSOFT)) {
  133. logger.info("{} Current tts provider is microsoft!", getTraceId());
  134. }
  135. if(ttsProvider.equalsIgnoreCase(TtsProvider.CHINA_TELECOM)) {
  136. logger.info("{} Current tts provider is china_telecom!", getTraceId());
  137. }
  138. EslMessage apiResponseMsg = EslConnectionUtil.sendSyncApiCommand(
  139. "uuid_exists",
  140. uniqueID,
  141. this.eslConnectionPool
  142. );
  143. if (apiResponseMsg != null && apiResponseMsg.getBodyLines().size() != 0) {
  144. String apiResponseText = apiResponseMsg.getBodyLines().get(0);
  145. if ("false".equalsIgnoreCase(apiResponseText)) {
  146. logger.info("{} session is hangup, try to stop robot process.", getTraceId());
  147. this.processFsMsg(this.generateHangupEvent("hangup-before-robot-process"));
  148. return;
  149. }
  150. } else {
  151. logger.info("{} uuid_exists check error, can not get apiResponseMsg...", getTraceId());
  152. }
  153. logger.info("{} start robot Process...", getTraceId());
  154. startAsrProcess(getAsrModelType(), false);
  155. interactWithRobot();
  156. }
  157. protected void processFsMsgEx(Map<String, String> headers) {
  158. String eventName = headers.get("Event-Name");
  159. String eventDateTimestamp = headers.get("Event-Date-Timestamp");
  160. if(null != eventDateTimestamp) {
  161. // esl消息从产生到被处理的延迟时间; 毫秒数
  162. long eventTime = Long.parseLong(eventDateTimestamp) / 1000L;
  163. long timeDelay = System.currentTimeMillis() - eventTime;
  164. logger.info("{} The [{}] event takes {} ms from generation to processing.", getTraceId(), eventName, timeDelay);
  165. }
  166. String eventSubClass = headers.get("Event-Subclass");
  167. String playbackFilePath = headers.get("Playback-File-Path");
  168. String detail = headers.get("Ecc365-Event-Detail");
  169. logger.info("{} Event-Name:{} ", getTraceId(), eventName);
  170. if(EventNames.PLAYBACK_START.equalsIgnoreCase(eventName)){
  171. if(EventNames.PLAYBACK_START.equalsIgnoreCase(detail)) {
  172. chatRobot.setTtsChannelState(TtsChannelState.OPENED);
  173. chatRobot.flushTtsRequestQueue();
  174. long timeSpent = System.currentTimeMillis() - playbackStartTime;
  175. logger.info("{} PLAYBACK_START event, time cost = {} ms. ", getTraceId(), timeSpent);
  176. }
  177. if(playbackFilePath != null && playbackFilePath.endsWith(LLM_WAIT_WAV_FILE)){
  178. releasePlayBackStartSignalForLlmConcurrency();
  179. logger.info("{} recv PLAYBACK_START event for wav file {}. ", getTraceId(), playbackFilePath);
  180. }
  181. }else if(EventNames.CHANNEL_PARK.equalsIgnoreCase(eventName))
  182. {
  183. logger.info("{} recv CHANNEL_PARK event. ", uuid);
  184. }
  185. else if (EventNames.PLAYBACK_STOP.equalsIgnoreCase(eventName)) {
  186. if(playbackFilePath != null && playbackFilePath.endsWith(LLM_WAIT_WAV_FILE)){
  187. releasePlayBackStoppedSignalForLlmConcurrency();
  188. logger.info("{} recv PLAYBACK_STOP event for wav file {}. ", getTraceId(), playbackFilePath);
  189. return;
  190. }
  191. if(EventNames.PLAYBACK_STOP.equalsIgnoreCase(detail)) {
  192. chatRobot.setTtsChannelState(TtsChannelState.CLOSED);
  193. recvPlayBackEndEvent = true;
  194. playbackEndTime = System.currentTimeMillis();
  195. releasePlayBackFinishedSignal();
  196. logger.info("{} streaming tts playback finished.", getTraceId());
  197. }
  198. if(recvHangupSignal){
  199. logger.info("{} The hang signal was received in the previous interaction process, and the call is about to hang up.",
  200. getTraceId());
  201. hangupAndCloseConn("recvHangupSignal");
  202. }
  203. }else if (EventNames.DTMF.equalsIgnoreCase(eventName)) {
  204. // get the dtmf key to check if its value is the same as
  205. // the key configured for manual transferring in the system.
  206. String digit = headers.get("DTMF-Digit");
  207. logger.info("{} recv DTMF event, digit = {}", getTraceId(), digit);
  208. String transferManualDigit = chatRobot.getAccount().transferManualDigit;
  209. if(transferManualDigit.equalsIgnoreCase(digit)){
  210. logger.info("{} DTMF digit equals transferManualDigit.", getTraceId());
  211. if (recvPlayBackEndEvent || getAllowInterrupt()) {
  212. if(!setTransferState()){
  213. return;
  214. }
  215. logger.info("{} The digit-key during call have successfully activated the condition " +
  216. "for transferring to a human operator. recvPlayBackEndEvent={}, getAllowInterrupt()={} ",
  217. getTraceId(), recvPlayBackEndEvent, getAllowInterrupt()
  218. );
  219. if(getAllowInterrupt() && !recvPlayBackEndEvent) {
  220. interruptRobotSpeech();
  221. releasePlayBackFinishedSignal();
  222. ThreadUtil.sleep(100);
  223. // wait for tts closed
  224. int step = 50;
  225. int maxWaitMills = 2000;
  226. int counter = 0;
  227. logger.info("{} wait for tts channel closed.", getTraceId());
  228. while (!ttsChannelClosed && !isHangup && counter <= maxWaitMills) {
  229. ThreadUtil.sleep(step);
  230. counter += step;
  231. }
  232. if(!ttsChannelClosed){
  233. ttsChannelClosed = true;
  234. chatRobot.setTtsChannelState(TtsChannelState.CLOSED);
  235. logger.warn("{} We haven't received the event of the TTS channel being closed within two seconds, we consider it to have been closed. .", getTraceId());
  236. }else{
  237. logger.info("{} tts channel is closed.", getTraceId());
  238. }
  239. }
  240. releaseSignal();
  241. getRobotMainThreadPool().execute(new Runnable() {
  242. @Override
  243. public void run() {
  244. doTransferToManualAgent(null);
  245. }
  246. });
  247. }else {
  248. logger.info("{} The digit-key during call have been failed to activate the condition " +
  249. "for transferring to a human operator. recvPlayBackEndEvent={}, getAllowInterrupt()={} ",
  250. getTraceId(), recvPlayBackEndEvent, getAllowInterrupt()
  251. );
  252. }
  253. }
  254. }else if (EventNames.CHANNEL_HANGUP.equalsIgnoreCase(eventName)) {
  255. if(isHangup){
  256. return;
  257. }
  258. releasePlayBackFinishedSignal();
  259. releaseSignal();
  260. isHangup = true;
  261. // 挂机后立即释放通道
  262. releaseThreadNum();
  263. releaseDtmf();
  264. displayNoVoiceNum();
  265. // 保存催记内容
  266. try {
  267. saveCdr(headers);
  268. } catch (Throwable e) {
  269. logger.info("{} save cdr error: {}, {} ", getTraceId(), e.toString(),
  270. CommonUtils.getStackTraceString(e.getStackTrace()));
  271. }
  272. if(!transferToAgent){
  273. CdrDetail cdrRecord = new CdrDetail();
  274. cdrRecord.setUuid(uuid);
  275. if(callDetail.getOutboundPhoneInfo() == null) {
  276. cdrRecord.setCdrType("inbound");
  277. }else{
  278. cdrRecord.setCdrType("outbound");
  279. }
  280. cdrRecord.setCdrBody(JSON.toJSONString(callDetail));
  281. CdrPush.addCdrToQueue(cdrRecord);
  282. }
  283. } else if ("CUSTOM".equalsIgnoreCase(eventName) && (
  284. "TtsEvent".equalsIgnoreCase(eventSubClass)
  285. )) {
  286. String event = headers.get("Tts-Event-Detail");
  287. if("Speech-Closed".equalsIgnoreCase(event)){
  288. chatRobot.setTtsChannelState(TtsChannelState.CLOSED);
  289. logger.info("{} TtsChannelClosed = true.", getTraceId());
  290. ttsChannelClosed = true;
  291. }
  292. if("Speech-Open".equalsIgnoreCase(event)){
  293. chatRobot.setTtsChannelState(TtsChannelState.OPENED);
  294. chatRobot.flushTtsRequestQueue();
  295. long timeSpent = System.currentTimeMillis() - playbackStartTime;
  296. logger.info("{} Speech-Open event, time cost = {} ms. ", getTraceId(), timeSpent);
  297. }
  298. if ("NetworkError".equalsIgnoreCase(event)) {
  299. CommonUtils.setHangupCauseDetail(
  300. callDetail,
  301. HangupCause.TTS_SERVER_CONNECTED_FAILED,
  302. headers.get("Error-Details")
  303. );
  304. logger.info("{} recv NetworkError event, hangup call session.", getTraceId());
  305. hangupAndCloseConn("Asr-TTs-NetworkError");
  306. }
  307. }
  308. else if ("CUSTOM".equalsIgnoreCase(eventName) && (
  309. "AsrEvent".equalsIgnoreCase(eventSubClass)
  310. )) {
  311. String speechEvent = headers.get("ASR-Event-Detail");
  312. String asrResponse = headers.get("Detect-Speech-Result");
  313. if (null != asrResponse) {
  314. asrResponse = headers.get("Detect-Speech-Result").trim();
  315. }
  316. if ("NetworkError".equalsIgnoreCase(speechEvent)) {
  317. CommonUtils.setHangupCauseDetail(
  318. callDetail,
  319. HangupCause.ASR_SERVER_CONNECTED_FAILED,
  320. asrResponse
  321. );
  322. hangupAndCloseConn("Asr-Tts-NetworkError");
  323. return;
  324. }
  325. lastTalkTime = System.currentTimeMillis();
  326. if (isHangup || interactiveParam.checkInHangupState() || transferToAgent) {
  327. logger.info("{} Session is going to be hangup or is already being transferred to human operator, drop asr result: {}", getTraceId(), asrResponse);
  328. return;
  329. }
  330. if (!getAllowInterrupt() && !recvPlayBackEndEvent) {
  331. if ("vad".equalsIgnoreCase(speechEvent)) {
  332. dropAsrCounter.incrementAndGet();
  333. logger.info("{} (vad event) drop asr result: {}", getTraceId(), asrResponse);
  334. } else {
  335. logger.info("{} (vad event) drop asr result: {}", getTraceId(), asrResponse);
  336. }
  337. return;
  338. }
  339. if ("middle".equalsIgnoreCase(speechEvent)) {
  340. logger.info("{} ** asr-websocket, begin-speaking ** {}", getTraceId(), asrResponse);
  341. logger.info("{} recv asr middle result event, recvPlayBackEndEvent={}, allowInterrupt={}, !checkSpeaking={}",
  342. getTraceId(),
  343. recvPlayBackEndEvent,
  344. getAllowInterrupt(),
  345. !interactiveParam.checkInSpeaking()
  346. );
  347. if (recvPlayBackEndEvent || getAllowInterrupt()) {
  348. if (!interactiveParam.checkInSpeaking()) {
  349. synchronized (getTraceId().intern()) {
  350. if (!interactiveParam.checkInSpeaking()) {
  351. interactiveParam.setInSpeaking(true);
  352. // Main thread awakened to extend customer speaking time beyond 6 seconds.
  353. logger.info("{} customer speech detected. ", getTraceId());
  354. if (chatRobot.getAccount().interruptFlag == 2) {
  355. interruptRobotSpeech();
  356. releasePlayBackFinishedSignal();
  357. ThreadUtil.sleep(100);
  358. }
  359. }
  360. }
  361. }
  362. }
  363. } else if ("vad".equalsIgnoreCase(speechEvent)) {
  364. logger.info("{} ** vad end-speaking: {}", getTraceId(), asrResponse);
  365. interactiveParam.setInSpeaking(false);
  366. if(StringUtils.isEmpty(asrResponse)){
  367. logger.error("{} error, vad result is null.", getTraceId());
  368. return;
  369. }
  370. if (!StringUtil.isNullOrEmpty(asrResponse)) {
  371. asrResultEx.add(asrResponse);
  372. }
  373. if(chatRobot.getAccount().interruptFlag == 1 && !recvPlayBackEndEvent) {
  374. if (checkSpeechInterrupt(asrResponse)) {
  375. interruptRobotSpeech();
  376. releasePlayBackFinishedSignal();
  377. ThreadUtil.sleep(100);
  378. }else{
  379. return;
  380. }
  381. }
  382. if(recvPlayBackEndEvent || getAllowInterrupt()){
  383. logger.info("{} releaseSignal for vad event.", getTraceId());
  384. releaseSignal();
  385. }
  386. }
  387. }
  388. }
  389. private boolean setTransferState() {
  390. synchronized (uuid.intern()) {
  391. if (transferToAgent) {
  392. logger.info("{} transferring to a human operator is already being handled. skip...", getTraceId());
  393. return false;
  394. }
  395. transferToAgent = true;
  396. return true;
  397. }
  398. }
  399. protected void interruptRobotSpeech(){
  400. logger.info("{} send uuid_break command to FreeSWITCH.", uuid);
  401. EslConnectionUtil.sendSyncApiCommand("uuid_break", uuid + " all");
  402. }
  403. @Override
  404. protected void processFsMsg(Map<String, String> headers) {
  405. try {
  406. processFsMsgEx(headers);
  407. } catch (Throwable e) {
  408. logger.error("{} processFsMsg error: {}, {}", getTraceId(), e.toString(),
  409. CommonUtils.getStackTraceString(e.getStackTrace()));
  410. }
  411. }
  412. protected void processFsMsg2(EslEvent event) {
  413. Map<String, String> headers = event.getEventHeaders();
  414. String eventName = headers.get("Event-Name");
  415. if (eventName.equalsIgnoreCase("DETECTED_SPEECH")) {
  416. String speechEvent = headers.get("Speech-Type");
  417. lastTalkTime = System.currentTimeMillis();
  418. if (isHangup || interactiveParam.checkInHangupState()) {
  419. logger.info("{} Session is going to be hangup, drop mrcp asr result: {}", getTraceId(),
  420. headers.get("detect_speech_result"));
  421. return;
  422. }
  423. if ("begin-speaking".equalsIgnoreCase(speechEvent)) {
  424. if (!interactiveParam.checkInSpeaking()) {
  425. logger.info(getTraceId() + " ** customer begin-speaking detected. **");
  426. // 用户开始讲话标识
  427. interactiveParam.setInSpeaking(true);
  428. releaseSignal(); // 唤醒主线程,让主线程可以超出6秒限制;
  429. }
  430. } else if ("detected-partial-speech".equalsIgnoreCase(speechEvent)) {
  431. synchronized (getTraceId().intern()) {
  432. if (!interactiveParam.checkInSpeaking()) {
  433. // 用户开始讲话标识
  434. interactiveParam.setInSpeaking(true);
  435. // 唤醒主线程,让主线程可以超出6秒限制;
  436. releaseSignal();
  437. }
  438. }
  439. // 语音识别的中间结果;
  440. logger.info("{} detected-partial-speech = {}", getTraceId(), headers.get("detect_speech_result"));
  441. } else if ("detected-speech".equalsIgnoreCase(speechEvent)) {
  442. if (!interactiveParam.checkInSpeaking()) {
  443. logger.info(getTraceId() + " mrcp return, no asr result got. isInSpeaking=false.");
  444. } else {
  445. logger.info(getTraceId() + " ****** customer stop-speaking detected. ******");
  446. }
  447. interactiveParam.setInSpeaking(false);
  448. String speechResult = headers.get("detect_speech_result");
  449. if(StringUtils.isEmpty(speechResult)){
  450. speechResult = CommonUtils.ListToString(event.getEventBodyLines(), false);
  451. }
  452. if (StringUtils.isEmpty(speechResult) || "Completion-Cause: 002".equalsIgnoreCase(speechResult.trim())) {
  453. logger.info(getTraceId() + " mrcp return, no asr result got...");
  454. releaseSignal();
  455. return;
  456. }
  457. if(speechResult.startsWith("<?xml")){
  458. parseMrcpResultXmlStr(speechResult);
  459. logger.info("{} detect_speech_result: {}", getTraceId(), speechResult);
  460. }else{
  461. try {
  462. String tmpResult = URLDecoder.decode(speechResult,"utf-8").replace(" ","");
  463. asrResultEx.add(tmpResult);
  464. logger.info("{} kaldi asr response: {}",getTraceId(), tmpResult);
  465. } catch (Throwable e) {
  466. logger.error("{} URLDecoder.decode Error: {}", getTraceId(), speechResult);
  467. }
  468. }
  469. releaseSignal();
  470. }
  471. }
  472. }
  473. /**
  474. * 解析unimrcp-client接收到的xml字符串;
  475. * @param xmlStr
  476. */
  477. private void parseMrcpResultXmlStr(String xmlStr){
  478. SpeechResultEntity resultEntity = null;
  479. try {
  480. String input = URLDecoder.decode(xmlStr, "utf-8");
  481. resultEntity = getSpeechResult(input);
  482. } catch (UnsupportedEncodingException e) {
  483. }
  484. if (null != resultEntity) {
  485. if (!StringUtils.isEmpty(resultEntity.getResult())) {
  486. String asrResult = resultEntity.getResult();
  487. if(!StringUtil.isNullOrEmpty(asrResult)) {
  488. asrResultEx.add(asrResult);
  489. }
  490. logger.info(getTraceId() + " mrcp asr result: {}, requestid: {}", resultEntity.getResult(), resultEntity.getRequestId());
  491. }
  492. } else {
  493. logger.info(getTraceId() + " cant not parse variable detect_speech_result.");
  494. }
  495. }
  496. @Override
  497. public void eventReceived(String addr, EslEvent event) {
  498. fsEsNotifyThreadPool.execute(new Runnable() {
  499. @Override
  500. public void run() {
  501. Map<String, String> headers = event.getEventHeaders();
  502. boolean mrcpResult = EventNames.DETECTED_SPEECH.equalsIgnoreCase(headers.get("Event-Name"));
  503. if (getAsrModelType().equalsIgnoreCase(ASR_TYPE_MRCP) && mrcpResult) {
  504. processFsMsg2(event);
  505. } else {
  506. // output of FreeSWITCH websocket asr module
  507. processFsMsg(headers);
  508. }
  509. }
  510. });
  511. }
  512. @Override
  513. public String context() {
  514. return this.getClass().getName();
  515. }
  516. @Override
  517. public void backgroundJobResultReceived(String addr, EslEvent event) {
  518. }
  519. /**
  520. * interactWithRobot
  521. **/
  522. private void interactWithRobot() {
  523. if (checkCallSession()) {
  524. return;
  525. }
  526. interactiveParam.setAllowInterrupt(0);
  527. recvPlayBackEndEvent = false;
  528. firstSpeak = false;
  529. interactiveParam.setInSpeaking(false);
  530. if(getAsrModelType().equalsIgnoreCase(ASR_TYPE_WEBSOCKET)) {
  531. pauseAsr();
  532. }
  533. // 送robot理解客户意图,返回合成后的语音文件路径
  534. StringBuilder asrStr = new StringBuilder();
  535. for (String result : asrResultEx) {
  536. asrStr.append(result);
  537. }
  538. // 清空 asrResultEx; 重新初始化字段;
  539. asrResultEx.clear();
  540. // 识别开始时间
  541. Long startTime = System.currentTimeMillis();
  542. LlmAiphoneRes aiphoneRes;
  543. try {
  544. String question = asrStr.toString();
  545. if (StringUtils.isEmpty(question)) {
  546. int counter = noVoiceCounter.incrementAndGet();
  547. if (counter > MAX_CONSECUTIVE_NO_VOICE_NUMBER) {
  548. logger.info("{} There has been no sound for {} consecutive times. Play hangupTips and then hangup call.",
  549. getTraceId(), MAX_CONSECUTIVE_NO_VOICE_NUMBER);
  550. chatRobot.sendTtsRequest(chatRobot.getAccount().hangupTips);
  551. chatRobot.closeTts();
  552. recvHangupSignal = true;
  553. return;
  554. }
  555. }else{
  556. if(noVoiceCounter.get() > 1) {
  557. noVoiceCounter.set(0);
  558. }
  559. }
  560. logger.info("{} send question to chatRobot: {}", getTraceId(), question);
  561. aiphoneRes = chatRobot.talkWithAiAgent(question, kbQueryExecuted);
  562. while ((aiphoneRes == null || aiphoneRes.getStatus_code() == 0)
  563. && Llm_max_try_counter.get() < LLM_MAX_TRY) {
  564. logger.error("{} llm api error, retry to send question to chatRobot: {}", getTraceId(), question);
  565. aiphoneRes = chatRobot.talkWithAiAgent(question, kbQueryExecuted);
  566. Llm_max_try_counter.incrementAndGet();
  567. if (checkCallSession()) {
  568. return;
  569. }
  570. }
  571. Llm_max_try_counter.set(0);
  572. kbQueryExecuted = false;
  573. if (aiphoneRes == null || aiphoneRes.getStatus_code() == 0) {
  574. String tips = SystemConfig.getValue("llm-max-try-fail-tips", "");
  575. if (!StringUtils.isEmpty(tips)) {
  576. chatRobot.sendTtsRequest(tips);
  577. chatRobot.closeTts();
  578. } else {
  579. CommonUtils.setHangupCauseDetail(
  580. callDetail,
  581. HangupCause.LLM_API_SERVER_ERROR,
  582. String.format("The large model failed to access successfully despite more than %d connection attempts.", LLM_MAX_TRY)
  583. );
  584. hangupAndCloseConn("reach-llm-max-try-error");
  585. return;
  586. }
  587. }
  588. talkRound.increment();
  589. Long spentCost = System.currentTimeMillis() - startTime;
  590. logger.info("{} talkWithLargeModel spent time: {} ms, aiphoneRes = {}",
  591. getTraceId(), spentCost, JSON.toJSONString(aiphoneRes)
  592. );
  593. if(aiphoneRes != null && aiphoneRes.getStatus_code() == 1) {
  594. ttsChannelClosed = false;
  595. String body = aiphoneRes.getBody();
  596. if(!StringUtils.isEmpty(body)){
  597. if(body.contains(LlmToolRequest.TRANSFER_TO_AGENT)){
  598. aiphoneRes.setTransferToAgent(1);
  599. body = body.replace(LlmToolRequest.TRANSFER_TO_AGENT, "");
  600. }
  601. if(body.contains(LlmToolRequest.HANGUP)){
  602. aiphoneRes.setClose_phone(1);
  603. body = body.replace(LlmToolRequest.HANGUP, "");
  604. }
  605. if(body.contains(LlmToolRequest.TRANSFER_TO_TEL)){
  606. aiphoneRes.setTransferToAgent(1);
  607. }
  608. if(body.contains(LlmToolRequest.KB_QUERY + "=")){
  609. kbQueryExecuted = true;
  610. int catId = chatRobot.getAccount().kbCatId;
  611. String title = body.replace(LlmToolRequest.KB_QUERY + "=", "").replace(" ","");
  612. LlmKb kb = AppContextProvider.getBean(SysService.class).getKbContentByCat(catId, title);
  613. String response = "No relevant topics were found.";
  614. if(kb != null){
  615. response = kb.getContent();
  616. logger.info("{} 1 relevant topics {} were found: {} ", getTraceId(), title, response.substring(0, 10));
  617. }
  618. JSONObject userMessage = new JSONObject();
  619. userMessage.put("role", "system");
  620. userMessage.put("content", "kbQuery response:" + response);
  621. userMessage.put("content_type", "text");
  622. chatRobot.getDialogues().add(userMessage);
  623. interactWithRobot();
  624. return;
  625. }
  626. }
  627. if (checkCallSession()) {
  628. return;
  629. }
  630. if (aiphoneRes.getTransferToAgent() == 1) {
  631. if(!setTransferState()){
  632. return;
  633. }
  634. doTransferToManualAgent(body);
  635. return;
  636. }
  637. if (aiphoneRes.getClose_phone() == 1) {
  638. if(StringUtils.isEmpty(body)){
  639. chatRobot.sendTtsRequest(chatRobot.getAccount().hangupTips);
  640. }
  641. chatRobot.closeTts();
  642. waitForPlayBackFinished(11000);
  643. long startTimeTick = System.currentTimeMillis();
  644. while (!ttsChannelClosed && !isHangup) {
  645. ThreadUtil.sleep(1000);
  646. if(System.currentTimeMillis() - startTimeTick > 11000){
  647. break;
  648. }
  649. }
  650. hangupAndCloseConn("system-hangup");
  651. return;
  652. }
  653. }
  654. } catch (Throwable e) {
  655. logger.error("{} talkWithLargeModel error! {} {} ",
  656. getTraceId(), e.toString(), CommonUtils.getStackTraceString(e.getStackTrace())
  657. );
  658. CommonUtils.setHangupCauseDetail(
  659. callDetail,
  660. HangupCause.SYSTEM_INTERNAL_ERROR,
  661. String.format("server error: %s", e.toString())
  662. );
  663. hangupAndCloseConn(HangupCause.SYSTEM_INTERNAL_ERROR.getCode());
  664. return;
  665. }
  666. if(aiphoneRes != null && aiphoneRes.getIfcan_interrupt() == 1) {
  667. interactiveParam.setAllowInterrupt(1);
  668. logger.info("{} allowSpeechInterrupt={}", getTraceId(), 1);
  669. }
  670. if (!interactiveParam.checkInHangupState()) {
  671. if (aiphoneRes.getClose_phone() == 1) {
  672. logger.info(getTraceId() + " hangup signal is detected. ");
  673. interactiveParam.setInHangUpState(true);
  674. recvHangupSignal = true;
  675. } else {
  676. waitForCustomerSpeakEx();
  677. }
  678. }
  679. }
  680. private void waitAndDetectSpeaking(){
  681. if (interactiveParam.checkInSpeaking()){
  682. logger.info("{} Speaking is detected, Wait for customer to finish speaking. Timeout: {} ",
  683. getTraceId(),
  684. maxWaitTimeMills
  685. );
  686. acquire(maxWaitTimeMills);
  687. }
  688. }
  689. private synchronized void doTransferToManualAgent(String audioTipsText){
  690. if(transferToAgentExecuted){
  691. logger.warn("{} The call transfer to a human agent has already been processed.", getTraceId());
  692. return;
  693. }
  694. transferToAgentExecuted = true;
  695. callDetail.setChatContent(chatRobot.getDialogues());
  696. // Replace the prompt words for manual transfer in the text with blank spaces.
  697. String transferToTel = "";
  698. if(!StringUtils.isEmpty(audioTipsText) && audioTipsText.contains(LlmToolRequest.TRANSFER_TO_TEL)){
  699. if(!TransferToAgent.TRANSFER_TO_GATEWAY.equalsIgnoreCase(chatRobot.getAccount().aiTransferType)){
  700. logger.error("{} instruction `{}` is only applicable when an external gateway is used to transfer to a manual agent.",
  701. uuid, LlmToolRequest.TRANSFER_TO_TEL);
  702. hangupAndCloseConn("llm-instruction-error");
  703. return;
  704. }
  705. List<String> matches = RegExp.GetMatchFromStringByRegExp(audioTipsText, LlmToolRequest.TRANSFER_TO_TEL_REGEXP);
  706. for (String match : matches) {
  707. audioTipsText = audioTipsText.replace(match, "");
  708. List<String> tmp = RegExp.GetMatchFromStringByRegExp(match, "\\d{7,12}");
  709. transferToTel = tmp.get(0);
  710. logger.info("{} Successfully retrieved transferToTel number {}", uuid, transferToTel);
  711. JSONObject jsonObject = JSON.parseObject(chatRobot.getAccount().aiTransferData);
  712. jsonObject.put("destNumber", transferToTel);
  713. chatRobot.getAccount().aiTransferData = JSON.toJSONString(jsonObject);
  714. logger.info("{} Successfully update aiTransferData: {} ", uuid, chatRobot.getAccount().aiTransferData);
  715. }
  716. }
  717. if(StringUtils.isEmpty(audioTipsText)){
  718. String tips = chatRobot.getAccount().transferToAgentTips;
  719. logger.info("{} Try to play tts transferToAgentTips {}", getTraceId(), tips);
  720. chatRobot.sendTtsRequest(tips);
  721. chatRobot.closeTts();
  722. waitForPlayBackFinished(6000);
  723. // wait for tips playback finished
  724. }
  725. // stop_asr 的顺序很重要,需要放在播放tts之后,否则不起作用;会被uuid_break清空指令;
  726. logger.info("{} Try to stop asr {}", getTraceId(), chatRobot.getAccount().asrProvider);
  727. EslConnectionUtil.sendExecuteCommand(
  728. String.format("stop_%s_asr", chatRobot.getAccount().asrProvider), "", uuid);
  729. ThreadUtil.sleep(200);
  730. if(!isHangup) {
  731. releaseThreadNum();
  732. TransferToAgent.transfer(callDetail, chatRobot.getAccount());
  733. }
  734. }
  735. /**
  736. * Check if the call has been hung up or has been transferred to a human handler.
  737. * @return
  738. */
  739. private boolean checkCallSession(){
  740. return isHangup || transferToAgent;
  741. }
  742. /**
  743. * Play TTS and wait for the customer to speak.
  744. */
  745. private void waitForCustomerSpeakEx() {
  746. if (checkCallSession()) {
  747. return;
  748. }
  749. logger.info("{} enter into waitForCustomerSpeak ...", getTraceId());
  750. // The duration of streaming TTS playback should not exceed 181 seconds.
  751. if (!recvPlayBackEndEvent) {
  752. logger.info("{} enter into waitForPlayBackFinished ...", getTraceId());
  753. waitForPlayBackFinished();
  754. }
  755. if (checkCallSession()) {
  756. return;
  757. }
  758. if (!recvPlayBackEndEvent) {
  759. logger.info("{} robot speech interrupt detected. ", getTraceId());
  760. } else {
  761. logger.info("{} robot speech playback finished. ", getTraceId());
  762. }
  763. if (getAsrModelType().equalsIgnoreCase(ASR_TYPE_WEBSOCKET)) {
  764. resumeAsr();
  765. }
  766. if (getAsrModelType().equalsIgnoreCase(ASR_TYPE_MRCP)) {
  767. startMrcp();
  768. }
  769. long startWaitTimeMills = System.currentTimeMillis();
  770. logger.info("{} wait for customer speaking ...", getTraceId());
  771. acquire(7000);
  772. logger.info("{} wait for customer speaking, time passed = {}ms. ...",
  773. getTraceId(),
  774. System.currentTimeMillis() - startWaitTimeMills
  775. );
  776. if (checkCallSession()) {
  777. return;
  778. }
  779. logger.info("{} enter into waitAndDetectSpeaking ...", getTraceId());
  780. // If speech is detected within 7 seconds, continue waiting.
  781. waitAndDetectSpeaking();
  782. logger.info(getTraceId() + " Robot main thread has woken up.");
  783. if (checkCallSession()) {
  784. return;
  785. }
  786. if (!interactiveParam.checkInSpeaking()) {
  787. // 前面的流程都正常,客户讲话有中间结果,且有最终的vad结果;
  788. // 根据vad结果产生不同的时间段,计算不同的应继续等待时间;
  789. long waitMills = calcWaitSecsDuration6Secs();
  790. if (waitMills > 100L) {
  791. logger.info("{} Wait another {} milliseconds to ensure the customer is finished speaking. ",
  792. getTraceId(),
  793. waitMills
  794. );
  795. acquire(waitMills);
  796. logger.info("{} enter into waitAndDetectSpeaking ...", getTraceId());
  797. waitAndDetectSpeaking();
  798. }
  799. }else{
  800. // The customer's speech is not over yet.
  801. logger.info("{} Oh, it seems that the customer might still be speaking. ", uuid);
  802. while (interactiveParam.checkInSpeaking()){
  803. acquire(100);
  804. }
  805. }
  806. if (checkCallSession()) {
  807. return;
  808. }
  809. //如果没有接收到asr识别结果,则延迟下,继续等待0.5秒钟:
  810. if (asrResultEx.size() == 0) {
  811. acquire(500);
  812. }
  813. if (checkCallSession()) {
  814. return;
  815. }
  816. if (asrResultEx.size() == 0) {
  817. logger.info("{} No asr result got: NO_VOICE ", getTraceId());
  818. } else {
  819. calleeSpeakNumber.incrementAndGet();
  820. }
  821. interactRounds.incrementAndGet();
  822. int muteTimeLong = (int) (System.currentTimeMillis() - startWaitTimeMills);
  823. logger.info("{} The time spent waiting for the customer to finish speaking is {} ms.", getTraceId(), muteTimeLong);
  824. interactWithRobot();
  825. }
  826. /**
  827. * 显示本通电话的 No_Voice轮次,打印 语音识别连接是否成功信息;
  828. */
  829. private void displayNoVoiceNum() {
  830. String tips = "";
  831. if (calleeSpeakNumber.get() == 0) {
  832. tips = "No speech was detected all over the session.";
  833. }
  834. logger.info("{} {} calleeSpeakNumber:{}, interactRounds:{}, NO_VOICE_NUMBER:{}, dropAsrCounter:{}, wavFile: {}, recordings file exists:{}",
  835. getTraceId(),
  836. tips,
  837. calleeSpeakNumber.get(),
  838. interactRounds.get(),
  839. interactRounds.get() - calleeSpeakNumber.get(),
  840. dropAsrCounter.get(),
  841. this.recordingsFileName,
  842. interactRounds.get() != 0 ? new java.io.File(this.recordingsFileName).exists() : "No talk interaction."
  843. );
  844. }
  845. /**
  846. * saveCdr
  847. */
  848. private void saveCdr(Map<String, String> headers) {
  849. String hangupCause = headers.get("Hangup-Cause");
  850. String sipCode = headers.get("variable_proto_specific_hangup_cause");
  851. logger.info("{} session is hangup, hangupCause={}, sipCode={}", getTraceId(), hangupCause, sipCode);
  852. callDetail.setExtnum("robot");
  853. callDetail.setOpnum("robot");
  854. callDetail.setHangupTime(System.currentTimeMillis());
  855. callDetail.setChatContent(chatRobot.getDialogues());
  856. long timeLen = System.currentTimeMillis() - callDetail.getInboundTime();
  857. long answeredTimeLen = System.currentTimeMillis() - callDetail.getAnsweredTime();
  858. callDetail.setTimeLen(timeLen);
  859. callDetail.setAnsweredTimeLen(answeredTimeLen);
  860. if(StringUtils.isEmpty(chatRobot.getCallDetail().getHangupCause())){
  861. CommonUtils.setHangupCauseDetail(
  862. callDetail,
  863. hangupCause,
  864. "sip-code=" + sipCode
  865. );
  866. }
  867. AcdSqlQueue.addToSqlQueue(callDetail);
  868. }
  869. }