RobotChat.java 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288
  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.*;
  6. import com.telerobot.fs.entity.bo.InboundDetail;
  7. import com.telerobot.fs.entity.bo.LlmConsumer;
  8. import com.telerobot.fs.entity.dao.LlmKb;
  9. import com.telerobot.fs.entity.dto.AlibabaTokenEntity;
  10. import com.telerobot.fs.entity.dto.LlmAiphoneRes;
  11. import com.telerobot.fs.entity.dto.llm.AccountBaseEntity;
  12. import com.telerobot.fs.entity.po.CdrDetail;
  13. import com.telerobot.fs.entity.po.HangupCause;
  14. import com.telerobot.fs.entity.pojo.*;
  15. import com.telerobot.fs.global.CdrPush;
  16. import com.telerobot.fs.robot.impl.LocalWebApiTest;
  17. import com.telerobot.fs.service.InboundDetailService;
  18. import com.telerobot.fs.service.SysService;
  19. import com.telerobot.fs.tts.aliyun.AliyunTTSWebApi;
  20. import com.telerobot.fs.utils.CommonUtils;
  21. import com.telerobot.fs.utils.RegExp;
  22. import com.telerobot.fs.utils.ThreadUtil;
  23. import io.netty.util.internal.StringUtil;
  24. import link.thingscloud.freeswitch.esl.EslConnectionUtil;
  25. import link.thingscloud.freeswitch.esl.constant.EventNames;
  26. import link.thingscloud.freeswitch.esl.constant.UuidKeys;
  27. import link.thingscloud.freeswitch.esl.transport.event.EslEvent;
  28. import link.thingscloud.freeswitch.esl.transport.message.EslMessage;
  29. import org.apache.commons.lang.StringUtils;
  30. import java.io.UnsupportedEncodingException;
  31. import java.net.URLDecoder;
  32. import java.util.ArrayList;
  33. import java.util.List;
  34. import java.util.Map;
  35. /**
  36. * @author easycallcenter365@126.com
  37. */
  38. public class RobotChat extends RobotBase {
  39. private static int initApp = init();
  40. private static int init() {
  41. int maxRobotNumber = Integer.parseInt(SystemConfig.getValue("max-call-concurrency")) ;
  42. startRobotMainThreadPool(maxRobotNumber);
  43. startFsEsNotifyThreadPool(maxRobotNumber);
  44. startRobotStatThread();
  45. return 1;
  46. }
  47. public static int getInit() {
  48. return initApp;
  49. }
  50. public RobotChat(InboundDetail callDetail, AccountBaseEntity llmAccountInfo) {
  51. this.uuid = callDetail.getUuid();
  52. ThreadLocalTraceId.getInstance().setTraceId(uuid);
  53. this.setAsrModelType(SystemConfig.getValue("robot-asr-type", ASR_TYPE_MRCP));
  54. logger.info("{} current robot_asr_type={}.",
  55. getTraceId(), this.getAsrModelType()
  56. );
  57. logger.info("{} robot allow interrupt={}, interrupt_keywords={}, interrupt_ignore_keywords={}",
  58. getTraceId(), llmAccountInfo.interruptFlag == 1, llmAccountInfo.interruptKeywords, llmAccountInfo.interruptIgnoreKeywords
  59. );
  60. this.callDetail = callDetail;
  61. getEslConnectionPool(
  62. uuid,
  63. SystemConfig.getValue("event-socket-ip"),
  64. Integer.parseInt(SystemConfig.getValue("event-socket-port"))
  65. );
  66. callTaskList.put(uuid, this);
  67. createChatBot(llmAccountInfo.provider);
  68. chatRobot.setAccount(llmAccountInfo);
  69. chatRobot.setCallDetail(callDetail);
  70. chatRobot.setTtsProvider(llmAccountInfo.voiceSource);
  71. chatRobot.setTtsVoiceName(llmAccountInfo.voiceCode);
  72. if(chatRobot instanceof LocalWebApiTest){
  73. ((LocalWebApiTest)chatRobot).makeMockData();
  74. }
  75. if(getAllowInterrupt() && ASR_TYPE_MRCP.equalsIgnoreCase(this.getAsrModelType())){
  76. logger.error("{} `robot-speech-interrupt-allowed` is not effective in the mrcp speech recognition mode.", uuid);
  77. }
  78. EslConnectionUtil.sendExecuteCommand(
  79. "playback",
  80. "$${sounds_dir}/" + LLM_WAIT_WAV_FILE ,
  81. uuid
  82. );
  83. waitForPlayBackStartSignalForLlmConcurrency();
  84. long startTime = System.currentTimeMillis();
  85. logger.info("{} Try to acquire llm permit, llmAccountInfo.id={}, llmAccountInfo.concurrentNum={}.",
  86. getTraceId(), llmAccountInfo.id, llmAccountInfo.concurrentNum);
  87. llmConsumer = new LlmConsumer(uuid, llmAccountInfo.id, llmAccountInfo.concurrentNum);
  88. LlmThreadManager.acquire(llmConsumer);
  89. logger.info("{} Acquire llm permit successfully, took {} milliseconds. ", getTraceId(),
  90. System.currentTimeMillis() - startTime
  91. );
  92. int maxTry = 3;
  93. int tryCounter = 0;
  94. while(!checkSignalForLlmConcurrency() && tryCounter <= maxTry) {
  95. interruptRobotSpeech();
  96. waitForPlayBackStoppedSignalForLlmConcurrency();
  97. tryCounter++;
  98. }
  99. callDetail.setAnsweredTime(System.currentTimeMillis());
  100. AcdSqlQueue.addToSqlQueue(callDetail);
  101. }
  102. public void startProcess(String uniqueID) {
  103. if(callDetail.getOutboundPhoneInfo() != null) {
  104. // In the outbound call scenario, solve the problem that the first few words of the first sentence
  105. // cannot be heard clearly, because it takes about 2 seconds for the customer to transfer from
  106. // the receiver to the headphones after answering the call.
  107. ThreadUtil.sleep(500);
  108. if (isHangup) {
  109. return;
  110. }
  111. }
  112. String ttsProvider = chatRobot.getAccount().voiceSource;
  113. String asrProvider = chatRobot.getAccount().asrProvider;
  114. if(StringUtils.isEmpty(ttsProvider)){
  115. logger.warn("{} No TTS voice is currently configured for the robot. Sound playback will be handled by playing pre-synthesized TTS files.",
  116. getTraceId());
  117. }
  118. if(StringUtils.isEmpty(asrProvider)){
  119. logger.error("{} asrProvider cant not be null, please check your speech-to-text configuration. ", getTraceId());
  120. hangupAndCloseConn("asrProvider-can-not-be-null");
  121. return;
  122. }
  123. if(ttsProvider.equalsIgnoreCase(TtsProvider.ALIYUN)
  124. || asrProvider.equalsIgnoreCase(AsrProvider.ALIYUN)) {
  125. if((!AliyunTTSWebApi.setAliyunTokenToFreeSWITCH(uuid))) {
  126. String errMsg = "AliyunTTSWebApi getToken error!";
  127. logger.error("{} {} ", getTraceId(), errMsg);
  128. CommonUtils.setHangupCauseDetail(
  129. callDetail,
  130. HangupCause.TTS_ACCOUNT_INFO_INCORRECT,
  131. "error msg:" + errMsg
  132. );
  133. hangupAndCloseConn("AliyunTTSWebApi-getToken-error");
  134. return;
  135. }
  136. }
  137. if(ttsProvider.equalsIgnoreCase(TtsProvider.DOUBAO)) {
  138. String ttsModels = chatRobot.getAccount().ttsModels;
  139. logger.info("{} Current tts provider is doubao, set ttsModels={}", getTraceId(), ttsModels);
  140. }
  141. if(ttsProvider.equalsIgnoreCase(TtsProvider.XFYUN)) {
  142. String ttsModels = StringUtils.trimToEmpty(chatRobot.getAccount().ttsModels);
  143. String xfTtsMode = "clone".equalsIgnoreCase(ttsModels) ? "clone" : "";
  144. logger.info("{} Current tts provider is xfyun, set xf_tts_mode={}, ttsModels={}",
  145. getTraceId(),
  146. StringUtils.isEmpty(xfTtsMode) ? "standard" : xfTtsMode,
  147. ttsModels
  148. );
  149. EslConnectionUtil.sendExecuteCommand("set",
  150. "xf_tts_mode=" + xfTtsMode,
  151. uuid
  152. );
  153. EslConnectionUtil.sendExecuteCommand("set",
  154. "xf_tts_verify_peer=false",
  155. uuid
  156. );
  157. // xfyun streaming continuation relies on xf_tts_resume, which needs cached speech handles.
  158. EslConnectionUtil.sendExecuteCommand("set",
  159. "cache_speech_handles=true",
  160. uuid
  161. );
  162. }
  163. if(ttsProvider.equalsIgnoreCase(TtsProvider.TX_TTS1)) {
  164. logger.info("{} Current tts provider is tx_tts1, enable cache_speech_handles for tx_tts1_resume.",
  165. getTraceId());
  166. EslConnectionUtil.sendExecuteCommand("set",
  167. "cache_speech_handles=true",
  168. uuid
  169. );
  170. }
  171. if(ttsProvider.equalsIgnoreCase(TtsProvider.MICROSOFT)) {
  172. logger.info("{} Current tts provider is microsoft!", getTraceId());
  173. }
  174. if(ttsProvider.equalsIgnoreCase(TtsProvider.CHINA_TELECOM)) {
  175. logger.info("{} Current tts provider is china_telecom!", getTraceId());
  176. }
  177. // set tts_models、asr_models、asr_language_code、tts_language_code
  178. EslConnectionUtil.sendExecuteCommand("set",
  179. "ecc365_tts_models=" + chatRobot.getAccount().ttsModels,
  180. uuid
  181. );
  182. EslConnectionUtil.sendExecuteCommand("set",
  183. "ecc365_asr_models=" + chatRobot.getAccount().asrModels,
  184. uuid
  185. );
  186. EslConnectionUtil.sendExecuteCommand("set",
  187. "ecc365_asr_language_code=" + chatRobot.getAccount().asrLanguageCode,
  188. uuid
  189. );
  190. EslConnectionUtil.sendExecuteCommand("set",
  191. "ecc365_tts_language_code=" + chatRobot.getAccount().ttsLanguageCode,
  192. uuid
  193. );
  194. EslMessage apiResponseMsg = EslConnectionUtil.sendSyncApiCommand(
  195. "uuid_exists",
  196. uniqueID,
  197. this.eslConnectionPool
  198. );
  199. if (apiResponseMsg != null && apiResponseMsg.getBodyLines().size() != 0) {
  200. String apiResponseText = apiResponseMsg.getBodyLines().get(0);
  201. if ("false".equalsIgnoreCase(apiResponseText)) {
  202. logger.info("{} session is hangup, try to stop robot process.", getTraceId());
  203. this.processFsMsg(this.generateHangupEvent("hangup-before-robot-process"));
  204. return;
  205. }
  206. } else {
  207. logger.info("{} uuid_exists check error, can not get apiResponseMsg...", getTraceId());
  208. }
  209. logger.info("{} start robot Process...", getTraceId());
  210. startAsrProcess(getAsrModelType(), false);
  211. interactWithRobot();
  212. }
  213. protected void processFsMsgEx(Map<String, String> headers) {
  214. String eventName = headers.get("Event-Name");
  215. String eventDateTimestamp = headers.get("Event-Date-Timestamp");
  216. if(null != eventDateTimestamp) {
  217. // esl消息从产生到被处理的延迟时间; 毫秒数
  218. long eventTime = Long.parseLong(eventDateTimestamp) / 1000L;
  219. long timeDelay = System.currentTimeMillis() - eventTime;
  220. logger.info("{} The [{}] event takes {} ms from generation to processing.", getTraceId(), eventName, timeDelay);
  221. }
  222. String eventSubClass = headers.get("Event-Subclass");
  223. String playbackFilePath = headers.get("Playback-File-Path");
  224. String detail = headers.get("Ecc365-Event-Detail");
  225. logger.info("{} Event-Name:{} ", getTraceId(), eventName);
  226. if(EventNames.PLAYBACK_START.equalsIgnoreCase(eventName)){
  227. if(EventNames.PLAYBACK_START.equalsIgnoreCase(detail)) {
  228. chatRobot.setTtsChannelState(TtsChannelState.OPENED);
  229. chatRobot.flushTtsRequestQueue();
  230. long timeSpent = System.currentTimeMillis() - playbackStartTime;
  231. logger.info("{} PLAYBACK_START event, time cost = {} ms. ", getTraceId(), timeSpent);
  232. }
  233. if(StringUtils.isNotEmpty(playbackFilePath)){
  234. if(playbackFilePath.endsWith(LLM_WAIT_WAV_FILE)) {
  235. releasePlayBackStartSignalForLlmConcurrency();
  236. logger.info("{} recv PLAYBACK_START event for wav file {}. ", getTraceId(), playbackFilePath);
  237. }else{
  238. logger.info("{} The currently playing file is {} . ", getTraceId(), playbackFilePath);
  239. }
  240. }
  241. }else if(EventNames.CHANNEL_PARK.equalsIgnoreCase(eventName))
  242. {
  243. logger.info("{} recv CHANNEL_PARK event. ", uuid);
  244. }
  245. else if (EventNames.PLAYBACK_STOP.equalsIgnoreCase(eventName)) {
  246. if(StringUtils.isNotEmpty(playbackFilePath)) {
  247. if (playbackFilePath.endsWith(LLM_WAIT_WAV_FILE)) {
  248. releasePlayBackStoppedSignalForLlmConcurrency();
  249. logger.info("{} recv PLAYBACK_STOP event for wav file {}. ", getTraceId(), playbackFilePath);
  250. }else {
  251. logger.info("{} The playback of the file {} has completed. ", getTraceId(), playbackFilePath);
  252. if(fileBasedTtsPlayback) {
  253. fileBasedTtsPlayback = false;
  254. ttsChannelClosed = true;
  255. chatRobot.setTtsChannelState(TtsChannelState.CLOSED);
  256. logger.info("{} file-based tts playback finished.", getTraceId());
  257. }
  258. recvPlayBackEndEvent = true;
  259. playbackEndTime = System.currentTimeMillis();
  260. releasePlayBackFinishedSignal();
  261. }
  262. }
  263. if(EventNames.PLAYBACK_STOP.equalsIgnoreCase(detail)) {
  264. logger.info("{} ************ streaming tts playback finished.", getTraceId());
  265. chatRobot.setTtsChannelState(TtsChannelState.CLOSED);
  266. recvPlayBackEndEvent = true;
  267. playbackEndTime = System.currentTimeMillis();
  268. releasePlayBackFinishedSignal();
  269. }
  270. if(recvHangupSignal){
  271. logger.info("{} The hang signal was received in the previous interaction process, and the call is about to hang up.",
  272. getTraceId());
  273. hangupAndCloseConn("recvHangupSignal");
  274. }
  275. }else if (EventNames.DTMF.equalsIgnoreCase(eventName)) {
  276. // get the dtmf key to check if its value is the same as
  277. // the key configured for manual transferring in the system.
  278. String digit = headers.get("DTMF-Digit");
  279. logger.info("{} recv DTMF event, digit = {}", getTraceId(), digit);
  280. String transferManualDigit = chatRobot.getAccount().transferManualDigit;
  281. if(transferManualDigit.equalsIgnoreCase(digit)){
  282. logger.info("{} DTMF digit equals transferManualDigit.", getTraceId());
  283. if (recvPlayBackEndEvent || getAllowInterrupt()) {
  284. if(!setTransferState()){
  285. return;
  286. }
  287. logger.info("{} The digit-key during call have successfully activated the condition " +
  288. "for transferring to a human operator. recvPlayBackEndEvent={}, getAllowInterrupt()={} ",
  289. getTraceId(), recvPlayBackEndEvent, getAllowInterrupt()
  290. );
  291. if(getAllowInterrupt() && !recvPlayBackEndEvent) {
  292. interruptRobotSpeech();
  293. releasePlayBackFinishedSignal();
  294. ThreadUtil.sleep(100);
  295. if(StringUtils.isEmpty(chatRobot.getAccount().voiceSource)) {
  296. ttsChannelClosed = true;
  297. }
  298. // wait for tts closed
  299. int step = 50;
  300. int maxWaitMills = 2000;
  301. int counter = 0;
  302. logger.info("{} wait for tts channel closed.", getTraceId());
  303. while (!ttsChannelClosed && !isHangup && counter <= maxWaitMills) {
  304. ThreadUtil.sleep(step);
  305. counter += step;
  306. }
  307. if(!ttsChannelClosed){
  308. ttsChannelClosed = true;
  309. chatRobot.setTtsChannelState(TtsChannelState.CLOSED);
  310. 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());
  311. }else{
  312. logger.info("{} tts channel is closed.", getTraceId());
  313. }
  314. }
  315. releaseSignal();
  316. getRobotMainThreadPool().execute(new Runnable() {
  317. @Override
  318. public void run() {
  319. doTransferToManualAgent(null);
  320. }
  321. });
  322. }else {
  323. logger.info("{} The digit-key during call have been failed to activate the condition " +
  324. "for transferring to a human operator. recvPlayBackEndEvent={}, getAllowInterrupt()={} ",
  325. getTraceId(), recvPlayBackEndEvent, getAllowInterrupt()
  326. );
  327. }
  328. }
  329. }else if (EventNames.CHANNEL_HANGUP.equalsIgnoreCase(eventName)) {
  330. if(isHangup){
  331. return;
  332. }
  333. releasePlayBackFinishedSignal();
  334. releaseSignal();
  335. isHangup = true;
  336. // 挂机后立即释放通道
  337. releaseThreadNum();
  338. releaseDtmf();
  339. displayNoVoiceNum();
  340. // 保存催记内容
  341. try {
  342. saveCdr(headers);
  343. } catch (Throwable e) {
  344. logger.info("{} save cdr error: {}, {} ", getTraceId(), e.toString(),
  345. CommonUtils.getStackTraceString(e.getStackTrace()));
  346. }
  347. if(!transferToAgent){
  348. CdrDetail cdrRecord = new CdrDetail();
  349. cdrRecord.setUuid(uuid);
  350. if(callDetail.getOutboundPhoneInfo() == null) {
  351. cdrRecord.setCdrType("inbound");
  352. }else{
  353. cdrRecord.setCdrType("outbound");
  354. }
  355. cdrRecord.setCdrBody(JSON.toJSONString(callDetail));
  356. CdrPush.addCdrToQueue(cdrRecord);
  357. }
  358. } else if ("CUSTOM".equalsIgnoreCase(eventName) && (
  359. "TtsEvent".equalsIgnoreCase(eventSubClass)
  360. )) {
  361. String event = headers.get("Tts-Event-Detail");
  362. if("Speech-Closed".equalsIgnoreCase(event)){
  363. chatRobot.setTtsChannelState(TtsChannelState.CLOSED);
  364. logger.info("{} TtsChannelClosed = true.", getTraceId());
  365. ttsChannelClosed = true;
  366. recvPlayBackEndEvent = true;
  367. playbackEndTime = System.currentTimeMillis();
  368. releasePlayBackFinishedSignal();
  369. }
  370. if("Speech-Open".equalsIgnoreCase(event)){
  371. chatRobot.setTtsChannelState(TtsChannelState.OPENED);
  372. chatRobot.flushTtsRequestQueue();
  373. long timeSpent = System.currentTimeMillis() - playbackStartTime;
  374. logger.info("{} Speech-Open event, time cost = {} ms. ", getTraceId(), timeSpent);
  375. }
  376. if ("NetworkError".equalsIgnoreCase(event)) {
  377. CommonUtils.setHangupCauseDetail(
  378. callDetail,
  379. HangupCause.TTS_SERVER_CONNECTED_FAILED,
  380. headers.get("Error-Details")
  381. );
  382. logger.info("{} recv NetworkError event, hangup call session.", getTraceId());
  383. hangupAndCloseConn("Asr-TTs-NetworkError");
  384. }
  385. }
  386. else if ("CUSTOM".equalsIgnoreCase(eventName) && (
  387. "AsrEvent".equalsIgnoreCase(eventSubClass)
  388. )) {
  389. String speechEvent = headers.get("ASR-Event-Detail");
  390. String asrResponse = headers.get("Detect-Speech-Result");
  391. if (null != asrResponse) {
  392. asrResponse = headers.get("Detect-Speech-Result").trim();
  393. }
  394. // #region debug-point xfyun-asr-no-response-asr-event
  395. logger.info("{} dbg_asr_event recv event={}, recvPlayBackEndEvent={}, allowInterrupt={}, inSpeaking={}, isHangup={}, transferToAgent={}, keepAiDuringTransferWait={}, manualAnsweredTime={}, isReleased={}, response={}",
  396. getTraceId(),
  397. speechEvent,
  398. recvPlayBackEndEvent,
  399. getAllowInterrupt(),
  400. interactiveParam.checkInSpeaking(),
  401. isHangup,
  402. transferToAgent,
  403. shouldKeepAiConversationDuringTransferWait(),
  404. callDetail == null ? -1L : callDetail.getManualAnsweredTime(),
  405. isReleased,
  406. asrResponse
  407. );
  408. // #endregion
  409. if ("NetworkError".equalsIgnoreCase(speechEvent)) {
  410. CommonUtils.setHangupCauseDetail(
  411. callDetail,
  412. HangupCause.ASR_SERVER_CONNECTED_FAILED,
  413. asrResponse
  414. );
  415. hangupAndCloseConn("Asr-Tts-NetworkError");
  416. return;
  417. }
  418. lastTalkTime = System.currentTimeMillis();
  419. boolean transferWaitingForManualAnswer = shouldKeepAiConversationDuringTransferWait();
  420. if (isHangup || interactiveParam.checkInHangupState() || (transferToAgent && !transferWaitingForManualAnswer)) {
  421. // #region debug-point xfyun-asr-no-response-drop-hangup
  422. logger.info("{} dbg_asr_event drop by hangup-state event={}, isHangup={}, inHangupState={}, transferToAgent={}, transferWaitingForManualAnswer={}, response={}",
  423. getTraceId(),
  424. speechEvent,
  425. isHangup,
  426. interactiveParam.checkInHangupState(),
  427. transferToAgent,
  428. transferWaitingForManualAnswer,
  429. asrResponse
  430. );
  431. // #endregion
  432. logger.info("{} Session is going to be hangup or is already being transferred to human operator, drop asr result: {}", getTraceId(), asrResponse);
  433. return;
  434. }
  435. boolean allowTransferWaitBargeIn = shouldKeepAiConversationDuringTransferWait();
  436. if (!getAllowInterrupt() && !recvPlayBackEndEvent && !allowTransferWaitBargeIn) {
  437. // #region debug-point xfyun-asr-no-response-drop-playback
  438. logger.info("{} dbg_asr_event drop by playback-gate event={}, recvPlayBackEndEvent={}, allowInterrupt={}, allowTransferWaitBargeIn={}, response={}",
  439. getTraceId(),
  440. speechEvent,
  441. recvPlayBackEndEvent,
  442. getAllowInterrupt(),
  443. allowTransferWaitBargeIn,
  444. asrResponse
  445. );
  446. // #endregion
  447. if ("vad".equalsIgnoreCase(speechEvent)) {
  448. dropAsrCounter.incrementAndGet();
  449. logger.info("{} (vad event) drop asr result: {}", getTraceId(), asrResponse);
  450. } else {
  451. logger.info("{} (vad event) drop asr result: {}", getTraceId(), asrResponse);
  452. }
  453. return;
  454. }
  455. if ("middle".equalsIgnoreCase(speechEvent)) {
  456. logger.info("{} ** asr-websocket, begin-speaking ** {}", getTraceId(), asrResponse);
  457. logger.info("{} recv asr middle result event, recvPlayBackEndEvent={}, allowInterrupt={}, !checkSpeaking={}",
  458. getTraceId(),
  459. recvPlayBackEndEvent,
  460. getAllowInterrupt(),
  461. !interactiveParam.checkInSpeaking()
  462. );
  463. if (recvPlayBackEndEvent || getAllowInterrupt() || allowTransferWaitBargeIn) {
  464. if (!interactiveParam.checkInSpeaking()) {
  465. String lockerKey = String.format("%s%s", uuid, "checkInSpeaking");
  466. synchronized (lockerKey.intern()) {
  467. if (!interactiveParam.checkInSpeaking()) {
  468. interactiveParam.setInSpeaking(true);
  469. // Main thread awakened to extend customer speaking time beyond 6 seconds.
  470. logger.info("{} customer speech detected. ", getTraceId());
  471. if (allowTransferWaitBargeIn && !recvPlayBackEndEvent) {
  472. logger.info("{} transfer-wait barge-in detected by middle event, interrupt current robot speech.", getTraceId());
  473. interruptRobotSpeech();
  474. releasePlayBackFinishedSignal();
  475. ThreadUtil.sleep(100);
  476. } else if (chatRobot.getAccount().interruptFlag == 2) {
  477. interruptRobotSpeech();
  478. releasePlayBackFinishedSignal();
  479. ThreadUtil.sleep(100);
  480. }
  481. }
  482. }
  483. }
  484. }
  485. } else if ("vad".equalsIgnoreCase(speechEvent)) {
  486. logger.info("{} ** vad end-speaking: {}", getTraceId(), asrResponse);
  487. interactiveParam.setInSpeaking(false);
  488. if(StringUtils.isEmpty(asrResponse)){
  489. logger.error("{} error, vad result is null.", getTraceId());
  490. return;
  491. }
  492. if (!StringUtil.isNullOrEmpty(asrResponse)) {
  493. asrResultEx.add(asrResponse);
  494. // #region debug-point xfyun-asr-no-response-asr-cache
  495. logger.info("{} dbg_asr_event cached vad result, queueSize={}, response={}",
  496. getTraceId(),
  497. asrResultEx.size(),
  498. asrResponse
  499. );
  500. // #endregion
  501. }
  502. if (allowTransferWaitBargeIn && !recvPlayBackEndEvent) {
  503. logger.info("{} transfer-wait barge-in detected by vad event, interrupt current robot speech.", getTraceId());
  504. interruptRobotSpeech();
  505. releasePlayBackFinishedSignal();
  506. ThreadUtil.sleep(100);
  507. } else if(chatRobot.getAccount().interruptFlag == 1 && !recvPlayBackEndEvent) {
  508. if (checkSpeechInterrupt(asrResponse)) {
  509. interruptRobotSpeech();
  510. releasePlayBackFinishedSignal();
  511. ThreadUtil.sleep(100);
  512. }else{
  513. return;
  514. }
  515. }
  516. if(recvPlayBackEndEvent || getAllowInterrupt() || allowTransferWaitBargeIn){
  517. // #region debug-point xfyun-asr-no-response-release-signal
  518. logger.info("{} dbg_asr_event release signal by vad event, queueSize={}, recvPlayBackEndEvent={}, allowInterrupt={}, allowTransferWaitBargeIn={}, response={}",
  519. getTraceId(),
  520. asrResultEx.size(),
  521. recvPlayBackEndEvent,
  522. getAllowInterrupt(),
  523. allowTransferWaitBargeIn,
  524. asrResponse
  525. );
  526. // #endregion
  527. logger.info("{} releaseSignal for vad event.", getTraceId());
  528. releaseSignal();
  529. }
  530. }
  531. }
  532. }
  533. private boolean setTransferState() {
  534. String lockerKey = String.format("%s%s", uuid, "setTransferState");
  535. synchronized (lockerKey.intern()) {
  536. if (transferToAgent) {
  537. logger.info("{} transferring to a human operator is already being handled. skip...", getTraceId());
  538. return false;
  539. }
  540. transferToAgent = true;
  541. return true;
  542. }
  543. }
  544. protected void interruptRobotSpeech(){
  545. logger.info("{} send uuid_break command to FreeSWITCH.", uuid);
  546. EslConnectionUtil.sendSyncApiCommand("uuid_break", uuid + " all");
  547. }
  548. @Override
  549. protected void processFsMsg(Map<String, String> headers) {
  550. try {
  551. processFsMsgEx(headers);
  552. } catch (Throwable e) {
  553. logger.error("{} processFsMsg error: {}, {}", getTraceId(), e.toString(),
  554. CommonUtils.getStackTraceString(e.getStackTrace()));
  555. }
  556. }
  557. protected void processFsMsg2(EslEvent event) {
  558. Map<String, String> headers = event.getEventHeaders();
  559. String eventName = headers.get("Event-Name");
  560. if (eventName.equalsIgnoreCase("DETECTED_SPEECH")) {
  561. String speechEvent = headers.get("Speech-Type");
  562. lastTalkTime = System.currentTimeMillis();
  563. if (isHangup || interactiveParam.checkInHangupState()) {
  564. logger.info("{} Session is going to be hangup, drop mrcp asr result: {}", getTraceId(),
  565. headers.get("detect_speech_result"));
  566. return;
  567. }
  568. if ("begin-speaking".equalsIgnoreCase(speechEvent)) {
  569. if (!interactiveParam.checkInSpeaking()) {
  570. logger.info(getTraceId() + " ** customer begin-speaking detected. **");
  571. // 用户开始讲话标识
  572. interactiveParam.setInSpeaking(true);
  573. releaseSignal(); // 唤醒主线程,让主线程可以超出6秒限制;
  574. }
  575. } else if ("detected-partial-speech".equalsIgnoreCase(speechEvent)) {
  576. synchronized (getTraceId().intern()) {
  577. if (!interactiveParam.checkInSpeaking()) {
  578. // 用户开始讲话标识
  579. interactiveParam.setInSpeaking(true);
  580. // 唤醒主线程,让主线程可以超出6秒限制;
  581. releaseSignal();
  582. }
  583. }
  584. // 语音识别的中间结果;
  585. logger.info("{} detected-partial-speech = {}", getTraceId(), headers.get("detect_speech_result"));
  586. } else if ("detected-speech".equalsIgnoreCase(speechEvent)) {
  587. if (!interactiveParam.checkInSpeaking()) {
  588. logger.info(getTraceId() + " mrcp return, no asr result got. isInSpeaking=false.");
  589. } else {
  590. logger.info(getTraceId() + " ****** customer stop-speaking detected. ******");
  591. }
  592. interactiveParam.setInSpeaking(false);
  593. String speechResult = headers.get("detect_speech_result");
  594. if(StringUtils.isEmpty(speechResult)){
  595. speechResult = CommonUtils.ListToString(event.getEventBodyLines(), false);
  596. }
  597. if (StringUtils.isEmpty(speechResult) || "Completion-Cause: 002".equalsIgnoreCase(speechResult.trim())) {
  598. logger.info(getTraceId() + " mrcp return, no asr result got...");
  599. releaseSignal();
  600. return;
  601. }
  602. if(speechResult.startsWith("<?xml")){
  603. parseMrcpResultXmlStr(speechResult);
  604. logger.info("{} detect_speech_result: {}", getTraceId(), speechResult);
  605. }else{
  606. try {
  607. String tmpResult = URLDecoder.decode(speechResult,"utf-8").replace(" ","");
  608. asrResultEx.add(tmpResult);
  609. logger.info("{} kaldi asr response: {}",getTraceId(), tmpResult);
  610. } catch (Throwable e) {
  611. logger.error("{} URLDecoder.decode Error: {}", getTraceId(), speechResult);
  612. }
  613. }
  614. releaseSignal();
  615. }
  616. }
  617. }
  618. /**
  619. * 解析unimrcp-client接收到的xml字符串;
  620. * @param xmlStr
  621. */
  622. private void parseMrcpResultXmlStr(String xmlStr){
  623. SpeechResultEntity resultEntity = null;
  624. try {
  625. String input = URLDecoder.decode(xmlStr, "utf-8");
  626. resultEntity = getSpeechResult(input);
  627. } catch (UnsupportedEncodingException e) {
  628. }
  629. if (null != resultEntity) {
  630. if (!StringUtils.isEmpty(resultEntity.getResult())) {
  631. String asrResult = resultEntity.getResult();
  632. if(!StringUtil.isNullOrEmpty(asrResult)) {
  633. asrResultEx.add(asrResult);
  634. }
  635. logger.info(getTraceId() + " mrcp asr result: {}, requestid: {}", resultEntity.getResult(), resultEntity.getRequestId());
  636. }
  637. } else {
  638. logger.info(getTraceId() + " cant not parse variable detect_speech_result.");
  639. }
  640. }
  641. @Override
  642. public void eventReceived(String addr, EslEvent event) {
  643. fsEsNotifyThreadPool.execute(new Runnable() {
  644. @Override
  645. public void run() {
  646. Map<String, String> headers = event.getEventHeaders();
  647. boolean mrcpResult = EventNames.DETECTED_SPEECH.equalsIgnoreCase(headers.get("Event-Name"));
  648. if (getAsrModelType().equalsIgnoreCase(ASR_TYPE_MRCP) && mrcpResult) {
  649. processFsMsg2(event);
  650. } else {
  651. // output of FreeSWITCH websocket asr module
  652. processFsMsg(headers);
  653. }
  654. }
  655. });
  656. }
  657. @Override
  658. public String context() {
  659. return this.getClass().getName();
  660. }
  661. @Override
  662. public void backgroundJobResultReceived(String addr, EslEvent event) {
  663. }
  664. /**
  665. * interactWithRobot
  666. **/
  667. private void interactWithRobot() {
  668. if (checkCallSession()) {
  669. return;
  670. }
  671. // #region debug-point xfyun-asr-no-response-interact-enter
  672. logger.info("{} dbg_interact enter, asrQueueSize={}, recvPlayBackEndEvent={}, ttsChannelClosed={}, inSpeaking={}, noVoiceCounter={}, transferToAgent={}, keepAiDuringTransferWait={}, manualAnsweredTime={}, isReleased={}",
  673. getTraceId(),
  674. asrResultEx.size(),
  675. recvPlayBackEndEvent,
  676. ttsChannelClosed,
  677. interactiveParam.checkInSpeaking(),
  678. noVoiceCounter.get(),
  679. transferToAgent,
  680. shouldKeepAiConversationDuringTransferWait(),
  681. callDetail == null ? -1L : callDetail.getManualAnsweredTime(),
  682. isReleased
  683. );
  684. // #endregion
  685. interactiveParam.setAllowInterrupt(0);
  686. recvPlayBackEndEvent = false;
  687. firstSpeak = false;
  688. interactiveParam.setInSpeaking(false);
  689. if(getAsrModelType().equalsIgnoreCase(ASR_TYPE_WEBSOCKET)) {
  690. pauseAsr();
  691. }
  692. // 送robot理解客户意图,返回合成后的语音文件路径
  693. StringBuilder asrStr = new StringBuilder();
  694. for (String result : asrResultEx) {
  695. asrStr.append(result);
  696. }
  697. // #region debug-point xfyun-asr-no-response-question-built
  698. logger.info("{} dbg_interact built question='{}', cachedResultCount={}",
  699. getTraceId(),
  700. asrStr.toString(),
  701. asrResultEx.size()
  702. );
  703. // #endregion
  704. // 清空 asrResultEx; 重新初始化字段;
  705. asrResultEx.clear();
  706. // 识别开始时间
  707. Long startTime = System.currentTimeMillis();
  708. LlmAiphoneRes aiphoneRes;
  709. try {
  710. String question = asrStr.toString();
  711. if (StringUtils.isEmpty(question)) {
  712. // #region debug-point xfyun-asr-no-response-empty-question
  713. logger.info("{} dbg_interact question empty, noVoiceCounterBefore={}, recvPlayBackEndEvent={}, ttsChannelClosed={}",
  714. getTraceId(),
  715. noVoiceCounter.get(),
  716. recvPlayBackEndEvent,
  717. ttsChannelClosed
  718. );
  719. // #endregion
  720. int counter = noVoiceCounter.incrementAndGet();
  721. if (counter > MAX_CONSECUTIVE_NO_VOICE_NUMBER) {
  722. logger.info("{} There has been no sound for {} consecutive times. Play hangupTips and then hangup call.",
  723. getTraceId(), MAX_CONSECUTIVE_NO_VOICE_NUMBER);
  724. playSound(chatRobot.getAccount().hangupTips);
  725. recvHangupSignal = true;
  726. return;
  727. }
  728. }else{
  729. if(noVoiceCounter.get() > 1) {
  730. noVoiceCounter.set(0);
  731. }
  732. }
  733. logger.info("{} send question to chatRobot: {}", getTraceId(), question);
  734. chatRobot.resetRoundTtsPlaybackState();
  735. aiphoneRes = chatRobot.talkWithAiAgent(question, kbQueryExecuted);
  736. while ((aiphoneRes == null || aiphoneRes.getStatus_code() == 0)
  737. && Llm_max_try_counter.get() < LLM_MAX_TRY) {
  738. logger.error("{} llm api error, retry to send question to chatRobot: {}", getTraceId(), question);
  739. chatRobot.resetRoundTtsPlaybackState();
  740. aiphoneRes = chatRobot.talkWithAiAgent(question, kbQueryExecuted);
  741. Llm_max_try_counter.incrementAndGet();
  742. if (checkCallSession()) {
  743. return;
  744. }
  745. }
  746. Llm_max_try_counter.set(0);
  747. kbQueryExecuted = false;
  748. if (aiphoneRes == null || aiphoneRes.getStatus_code() == 0) {
  749. String tips = SystemConfig.getValue("llm-max-try-fail-tips", "");
  750. if (!StringUtils.isEmpty(tips)) {
  751. playSound(tips);
  752. } else {
  753. CommonUtils.setHangupCauseDetail(
  754. callDetail,
  755. HangupCause.LLM_API_SERVER_ERROR,
  756. String.format("The large model failed to access successfully despite more than %d connection attempts.", LLM_MAX_TRY)
  757. );
  758. hangupAndCloseConn("reach-llm-max-try-error");
  759. }
  760. return;
  761. }
  762. talkRound.increment();
  763. Long spentCost = System.currentTimeMillis() - startTime;
  764. logger.info("{} talkWithLargeModel spent time: {} ms, aiphoneRes = {}",
  765. getTraceId(), spentCost, JSON.toJSONString(aiphoneRes)
  766. );
  767. if(aiphoneRes.getStatus_code() == 1) {
  768. ttsChannelClosed = false;
  769. String body = aiphoneRes.getBody();
  770. if(!StringUtils.isEmpty(body)){
  771. if(body.contains(LlmToolRequest.TRANSFER_TO_AGENT)){
  772. aiphoneRes.setTransferToAgent(1);
  773. body = body.replace(LlmToolRequest.TRANSFER_TO_AGENT, "");
  774. }
  775. if(body.contains(LlmToolRequest.HANGUP)){
  776. aiphoneRes.setClose_phone(1);
  777. body = body.replace(LlmToolRequest.HANGUP, "");
  778. }
  779. if(body.contains(LlmToolRequest.TRANSFER_TO_TEL)){
  780. aiphoneRes.setTransferToAgent(1);
  781. }
  782. if(body.contains(LlmToolRequest.KB_QUERY + "=")){
  783. kbQueryExecuted = true;
  784. int catId = chatRobot.getAccount().kbCatId;
  785. String title = body.replace(LlmToolRequest.KB_QUERY + "=", "").replace(" ","");
  786. LlmKb kb = AppContextProvider.getBean(SysService.class).getKbContentByCat(catId, title);
  787. String response = "No relevant topics were found.";
  788. if(kb != null){
  789. response = kb.getContent();
  790. logger.info("{} 1 relevant topics {} were found: {} ", getTraceId(), title, response.substring(0, 10));
  791. }
  792. JSONObject userMessage = new JSONObject();
  793. userMessage.put("role", "system");
  794. userMessage.put("content", "kbQuery response:" + response);
  795. userMessage.put("content_type", "text");
  796. chatRobot.getDialogues().add(userMessage);
  797. interactWithRobot();
  798. return;
  799. }
  800. aiphoneRes.setBody(body);
  801. }
  802. if (checkCallSession()) {
  803. return;
  804. }
  805. if (aiphoneRes.getTransferToAgent() == 1) {
  806. if (shouldKeepAiConversationDuringTransferWait() && transferToAgentExecuted) {
  807. logger.info("{} duplicate transfer-to-agent instruction detected during manual-wait, keep current transfer task and continue reply body={}.",
  808. getTraceId(),
  809. aiphoneRes.getBody()
  810. );
  811. aiphoneRes.setTransferToAgent(0);
  812. } else {
  813. if(!setTransferState()){
  814. return;
  815. }
  816. doTransferToManualAgent(aiphoneRes);
  817. return;
  818. }
  819. }
  820. if (aiphoneRes.getClose_phone() == 1) {
  821. if(StringUtils.isEmpty(body)){
  822. playSound(chatRobot.getAccount().hangupTips);
  823. }else{
  824. playResponse(aiphoneRes);
  825. }
  826. waitForPlayBackFinished(11000);
  827. long startTimeTick = System.currentTimeMillis();
  828. if(StringUtils.isEmpty(chatRobot.getAccount().voiceSource)){
  829. ttsChannelClosed = true;
  830. ThreadUtil.sleep(2000);
  831. }
  832. while (!ttsChannelClosed && !isHangup) {
  833. ThreadUtil.sleep(1000);
  834. if(System.currentTimeMillis() - startTimeTick > 11000){
  835. break;
  836. }
  837. }
  838. hangupAndCloseConn("system-hangup");
  839. return;
  840. }
  841. // play wav file
  842. playResponse(aiphoneRes);
  843. }
  844. } catch (Throwable e) {
  845. logger.error("{} talkWithLargeModel error! {} {} ",
  846. getTraceId(), e.toString(), CommonUtils.getStackTraceString(e.getStackTrace())
  847. );
  848. CommonUtils.setHangupCauseDetail(
  849. callDetail,
  850. HangupCause.SYSTEM_INTERNAL_ERROR,
  851. String.format("server error: %s", e.toString())
  852. );
  853. hangupAndCloseConn(HangupCause.SYSTEM_INTERNAL_ERROR.getCode());
  854. return;
  855. }
  856. if(aiphoneRes.getIfcan_interrupt() == 1) {
  857. interactiveParam.setAllowInterrupt(1);
  858. logger.info("{} allowSpeechInterrupt={}", getTraceId(), 1);
  859. }
  860. if (!interactiveParam.checkInHangupState()) {
  861. if (aiphoneRes.getClose_phone() == 1) {
  862. logger.info(getTraceId() + " hangup signal is detected. ");
  863. interactiveParam.setInHangUpState(true);
  864. recvHangupSignal = true;
  865. } else {
  866. waitForCustomerSpeakEx();
  867. }
  868. }
  869. }
  870. private void waitAndDetectSpeaking(){
  871. if (interactiveParam.checkInSpeaking()){
  872. logger.info("{} Speaking is detected, Wait for customer to finish speaking. Timeout: {} ",
  873. getTraceId(),
  874. maxWaitTimeMills
  875. );
  876. acquire(maxWaitTimeMills);
  877. }
  878. }
  879. private void playResponse(LlmAiphoneRes aiphoneRes){
  880. String ttsFilePathList = aiphoneRes.getTtsFilePathList();
  881. if (useXfyunCloneFilePlayback() && chatRobot.consumeDirectTtsPlaybackFlag()) {
  882. logger.info("{} skip xfyun clone file playback because response already streamed directly, text={}.",
  883. getTraceId(),
  884. aiphoneRes.getBody()
  885. );
  886. return;
  887. }
  888. if(StringUtils.isEmpty(ttsFilePathList) && useXfyunCloneFilePlayback()) {
  889. if (playXfyunCloneTtsIfNeeded(aiphoneRes.getBody())) {
  890. logger.info("{} try to play xfyun clone wav file for text {}.", getTraceId(), aiphoneRes.getBody());
  891. } else if(StringUtils.isNotEmpty(aiphoneRes.getBody())) {
  892. logger.info("{} fallback to streaming xfyun clone tts for text {}.", getTraceId(), aiphoneRes.getBody());
  893. chatRobot.sendTtsRequest(aiphoneRes.getBody());
  894. chatRobot.closeTts();
  895. }
  896. return;
  897. }
  898. if(!StringUtils.isEmpty(ttsFilePathList)){
  899. TtsFileInfo ttsFileInfo = AudioUtils.joinTtsFiles(ttsFilePathList);
  900. logger.info("{} try to play wav file for text {}.", getTraceId(), aiphoneRes.getBody());
  901. startPlayback(ttsFileInfo);
  902. return;
  903. }
  904. }
  905. private void doTransferToManualAgent(LlmAiphoneRes aiphoneRes){
  906. String audioTipsText = null;
  907. if(aiphoneRes != null) {
  908. audioTipsText = aiphoneRes.getBody();
  909. }
  910. String lockerKey = String.format("%s%s", uuid, "doTransferToManualAgent");
  911. synchronized (lockerKey.intern()) {
  912. if (transferToAgentExecuted) {
  913. logger.warn("{} The call transfer to a human agent has already been processed.", getTraceId());
  914. return;
  915. }
  916. transferToAgentExecuted = true;
  917. }
  918. callDetail.setChatContent(chatRobot.getDialogues());
  919. // Replace the prompt words for manual transfer in the text with blank spaces.
  920. String transferToTel = "";
  921. if(!StringUtils.isEmpty(audioTipsText) && audioTipsText.contains(LlmToolRequest.TRANSFER_TO_TEL)){
  922. if(!TransferToAgent.TRANSFER_TO_GATEWAY.equalsIgnoreCase(chatRobot.getAccount().aiTransferType)){
  923. logger.error("{} instruction `{}` is only applicable when an external gateway is used to transfer to a manual agent.",
  924. uuid, LlmToolRequest.TRANSFER_TO_TEL);
  925. hangupAndCloseConn("llm-instruction-error");
  926. return;
  927. }
  928. List<String> matches = RegExp.GetMatchFromStringByRegExp(audioTipsText, LlmToolRequest.TRANSFER_TO_TEL_REGEXP);
  929. for (String match : matches) {
  930. audioTipsText = audioTipsText.replace(match, "");
  931. List<String> tmp = RegExp.GetMatchFromStringByRegExp(match, "\\d{1,12}");
  932. transferToTel = tmp.get(0);
  933. logger.info("{} Successfully retrieved transferToTel number {}", uuid, transferToTel);
  934. JSONObject jsonObject = JSON.parseObject(chatRobot.getAccount().aiTransferData);
  935. jsonObject.put("destNumber", transferToTel);
  936. chatRobot.getAccount().aiTransferData = JSON.toJSONString(jsonObject);
  937. logger.info("{} Successfully update aiTransferData: {} ", uuid, chatRobot.getAccount().aiTransferData);
  938. }
  939. }
  940. if(StringUtils.isEmpty(audioTipsText) || audioTipsText.equalsIgnoreCase(LlmToolRequest.TRANSFER_TO_AGENT)){
  941. String tips = chatRobot.getAccount().transferToAgentTips;
  942. logger.info("{} Try to play tts transferToAgentTips {}", getTraceId(), tips);
  943. if(!StringUtils.isEmpty(tips)) {
  944. playSound(tips);
  945. waitForPlayBackFinished(9000);
  946. }
  947. // wait for tips playback finished
  948. }else {
  949. playResponse(aiphoneRes);
  950. waitForPlayBackFinished(9000);
  951. }
  952. boolean delayTransferMediaStopUntilManualAnswer = shouldDelayTransferMediaStopUntilManualAnswer();
  953. if (delayTransferMediaStopUntilManualAnswer) {
  954. logger.info("{} xfyun transfer flow keeps TTS/ASR active until manual agent answers. manualAnsweredTime={}, isReleased={}",
  955. getTraceId(),
  956. callDetail == null ? -1L : callDetail.getManualAnsweredTime(),
  957. isReleased);
  958. } else {
  959. // stop_asr 的顺序很重要,需要放在播放tts之后,否则不起作用;会被uuid_break清空指令;
  960. logger.info("{} Try to stop asr {}", getTraceId(), chatRobot.getAccount().asrProvider);
  961. EslConnectionUtil.sendExecuteCommand(
  962. String.format("stop_%s_asr", chatRobot.getAccount().asrProvider), "", uuid);
  963. ThreadUtil.sleep(200);
  964. }
  965. if(!isHangup) {
  966. // #region debug-point xfyun-transfer-wait-release-thread
  967. logger.info("{} dbg_transfer_wait before releaseThreadNum transferToAgent={}, transferToAgentExecuted={}, keepAiDuringTransferWait={}, manualAnsweredTime={}, isReleased={}",
  968. getTraceId(),
  969. transferToAgent,
  970. transferToAgentExecuted,
  971. shouldKeepAiConversationDuringTransferWait(),
  972. callDetail == null ? -1L : callDetail.getManualAnsweredTime(),
  973. isReleased
  974. );
  975. // #endregion
  976. if (!delayTransferMediaStopUntilManualAnswer) {
  977. releaseThreadNum();
  978. TransferToAgent.transfer(callDetail, chatRobot.getAccount());
  979. } else {
  980. logger.info("{} postpone releaseThreadNum until manual agent answers.", getTraceId());
  981. final InboundDetail currentCallDetail = callDetail;
  982. final AccountBaseEntity currentAccount = chatRobot.getAccount();
  983. getRobotMainThreadPool().execute(new Runnable() {
  984. @Override
  985. public void run() {
  986. ThreadLocalTraceId.getInstance().setTraceId(uuid);
  987. logger.info("{} async transfer-to-agent task started while AI keeps talking before manual answer.", getTraceId());
  988. TransferToAgent.transfer(currentCallDetail, currentAccount);
  989. }
  990. });
  991. waitForCustomerSpeakEx();
  992. }
  993. }
  994. }
  995. private boolean shouldDelayTransferMediaStopUntilManualAnswer() {
  996. String voiceSource = StringUtils.trimToEmpty(chatRobot.getAccount().voiceSource);
  997. String transferType = StringUtils.trimToEmpty(chatRobot.getAccount().aiTransferType);
  998. return TtsProvider.XFYUN.equalsIgnoreCase(voiceSource)
  999. && !TransferToAgent.TRANSFER_TO_ACD.equalsIgnoreCase(transferType);
  1000. }
  1001. private boolean shouldKeepAiConversationDuringTransferWait() {
  1002. return transferToAgent
  1003. && shouldDelayTransferMediaStopUntilManualAnswer()
  1004. && callDetail != null
  1005. && callDetail.getManualAnsweredTime() == 0L;
  1006. }
  1007. /**
  1008. * Check if the call has been hung up or has been transferred to a human handler.
  1009. * @return
  1010. */
  1011. private boolean checkCallSession(){
  1012. return isHangup || (transferToAgent && !shouldKeepAiConversationDuringTransferWait());
  1013. }
  1014. /**
  1015. * Play TTS and wait for the customer to speak.
  1016. */
  1017. private void waitForCustomerSpeakEx() {
  1018. if (checkCallSession()) {
  1019. return;
  1020. }
  1021. // #region debug-point xfyun-asr-no-response-wait-enter
  1022. logger.info("{} dbg_wait_customer enter, recvPlayBackEndEvent={}, ttsChannelClosed={}, asrQueueSize={}, inSpeaking={}, transferToAgent={}, keepAiDuringTransferWait={}, manualAnsweredTime={}, isReleased={}",
  1023. getTraceId(),
  1024. recvPlayBackEndEvent,
  1025. ttsChannelClosed,
  1026. asrResultEx.size(),
  1027. interactiveParam.checkInSpeaking(),
  1028. transferToAgent,
  1029. shouldKeepAiConversationDuringTransferWait(),
  1030. callDetail == null ? -1L : callDetail.getManualAnsweredTime(),
  1031. isReleased
  1032. );
  1033. // #endregion
  1034. logger.info("{} enter into waitForCustomerSpeak ...", getTraceId());
  1035. // The duration of streaming TTS playback should not exceed 181 seconds.
  1036. if (!recvPlayBackEndEvent) {
  1037. logger.info("{} enter into waitForPlayBackFinished ...", getTraceId());
  1038. waitForPlayBackFinished();
  1039. }
  1040. if (checkCallSession()) {
  1041. return;
  1042. }
  1043. if (!recvPlayBackEndEvent) {
  1044. logger.info("{} robot speech interrupt detected. ", getTraceId());
  1045. } else {
  1046. logger.info("{} robot speech playback finished. ", getTraceId());
  1047. }
  1048. if (getAsrModelType().equalsIgnoreCase(ASR_TYPE_WEBSOCKET)) {
  1049. resumeAsr();
  1050. }
  1051. if (getAsrModelType().equalsIgnoreCase(ASR_TYPE_MRCP)) {
  1052. startMrcp();
  1053. }
  1054. long startWaitTimeMills = System.currentTimeMillis();
  1055. logger.info("{} wait for customer speaking ...", getTraceId());
  1056. Integer maxWaitTimeCustomerSpeaking = Integer.parseInt(SystemConfig.getValue("max-wait-time-customer-speaking", "7000")) ;
  1057. acquire(maxWaitTimeCustomerSpeaking);
  1058. logger.info("{} wait for customer speaking, time passed = {}ms. ...",
  1059. getTraceId(),
  1060. System.currentTimeMillis() - startWaitTimeMills
  1061. );
  1062. // #region debug-point xfyun-asr-no-response-wait-after-acquire
  1063. logger.info("{} dbg_wait_customer after acquire, asrQueueSize={}, inSpeaking={}, recvPlayBackEndEvent={}",
  1064. getTraceId(),
  1065. asrResultEx.size(),
  1066. interactiveParam.checkInSpeaking(),
  1067. recvPlayBackEndEvent
  1068. );
  1069. // #endregion
  1070. if (checkCallSession()) {
  1071. return;
  1072. }
  1073. logger.info("{} enter into waitAndDetectSpeaking ...", getTraceId());
  1074. // If speech is detected within 7 seconds, continue waiting.
  1075. waitAndDetectSpeaking();
  1076. logger.info(getTraceId() + " Robot main thread has woken up.");
  1077. if (checkCallSession()) {
  1078. return;
  1079. }
  1080. if (!interactiveParam.checkInSpeaking()) {
  1081. // 前面的流程都正常,客户讲话有中间结果,且有最终的vad结果;
  1082. // 根据vad结果产生不同的时间段,计算不同的应继续等待时间;
  1083. long waitMills = calcWaitSecsDuration6Secs();
  1084. if (waitMills > 100L) {
  1085. logger.info("{} Wait another {} milliseconds to ensure the customer is finished speaking. ",
  1086. getTraceId(),
  1087. waitMills
  1088. );
  1089. acquire(waitMills);
  1090. logger.info("{} enter into waitAndDetectSpeaking ...", getTraceId());
  1091. waitAndDetectSpeaking();
  1092. }
  1093. }else{
  1094. // The customer's speech is not over yet.
  1095. logger.info("{} Oh, it seems that the customer might still be speaking. ", uuid);
  1096. while (interactiveParam.checkInSpeaking()){
  1097. acquire(100);
  1098. }
  1099. }
  1100. if (checkCallSession()) {
  1101. return;
  1102. }
  1103. //如果没有接收到asr识别结果,则延迟下,继续等待0.5秒钟:
  1104. if (asrResultEx.size() == 0) {
  1105. acquire(500);
  1106. }
  1107. // #region debug-point xfyun-asr-no-response-wait-exit
  1108. logger.info("{} dbg_wait_customer exit, asrQueueSize={}, inSpeaking={}, noVoiceCounter={}",
  1109. getTraceId(),
  1110. asrResultEx.size(),
  1111. interactiveParam.checkInSpeaking(),
  1112. noVoiceCounter.get()
  1113. );
  1114. // #endregion
  1115. if (checkCallSession()) {
  1116. return;
  1117. }
  1118. if (asrResultEx.size() == 0) {
  1119. logger.info("{} No asr result got: NO_VOICE ", getTraceId());
  1120. } else {
  1121. calleeSpeakNumber.incrementAndGet();
  1122. }
  1123. interactRounds.incrementAndGet();
  1124. int muteTimeLong = (int) (System.currentTimeMillis() - startWaitTimeMills);
  1125. logger.info("{} The time spent waiting for the customer to finish speaking is {} ms.", getTraceId(), muteTimeLong);
  1126. interactWithRobot();
  1127. }
  1128. /**
  1129. * 显示本通电话的 No_Voice轮次,打印 语音识别连接是否成功信息;
  1130. */
  1131. private void displayNoVoiceNum() {
  1132. String tips = "";
  1133. if (calleeSpeakNumber.get() == 0) {
  1134. tips = "No speech was detected all over the session.";
  1135. }
  1136. logger.info("{} {} calleeSpeakNumber:{}, interactRounds:{}, NO_VOICE_NUMBER:{}, dropAsrCounter:{}, wavFile: {}, recordings file exists:{}",
  1137. getTraceId(),
  1138. tips,
  1139. calleeSpeakNumber.get(),
  1140. interactRounds.get(),
  1141. interactRounds.get() - calleeSpeakNumber.get(),
  1142. dropAsrCounter.get(),
  1143. this.recordingsFileName,
  1144. interactRounds.get() != 0 ? new java.io.File(this.recordingsFileName).exists() : "No talk interaction."
  1145. );
  1146. }
  1147. /**
  1148. * saveCdr
  1149. */
  1150. private void saveCdr(Map<String, String> headers) {
  1151. String hangupCause = headers.get("Hangup-Cause");
  1152. String sipCode = headers.get("variable_proto_specific_hangup_cause");
  1153. logger.info("{} session is hangup, hangupCause={}, sipCode={}", getTraceId(), hangupCause, sipCode);
  1154. callDetail.setExtnum("robot");
  1155. callDetail.setOpnum("robot");
  1156. callDetail.setHangupTime(System.currentTimeMillis());
  1157. callDetail.setChatContent(chatRobot.getDialogues());
  1158. long timeLen = System.currentTimeMillis() - callDetail.getInboundTime();
  1159. long answeredTimeLen = System.currentTimeMillis() - callDetail.getAnsweredTime();
  1160. callDetail.setTimeLen(timeLen);
  1161. callDetail.setAnsweredTimeLen(answeredTimeLen);
  1162. if(StringUtils.isEmpty(chatRobot.getCallDetail().getHangupCause())){
  1163. CommonUtils.setHangupCauseDetail(
  1164. callDetail,
  1165. hangupCause,
  1166. "sip-code=" + sipCode
  1167. );
  1168. }
  1169. AcdSqlQueue.addToSqlQueue(callDetail);
  1170. }
  1171. }