def save_info_with_para(cls, question: str, session_id: str, match_list: list, qa: QA, qa_manager: QAManager, **kwargs) -> list: """ 将传参的参数值保存到session的info字典中 @param {str} question - 原始问题 @param {str} session_id - session_id @param {list} match_list - 匹配上的问题、答案对象: [(StdQuestion, Answer)] @param {QA} qa - 服务器的问答处理模块实例对象 @param {QAManager} qa_manager - 服务器的问答数据管理实例对象 @param {kwargs} - 要保存的参数 @returns {list} - 按照不同的处理要求返回内容 'answer', [str, ...] - 直接返回回复内容,第二个参数为回复内容 注:如果第二个参数返回None代表使用传入的答案的answer字段作为提示 'to', int - 跳转到指定问题处理,第二个参数为std_question_id """ _info = dict() for _key in kwargs: if _key not in ('action', 'is_sure'): _info[_key] = kwargs[_key] qa.update_session_info(session_id, _info) # 直接返回当前答案的提示信息 return 'answer', None
def _complaint_op_predeal(cls, op_para: dict, question: str, session_id: str, qa: QA, qa_manager: QAManager, **kwargs): """ 留言的表单操作预处理函数 @param {dict} op_para - 上送的表单操作参数 @param {str} question - 原始问题 @param {str} session_id - session_id @param {QA} qa - 服务器的问答处理模块实例对象 @param {QAManager} qa_manager - 服务器的问答数据管理实例对象 @param {kwargs} - 扩展传入参数 @returns {str, object} - 返回控制处理的参数: action, action_para action取值的类型和对应的行为如下: 'answer', [str, ...] - 直接返回回复内容,第二个参数为回复内容 'to', int - 跳转到指定问题处理,第二个参数为std_question_id 'save', None - 根据op_para参数保存表单 'upd', None - 根据op_para参数更新表单 'preview', None - 获取表单预览信息 'get', None - 获取表单完整信息 'create', None - 创建表单 """ # 检查用户是否已登陆 if qa.get_info_by_key(session_id, 'user_id', -1) == -1: return 'answer', [ '亲, 投诉需要先登陆哦', ] # 不改变操作模式 return op_para['action'], None
def multiple_save_info(cls, question: str, session_id: str, context_id: str, std_question_id: int, collection: str, partition: str, qa: QA, qa_manager: QAManager, **kwargs): """ 多轮问答保存信息至session的info中 @param {str} question - 客户反馈的信息文本(提问回答) @param {str} session_id - 客户的session id @param {str} context_id - 上下文临时id @param {int} std_question_id - 上下文中对应的提问问题id @param {str} collection - 提问答案参数指定的问题分类 @param {str} partition - 提问答案参数指定的场景标签 @param {QA} qa - 服务器的问答处理模块实例对象 @param {QAManager} qa_manager - 服务器的问答数据管理实例对象 @param {kwargs} - 扩展传入参数 ask {list} - 问题列表,注意最后的问题必须有tips或to参数 [ {'info_key': '保存的key值', 'next_tips': '下一个问题'}, ..., {'info_key': '保存的key值', 'tips': '处理完成提示', 'to': 跳转到指定问题id}, ] @param {str, object} - 按照不同的处理要求返回内容 'answer', [str, ...] - 直接返回回复内容,第二个参数为回复内容 'to', int - 跳转到指定问题处理,第二个参数为std_question_id 'again', [str, ...] - 再获取一次答案,第二个参数为提示内容,如果第2个参数为None代表使用原来的参数再提问一次 'break', [collection, partition] - 跳出问题(让问题继续走匹配流程),可以返回[collection, partition]变更分类和场景 默认为'again' """ _step = qa.get_cache_value(session_id, 'multiple_save_info', {}).get(context_id, 0) _ask = kwargs.get('ask')[_step] # 保存值 _info_key = _ask.get('info_key') _info = dict() _info[_info_key] = question qa.update_session_info(session_id, _info) # 处理下一个问题 _to = _ask.get('to', None) _tips = _ask.get('tips', None) _next_tips = _ask.get('next_tips', None) if _to is not None or _tips is not None: # 最后一个问题 qa.del_cache(session_id, 'multiple_save_info') if _to is None: return 'answer', [ _tips, ] else: return 'to', _to else: # 还有下一个问题 qa.add_cache(session_id, 'multiple_save_info', {context_id: _step + 1}) return 'again', [ _next_tips, ]
def _complaint_get_default(cls, question: str, session_id: str, qa: QA, qa_manager: QAManager, **kwargs) -> dict: """ 投诉表单默认值字典的函数 @param {str} question - 原始问题 @param {str} session_id - session_id @param {QA} qa - 服务器的问答处理模块实例对象 @param {QAManager} qa_manager - 服务器的问答数据管理实例对象 @param {kwargs} - 扩展传入参数 @returns {dict} - 返回的默认值字典 """ _dict = { 'user_id': qa.get_info_by_key(session_id, 'user_id', -1), 'user_name': qa.get_info_by_key(session_id, 'user_name', ''), } if question not in ['投诉', '我要投诉']: _dict['content'] = question return _dict
def save_info(cls, question: str, session_id: str, context_id: str, std_question_id: int, collection: str, partition: str, qa: QA, qa_manager: QAManager, **kwargs): """ 直接保存信息至session的info中 @param {str} question - 客户反馈的信息文本(提问回答) @param {str} session_id - 客户的session id @param {str} context_id - 上下文临时id @param {int} std_question_id - 上下文中对应的提问问题id @param {str} collection - 提问答案参数指定的问题分类 @param {str} partition - 提问答案参数指定的场景标签 @param {QA} qa - 服务器的问答处理模块实例对象 @param {QAManager} qa_manager - 服务器的问答数据管理实例对象 @param {kwargs} - 扩展传入参数 info_key {str} - 从info字典中设置的key值 to {int} - 处理完成后跳转到要处理的问题id,如果有该值则tips参数无效 tips {str} - 处理完成的提示信息 @returns {str, object} - 按照不同的处理要求返回内容 'answer', [str, ...] - 直接返回回复内容,第二个参数为回复内容 'to', int - 跳转到指定问题处理,第二个参数为std_question_id 'again', [str, ...] - 再获取一次答案,第二个参数为提示内容,如果第2个参数为None代表使用原来的参数再提问一次 'break', [collection, partition] - 跳出问题(让问题继续走匹配流程),可以返回[collection, partition]变更分类和场景 默认为'again' """ _info_key = kwargs.get('info_key') _tips = kwargs.get('tips', 'save success!') _to = kwargs.get('to', None) _info = dict() _info[_info_key] = question qa.update_session_info(session_id, _info) if _to is None: return 'answer', [ _tips, ] else: return 'to', _to
def save_msg(cls, question: str, session_id: str, context_id: str, std_question_id: int, collection: str, partition: str, qa: QA, qa_manager: QAManager, **kwargs): """ 保存留言信息 @param {str} question - 客户反馈的信息文本(提问回答) @param {str} session_id - 客户的session id @param {str} context_id - 上下文临时id @param {int} std_question_id - 上下文中对应的提问问题id @param {str} collection - 提问答案参数指定的问题分类 @param {str} partition - 提问答案参数指定的场景标签 @param {QA} qa - 服务器的问答处理模块实例对象 @param {QAManager} qa_manager - 服务器的问答数据管理实例对象 @param {kwargs} - 扩展传入参数 @returns {str, object} - 按照不同的处理要求返回内容 'answer', [str, ...] - 直接返回回复内容,第二个参数为回复内容 'to', int - 跳转到指定问题处理,第二个参数为std_question_id 'again', [str, ...] - 再获取一次答案,第二个参数为提示内容,如果第2个参数为None代表使用原来的参数再提问一次 'break', [collection, partition] - 跳出问题(让问题继续走匹配流程),可以返回[collection, partition]变更分类和场景 默认为'again' """ _context_dict = qa.get_context_dict(session_id) if 'leave_message' not in _context_dict['ask'].keys(): # 第一次进入留言模块 _leave_message = { 'context_id': context_id, # 临时会话id 'ref_id': -1, 'ref_msg': '', 'pic_urls': [], } if 'action' in kwargs.keys(): # 操作意图发起,属于客户聊天发起的留言 if kwargs['match_type'] == 'nlp_match' and len(question) > len( kwargs['match_word']) + 4: # 分词模式并且留言后面有内容,可以直接保存 _leave_message['msg'] = question else: # 直接发起的留言,可以传入引用信息 _ref_info = eval(question) _ref_id = _ref_info.get('ref_id', -1) if _ref_id != -1: _ref_data = LeaveMessagePluginData.get_or_none( LeaveMessagePluginData.id == _ref_id) if _ref_data is not None and _ref_data.ref_id != -1: _ref_id = _ref_data.ref_id _leave_message['ref_id'] = _ref_id _leave_message['ref_msg'] = _ref_info.get('ref_msg', '') else: # 第二次进入留言模块,检查是否上传图片的记录 _leave_message = _context_dict['ask']['leave_message'] try: _op_para = eval(question) _context_id = _op_para.get('context_id', '') if _context_id != context_id: # 非本次会话的留言 return 'answer', [ LEAVE_MESSAGE_PLUGIN_TIPS['context_error'] ] if 'action' in _op_para.keys(): if _op_para['action'] == 'upload_file': _leave_message['pic_urls'].append(_op_para['url']) elif _op_para['action'] == 'cancle': # 取消留言 return 'answer', [{ 'data_type': 'leave_message', 'tips': LEAVE_MESSAGE_PLUGIN_TIPS['cancle'], 'action': 'cancle', 'context_id': context_id }] else: _leave_message['msg'] = question else: _leave_message['msg'] = question except: _leave_message['msg'] = question if 'msg' not in _leave_message.keys(): # 第一次处理,保存问题缓存并提示客户回复留言 _context_dict['ask']['leave_message'] = _leave_message qa.add_ask_context(session_id, _context_dict['ask']) if len(_leave_message['pic_urls']) > 0: return 'again', [ LEAVE_MESSAGE_PLUGIN_TIPS['upload_success'], ] else: return 'again', [{ 'data_type': 'leave_message', 'tips': LEAVE_MESSAGE_PLUGIN_TIPS['start_tips'], 'action': 'add', 'context_id': context_id }] else: # 保存留言 _data = LeaveMessagePluginData.create( ref_id=_leave_message['ref_id'], user_id=qa.get_info_by_key(session_id, 'user_id', default=-1), user_name=qa.get_info_by_key(session_id, 'user_name', ''), msg=_leave_message['msg'], ref_msg=_leave_message['ref_msg'], pic_urls=','.join(_leave_message['pic_urls']), ) # 返回客户提示 return 'answer', [{ 'data_type': 'leave_message', 'tips': LEAVE_MESSAGE_PLUGIN_TIPS['success'], 'action': 'success', 'context_id': context_id, 'msg_id': _data.id, }]
def __init__(self, server_config: dict, app: Flask = None, **kwargs): """ 初始化QA问答服务 @param {dict} server_config - 服务配置字典 @param {Flask} app=None - 服务 """ self.debug = server_config.get('debug', True) self.execute_path = server_config['execute_path'] # 日志处理 self.logger: Logger = None if 'logger' in server_config.keys(): _logger_config = server_config['logger'] if len(_logger_config['conf_file_name'] ) > 0 and _logger_config['conf_file_name'][0] == '.': # 相对路径 _logger_config['conf_file_name'] = os.path.join( self.execute_path, _logger_config['conf_file_name']) if len(_logger_config['logfile_path'] ) > 0 and _logger_config['logfile_path'][0] == '.': # 相对路径 _logger_config['logfile_path'] = os.path.join( self.execute_path, _logger_config['logfile_path']) self.logger = Logger.create_logger_by_dict(_logger_config) self.server_config = server_config self.app = app if self.app is None: self.app = Flask(__name__) CORS(self.app) self.app.debug = self.debug self.app.send_file_max_age_default = datetime.timedelta( seconds=1) # 设置文件缓存1秒 self.app.config['JSON_AS_ASCII'] = False # 显示中文 # 上传文件大小限制 self.app.config['MAX_CONTENT_LENGTH'] = math.floor( self.server_config['max_upload_size'] * 1024 * 1024) # 插件字典,先定义清单,启动前完成加载 self.extend_plugin_path = self.server_config.get( 'extend_plugin_path', '') self.plugins = dict() # 装载数据管理模块 self.qa_manager = QAManager( self.server_config['answerdb'], self.server_config['milvus'], self.server_config['bert_client'], logger=self.logger, excel_batch_num=self.server_config['excel_batch_num'], excel_engine=self.server_config['excel_engine']) # 装载NLP _nlp_config = self.server_config['nlp_config'] _user_dict = None if _nlp_config['user_dict'] != '': _user_dict = _nlp_config['user_dict'] if _user_dict.startswith('.'): # 相对路径 _user_dict = os.path.join(self.execute_path, _user_dict) _set_dictionary = None if _nlp_config['set_dictionary'] != '': _set_dictionary = _nlp_config['set_dictionary'] if _set_dictionary.startswith('.'): # 相对路径 _set_dictionary = os.path.join(self.execute_path, _set_dictionary) self.nlp = NLP(plugins=self.plugins, data_manager_para=self.qa_manager.DATA_MANAGER_PARA, set_dictionary=None if _nlp_config['set_dictionary'] == '' else _nlp_config['set_dictionary'], user_dict=_user_dict, enable_paddle=_nlp_config['enable_paddle'], parallel_num=_nlp_config.get('parallel_num', None), logger=self.logger) # 初始化QA模块 self.qa = QA(self.qa_manager, self.nlp, self.server_config['execute_path'], plugins=self.plugins, qa_config=self.server_config['qa_config'], redis_config=self.server_config['redis'], logger=self.logger) # 动态加载路由 self.api_class = [Qa, QaDataManager] # 完成插件的加载 # plugins函数字典,格式为{'type':{'class_name': {'fun_name': fun, }, },} self.load_plugins(os.path.join(self.execute_path, 'plugins')) if self.extend_plugin_path != '': if self.extend_plugin_path[0:1] == '.': # 相对路径 self.extend_plugin_path = os.path.join(self.execute_path, self.extend_plugin_path) self.load_plugins(self.extend_plugin_path) # 安全关联 _security = self.server_config['security'] self.token_serializer = Serializer( _security['secret_key'], _security['token_expire'], salt=bytes(_security['salt'], encoding='utf-8'), algorithm_name=_security['algorithm_name']) # 验证ip白名单处理 _security['token_server_auth_ip_list'] = _security[ 'token_server_auth_ip_list'].split(',') # 增加令牌服务的路由 if _security['enable_token_server']: self.api_class.append(TokenServer) # 增加静态路径 _static_path = self.server_config['static_path'] if _static_path[0:1] == '.': # 相对路径 _static_path = os.path.realpath( os.path.join(self.execute_path, _static_path)) self.app.static_folder = os.path.join(_static_path, 'static') self.app.static_url_path = '/static/' # 增加客户端路由 if self.server_config['enable_client']: # 客户端路由api服务 self.api_class.append(Client) # 创建测试用户 if self.server_config['add_test_login_user']: _user = RestfulApiUser.get_or_none( RestfulApiUser.user_name == 'test') if _user is None: self.register_user('test', '123456') # 加入客户端主页 self.app.url_map.add(Rule('/', endpoint='client', methods=['GET'])) self.app.view_functions['client'] = self._client_view_function FlaskTool.add_route_by_class(self.app, self.api_class) self._log_debug(str(self.app.url_map))
def test_pay_fun(cls, question: str, session_id: str, context_id: str, std_question_id: int, collection: str, partition: str, qa: QA, qa_manager: QAManager, **kwargs): """ 多轮转账的示例 @param {str} question - 客户反馈的信息文本(提问回答) @param {str} session_id - 客户的session id @param {str} context_id - 上下文临时id @param {int} std_question_id - 上下文中对应的提问问题id @param {str} collection - 提问答案参数指定的问题分类 @param {str} partition - 提问答案参数指定的场景标签 @param {QA} qa - 服务器的问答处理模块实例对象 @param {QAManager} qa_manager - 服务器的问答数据管理实例对象 @param {kwargs} - 扩展传入参数 @returns {str, object} - 按照不同的处理要求返回内容 'answer', [str, ...] - 直接返回回复内容,第二个参数为回复内容 'to', int - 跳转到指定问题处理,第二个参数为std_question_id 'again', [str, ...] - 再获取一次答案,第二个参数为提示内容,如果第2个参数为None代表使用原来的参数再提问一次 默认为'again' """ _cache = qa.get_cache_dict(session_id, default={}, context_id=context_id) # 存入转账信息 if 'step' not in _cache.keys(): # 通过nlp意图发起的处理 qa.update_cache_dict(session_id, kwargs, context_id=context_id) _cache = qa.get_cache_dict(session_id, default={}, context_id=context_id) elif _cache['step'] == 'in_name': _cache['in_name'] = question qa.add_cache(session_id, 'in_name', question, context_id=context_id) elif _cache['step'] == 'amount': _cache['amount'] = question qa.add_cache(session_id, 'amount', question, context_id=context_id) elif _cache['step'] == 'confirm': if question == '是的': return 'answer', ['执行向 {$cache=in_name$} 转账 {$cache=amount$}'] else: return 'answer', ['取消转账操作'] # 判断要问的问题 if 'in_name' not in _cache.keys(): qa.add_cache(session_id, 'step', 'in_name', context_id=context_id) return 'again', ['请输入收款人名称'] elif 'amount' not in _cache.keys(): qa.add_cache(session_id, 'step', 'amount', context_id=context_id) return 'again', ['请输入转账金额'] else: # 最后一次确认 qa.add_cache(session_id, 'step', 'confirm', context_id=context_id) return 'again', [ '您确定要向 {$cache=in_name$} 转账 {$cache=amount$} 吗?输入 是的 执行转账操作,输入其他将取消操作' ]
def weather(cls, question: str, session_id: str, context_id: str, std_question_id: int, collection: str, partition: str, qa: QA, qa_manager: QAManager, **kwargs): """ 查询天气 @param {str} question - 客户反馈的信息文本(提问回答) @param {str} session_id - 客户的session id @param {str} context_id - 上下文临时id @param {int} std_question_id - 上下文中对应的提问问题id @param {str} collection - 提问答案参数指定的问题分类 @param {str} partition - 提问答案参数指定的场景标签 @param {QA} qa - 服务器的问答处理模块实例对象 @param {QAManager} qa_manager - 服务器的问答数据管理实例对象 @param {kwargs} - 扩展传入参数 time {list} - nlp分词的时间词语列表 addr {list} - nlp分词的地址词语列表 @returns {str, object} - 按照不同的处理要求返回内容 'answer', [str, ...] - 直接返回回复内容,第二个参数为回复内容 'to', int - 跳转到指定问题处理,第二个参数为std_question_id 'again', [str, ...] - 再获取一次答案,第二个参数为提示内容,如果第2个参数为None代表使用原来的参数再提问一次 'break', [collection, partition] - 跳出问题(让问题继续走匹配流程),可以返回[collection, partition]变更分类和场景 默认为'again' """ _match_city_code = '' _match_day = 0 # 0代表当前,1代表明天,2代表后天 # 检查是不是第二次的选项 _cache_info = qa.get_cache_value(session_id, 'weather', default=None, context_id=context_id) if _cache_info is not None: if question.isdigit(): if question not in _cache_info['addr_dict'].keys(): # 回答不在选项范围 return 'again', [ '亲, 您输入的序号不对, 请输入正确的序号选择城市', ] else: _match_day = _cache_info['day'] _match_city_code = _cache_info['addr_dict'][question] else: # 不是选项,是文字 return 'answer', [ WEATHER_ERROR, ] else: # 尝试获取日期 for _word in kwargs.get('time', []): if _word in ['明天']: _match_day = 1 elif _word in ['后天']: _match_day = 2 # 尝试获取地址 with redis.Redis(connection_pool=qa.redis_pool) as _redis: _match_addr = cls._search_city(kwargs.get('addr', []), _redis) if len(_match_addr) == 0: # 没有匹配到地址,尝试获取客户info中的地址 _addr = qa.get_info_by_key(session_id, 'addr', default='') if _addr == '' and WEATHER_TRY_USE_IP_ADDR: # 尝试通过IP地址获取地址 _addr = cls._get_addr_by_ip( qa.get_info_by_key(session_id, 'ip', default=''), qa) if _addr != '': _addrs_with_class = qa.nlp.cut_sentence(_addr) _addrs = [item[0] for item in _addrs_with_class] _match_addr = cls._search_city(_addrs, _redis) _len = len(_match_addr) if _len == 1: _match_city_code = _match_addr[0][1][0] elif _len > 1: # 匹配到多个,进行提问让客户选择 _tips = ['找到了多个地址,您想查询哪个地址的天气?请输入序号进行选择: '] _index = 1 _cache_info = dict() _cache_info['day'] = _match_day _cache_info['addr_dict'] = dict() for item in _match_addr: # 加入到上下文 _cache_info['addr_dict'][str(_index)] = item[1][0] # 获取详细地址 _tips.append( '%d.%s' % (_index, cls._get_city_full_name(item[0], _redis))) _index += 1 # 添加到cache中 qa.add_cache(session_id, 'weather', _cache_info, context_id=context_id) # 重新提问 return 'again', _tips if _match_city_code == '': # 没有找到地址,直接返回退出 return 'answer', [ WEATHER_ERROR, ] # 查询天气, 先查本地缓存 _today = datetime.datetime.now().strftime('%Y-%m-%d') with redis.Redis(connection_pool=qa.redis_pool) as _redis: _json = _redis.get('api_tool_ask:weather:cache:%s:%s' % (_match_city_code, _today)) if _json is None: # 要进行查询 _api_back = NetTool.restful_api_call( 'http://t.weather.sojson.com/api/weather/city/%s' % _match_city_code, back_type='text', logger=qa.logger) if _api_back['is_success']: _json = _api_back['back_object'] _wdata = json.loads(_json) if _wdata['status'] == 200: # 存入缓存,一天到期 _redis.set('api_tool_ask:weather:cache:%s:%s' % (_match_city_code, _today), _json, ex=86400) else: return 'answer', [ WEATHER_ERROR, ] else: # 获取失败 return 'answer', [ WEATHER_ERROR, ] else: _wdata = json.loads(_json) # 返回结果 _end_str = '' # 结束语 if _match_day == 0: _end_str = '湿度%s, PM2.5: %s, 空气质量%s, %s, ' % ( _wdata['data']['shidu'], _wdata['data']['pm25'], _wdata['data']['quality'], _wdata['data']['ganmao']) _answer = '亲, %s%s的天气%s, 吹%s%s, %s, %s, %s%s' % ( _wdata['cityInfo']['city'], _wdata['data']['forecast'][_match_day]['ymd'], _wdata['data']['forecast'][_match_day]['type'], _wdata['data']['forecast'][_match_day]['fl'], _wdata['data']['forecast'][_match_day]['fx'], _wdata['data']['forecast'][_match_day]['high'], _wdata['data']['forecast'][_match_day]['low'], _end_str, _wdata['data']['forecast'][_match_day]['notice']) return 'answer', [ _answer, ]