大家好!我是CSDN的Python新手博主~ 上一篇我们完成了看板的交互式可视化升级与全终端适配,解决了数据分析效率与移动办公需求,但企业用户反馈三大核心痛点:① 操作仍需手动点击,查询数据/执行任务需逐层找功能,高频操作繁琐;② 无语音交互能力,办公时双手忙碌(如整理文件)无法操作电脑,无法语音查数据、发指令;③ 办公常见问题无自动化解答,新人上手需翻阅大量文档,运维人员反复解答同类问题,效率低下。今天就带来超落地的新手实战项目------办公看板集成AI智能助手+语音交互+自动化问答!
本次基于之前的"全终端交互式看板"代码,新增3大核心功能:① AI智能助手(基于大模型实现自然语言交互,支持文字查询数据、执行定时任务/同步操作,无需手动点击);② 语音交互(集成语音识别/合成,实现语音查数据、语音发指令、语音接收结果,解放双手);③ 自动化问答(搭建企业办公知识库,适配常见问题智能匹配、自动化回复,支持知识库手动/批量维护)。全程基于现有技术栈(Flask+MySQL+ECharts+Redis),新增自然语言解析模块、语音交互引擎、知识库问答工具,代码注释详细,新手只需配置大模型参数、导入知识库数据,跟着步骤复制操作就能成功,让看板实现"能听、会说、懂问答、能办事"的智能办公体验~
一、本次学习目标
-
掌握大模型自然语言解析技巧,实现自然语言到看板操作的转换,支持文字查询数据、执行自动化任务,新手也能快速配置交互规则;
-
学会语音交互开发,集成语音识别(SpeechRecognition)与文字转语音(TTS),实现语音查询、语音指令、语音播报结果,适配办公场景解放双手;
-
理解企业知识库搭建逻辑,实现常见问题的智能匹配、自动化问答,支持知识库的增删改查、批量导入导出,降低新人学习成本;
-
实现智能助手、语音交互、自动化问答与现有看板功能的闭环联动,语音/文字指令可直接触达数据查询、任务执行、问题解答;
-
适配企业办公实际场景,支持智能交互日志追溯、语音指令权限管控、知识库按角色分类查看,兼顾智能性与安全性。
二、前期准备
- 安装核心依赖库
安装核心依赖(语音识别/合成、大模型交互、知识库匹配)
pip3 install SpeechRecognition pyttsx3 openai fuzzywuzzy python-Levenshtein flask-cors -i https://pypi.tuna.tsinghua.edu.cn/simple
语音识别依赖(PyAudio,Windows/Linux/Mac安装方式不同,新手按需执行)
Windows
pip3 install pyaudio -i https://pypi.tuna.tsinghua.edu.cn/simple
Linux
apt-get install portaudio19-dev
pip3 install pyaudio
Mac
brew install portaudio
pip3 install pyaudio
确保已有依赖正常(Flask、Redis、ECharts等)
pip3 install --upgrade flask flask-login flask-sqlalchemy redis pymysql openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simple
说明:语音识别基于SpeechRecognition+PyAudio实现(支持麦克风实时识别),文字转语音基于pyttsx3实现(离线可用,无需联网);AI智能助手基于OpenAI GPT实现自然语言解析,可替换为国内大模型(如文心一言、通义千问);知识库智能匹配基于fuzzywuzzy实现模糊匹配,适配口语化问题查询。
- 第三方服务与配置准备
-
大模型配置:获取OpenAI API Key(或国内大模型API地址/密钥),配置交互提示词(限定办公看板操作范围)、响应格式(结构化JSON),设置请求超时时间与重试次数;
-
语音交互配置:定义语音指令规则(如"查询今日订单量""触发ERP同步""打开销售数据图表"),设置语音识别语言(zh-CN)、TTS语音语速/音量,配置语音指令权限(仅允许有权限的用户执行高危语音指令);
-
知识库配置:梳理企业办公常见问题(如"如何导出审计日志""如何配置定时任务""看板忘记密码怎么办"),按问题类型分类(操作类/故障类/配置类),准备问题-答案对照表,支持批量导入Excel;
-
安全配置:在.env文件中补充大模型API密钥、语音指令白名单、知识库角色访问权限,开启智能交互/语音操作日志记录,确保操作可追溯;Redis缓存知识库数据,提升问答匹配速度。
- 数据库表优化与创建
-- 连接MySQL数据库(替换为你的数据库信息)
mysql -u office_user -p -h 47.108.xxx.xxx office_data
-- 创建企业知识库表(company_knowledge)
CREATE TABLE company_knowledge (
id INT AUTO_INCREMENT PRIMARY KEY,
question VARCHAR(500) NOT NULL COMMENT '问题(支持口语化)',
answer TEXT NOT NULL COMMENT '答案(支持富文本/链接)',
question_type ENUM('operation', 'fault', 'config', 'other') NOT NULL COMMENT '问题类型:操作/故障/配置/其他',
role_ids TEXT NOT NULL COMMENT '可查看的角色ID(JSON格式,[]表示所有角色)',
is_top TINYINT(1) DEFAULT 0 COMMENT '是否置顶(1-是,0-否)',
click_count INT DEFAULT 0 COMMENT '点击次数',
create_by INT NOT NULL COMMENT '创建人ID',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
KEY idx_question_type (question_type),
KEY idx_is_top (is_top),
FULLTEXT KEY idx_question (question) -- 全文索引,提升问题匹配速度
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='企业办公知识库表';
-- 创建智能交互日志表(ai_interact_log)
CREATE TABLE ai_interact_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL COMMENT '用户ID',
interact_type ENUM('text', 'voice') NOT NULL COMMENT '交互类型:文字/语音',
input_content VARCHAR(1000) NOT NULL COMMENT '输入内容(文字/语音转文字)',
output_content TEXT NOT NULL COMMENT '输出内容',
operate_action VARCHAR(100) NULL COMMENT '触发的看板操作(如query_data/run_task)',
operate_result ENUM('success', 'fail', 'no_operate') DEFAULT 'no_operate' COMMENT '操作执行结果',
ip_address VARCHAR(50) NOT NULL COMMENT '操作IP',
terminal_type ENUM('pc', 'mobile', 'pad') NOT NULL COMMENT '终端类型',
interact_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '交互时间',
KEY idx_user_id (user_id),
KEY idx_interact_type (interact_type),
KEY idx_interact_time (interact_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI智能助手交互日志表';
-- 创建问答记录统计表(qa_statistics)
CREATE TABLE qa_statistics (
id INT AUTO_INCREMENT PRIMARY KEY,
knowledge_id INT NULL COMMENT '关联知识库ID(NULL表示未匹配到)',
user_id INT NOT NULL COMMENT '提问用户ID',
question VARCHAR(500) NOT NULL COMMENT '用户提问内容',
match_score INT DEFAULT 0 COMMENT '问题匹配相似度(0-100)',
is_satisfied TINYINT(1) NULL COMMENT '是否满意(1-是,0-否,NULL表示未评价)',
ask_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '提问时间',
KEY idx_knowledge_id (knowledge_id),
KEY idx_ask_time (ask_time),
FOREIGN KEY (knowledge_id) REFERENCES company_knowledge(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='自动化问答记录统计表';
-- 初始化知识库示例数据
INSERT INTO company_knowledge (question, answer, question_type, role_ids, is_top, create_by)
VALUES (
'如何导出今日的销售订单数据',
'1. 进入看板【销售数据】页面;2. 筛选时间为【今日】;3. 点击页面右侧【导出】按钮,选择Excel格式即可导出。',
'operation',
'[]',
1,
1
);
INSERT INTO company_knowledge (question, answer, question_type, role_ids, is_top, create_by)
VALUES (
'ERP数据同步失败怎么办',
'1. 进入看板【系统管理】-【同步日志】查看失败原因;2. 常见原因:ERP接口密钥过期/网络不通/数据格式错误;3. 对应解决:更新密钥/检查网络/核对数据字段映射。',
'fault',
'[]',
1,
1
);
INSERT INTO company_knowledge (question, answer, question_type, role_ids, is_top, create_by)
VALUES (
'如何配置定时导出报表并邮件推送',
'1. 进入看板【报表管理】-【定制报表】;2. 选择需导出的报表,开启【定时导出】;3. 配置Cron表达式与接收人邮箱,保存即可。',
'config',
'[1]', -- 仅管理员可查看
0,
1
);
三、实战:AI智能助手+语音交互+自动化问答集成
- 第一步:搭建AI智能助手引擎,实现自然语言交互办事
-- coding: utf-8 --
ai_assistant.py AI智能助手引擎(自然语言交互)
import json
import time
from flask import Blueprint, request, jsonify, g
from flask_login import login_required, current_user
from models import db, AIInteractLog, Order, ERPSyncLog, ReportConfig
from audit_engine import audit_log, get_terminal_type
from fuzzywuzzy import fuzz
from dotenv import load_dotenv
import os
import redis
from openai import OpenAI
加载环境变量
load_dotenv()
ai_bp = Blueprint("ai_assistant", name)
初始化大模型客户端(OpenAI,可替换为国内大模型)
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-3.5-turbo")
client = OpenAI(api_key=OPENAI_API_KEY)
初始化Redis(缓存大模型解析结果、高频数据,提升响应速度)
redis_client = redis.Redis(
host=os.getenv("REDIS_HOST", "localhost"),
port=int(os.getenv("REDIS_PORT", 6379)),
db=int(os.getenv("REDIS_DB", 1)),
decode_responses=True
)
看板操作映射(限定大模型可执行的操作范围,避免越权)
OPERATE_MAP = {
"query_data": ["查询", "查看", "统计"],
"run_sync": ["同步", "更新", "刷新"],
"run_task": ["执行", "触发", "启动"],
"export_data": ["导出", "下载"]
}
====================== 核心功能:大模型自然语言解析 ======================
def parse_natural_language(input_text):
"""
调用大模型解析自然语言,提取操作意图、参数
:param input_text: 用户输入的自然语言内容
:return: 结构化解析结果(dict)
"""
构建提示词(限定范围、指定格式,确保解析结果可直接使用)
prompt = f"""
你是企业办公看板的智能助手,仅能处理看板相关的自然语言请求,需完成以下任务:
-
提取用户的操作意图 ,仅能从【query_data/run_sync/run_task/export_data/no_operate】中选择,no_operate表示无具体操作(仅问答);
-
若操作意图为query_data,提取查询参数 :数据类型(订单/销售/同步日志)、时间范围(今日/昨日/近7天/本月)、筛选条件(部门/状态);
-
若操作意图为run_sync/run_task/export_data,提取执行参数 :目标对象(ERP/订单报表/销售数据)、执行方式(立即/定时);
-
若无法识别或非看板相关请求,操作意图为no_operate,参数为空;
-
严格按以下JSON格式返回,无多余文字,键名不可修改:
{{
"operate_intent": "操作意图",
"params": {{
"target": "目标对象",
"time_range": "时间范围",
"filter": "筛选条件"
}}
}}
用户请求:{input_text}
"""
try:
# 调用大模型API
response = client.chat.completions.create(
model=OPENAI_MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=0.1, # 降低随机性,确保解析精准
timeout=30
)
# 解析返回结果
parse_result = json.loads(response.choices[0].message.content.strip())
return parse_result
except Exception as e:
# 解析失败,返回默认结果
return {
"operate_intent": "no_operate",
"params": {"target": "", "time_range": "", "filter": ""},
"error": f"解析失败:{str(e)}"
}
====================== 核心功能:执行看板操作(根据解析结果) ======================
def execute_board_operate(operate_intent, params):
"""
根据自然语言解析结果,执行对应的看板操作
:param operate_intent: 操作意图
:param params: 执行参数
:return: 操作结果(str)、操作状态(success/fail/no_operate)
"""
if operate_intent == "no_operate":
return "暂无具体看板操作,可尝试查询数据/执行同步任务~", "no_operate"
# 操作1:查询数据(订单/销售/同步日志)
if operate_intent == "query_data":
target = params.get("target", "订单")
time_range = params.get("time_range", "今日")
# 时间范围映射(复用之前的TIME_RANGE_MAP)
from chart_api import TIME_RANGE_MAP
start_time = TIME_RANGE_MAP.get(time_range, TIME_RANGE_MAP["today"])
try:
if target in ["订单", "销售"]:
# 查询订单/销售数据
query = Order.query.filter(Order.create_time >= start_time)
# 权限过滤
if not current_user.has_perm("view_all_data"):
user_dept_ids = [d.id for d in current_user.departments]
query = query.filter(Order.dept_id.in_(user_dept_ids))
data_count = query.count()
data_amount = query.with_entities(db.func.sum(Order.amount)).scalar() or 0
return f"【{time_range}{target}数据】共{data_count}条,总金额{data_amount:.2f}元~", "success"
elif target == "同步日志":
# 查询ERP同步日志
log = ERPSyncLog.query.order_by(ERPSyncLog.start_time.desc()).first()
if log:
return f"【最新ERP同步日志】{log.start_time.strftime('%Y-%m-%d %H:%M')},状态:{log.status},成功{log.success_count}条,失败{log.fail_count}条~", "success"
else:
return "暂无ERP同步日志~", "success"
else:
return f"暂不支持查询{target}数据~", "fail"
except Exception as e:
return f"查询数据失败:{str(e)}", "fail"
# 操作2:执行同步(ERP/其他系统)
elif operate_intent == "run_sync":
target = params.get("target", "ERP")
if target != "ERP":
return f"暂不支持{target}数据同步~", "fail"
try:
# 调用ERP同步函数(复用之前的erp_sync.py)
from erp_sync import sync_erp_to_dashboard
sync_result = sync_erp_to_dashboard("order", "increment", current_user.username)
if sync_result["success"]:
return f"ERP订单数据已立即同步,{sync_result['msg']}~", "success"
else:
return f"ERP同步失败:{sync_result['msg']}~", "fail"
except Exception as e:
return f"执行同步失败:{str(e)}", "fail"
# 操作3:执行定时任务/导出
elif operate_intent in ["run_task", "export_data"]:
target = params.get("target", "")
if not target:
return "请指定需执行/导出的目标对象(如订单报表)~", "fail"
try:
# 匹配报表名称(模糊匹配)
report = ReportConfig.query.filter(ReportConfig.report_name.like(f"%{target}%")).first()
if not report:
return f"未找到{target}相关报表~", "fail"
if operate_intent == "export_data":
# 调用报表导出函数(复用之前的report_export.py)
from report_export import export_excel_by_template
temp_path = export_excel_by_template(report)
return f"{target}报表已立即导出,可前往【报表管理】下载~", "success"
else:
return f"{target}相关定时任务已立即执行~", "success"
except Exception as e:
return f"执行操作失败:{str(e)}", "fail"
# 未知操作
else:
return "暂不支持该操作~", "no_operate"
====================== 核心功能:记录AI交互日志 ======================
def record_ai_log(interact_type, input_content, output_content, operate_action, operate_result):
"""记录AI智能助手交互日志"""
ai_log = AIInteractLog(
user_id=current_user.id,
interact_type=interact_type,
input_content=input_content[:1000], # 限制长度
output_content=output_content,
operate_action=operate_action,
operate_result=operate_result,
ip_address=request.remote_addr,
terminal_type=get_terminal_type()
)
db.session.add(ai_log)
db.session.commit()
====================== 接口:AI智能助手文字交互 ======================
@ai_bp.route("/ai/chat/text", methods=["POST"])
@login_required
@audit_log("query")
def ai_text_chat():
"""AI智能助手文字交互接口(核心)"""
data = request.get_json()
input_text = data.get("input_text", "").strip()
if not input_text:
return jsonify({"success": False, "error": "请输入交互内容~"})
# 先从Redis缓存获取结果(高频问题,提升响应速度)
cache_key = f"ai_chat:{current_user.id}:{input_text[:50]}"
cache_result = redis_client.get(cache_key)
if cache_result:
cache_data = json.loads(cache_result)
# 记录日志
record_ai_log("text", input_text, cache_data["output"], cache_data["intent"], cache_data["result"])
return jsonify({"success": True, "output": cache_data["output"]})
# 1. 解析自然语言
parse_result = parse_natural_language(input_text)
operate_intent = parse_result.get("operate_intent", "no_operate")
params = parse_result.get("params", {})
# 2. 执行看板操作
output_content, operate_result = execute_board_operate(operate_intent, params)
# 3. 记录交互日志
record_ai_log("text", input_text, output_content, operate_intent, operate_result)
# 4. 存入Redis缓存(过期时间1小时,高频问题快速响应)
redis_client.setex(
cache_key,
3600,
json.dumps({
"output": output_content,
"intent": operate_intent,
"result": operate_result
})
)
return jsonify({"success": True, "output": output_content})
====================== 接口:AI智能助手交互日志查询 ======================
@ai_bp.route("/ai/chat/log", methods=["GET"])
@login_required
@audit_log("query")
def get_ai_chat_log():
"""查询AI智能助手交互日志"""
page = int(request.args.get("page", 1))
page_size = int(request.args.get("page_size", 20))
interact_type = request.args.get("interact_type")
# 构建查询条件(普通用户仅能查看自己的日志,管理员可查看所有)
query = AIInteractLog.query.order_by(AIInteractLog.interact_time.desc())
if not current_user.has_perm("view_all_data"):
query = query.filter(AIInteractLog.user_id == current_user.id)
if interact_type:
query = query.filter(AIInteractLog.interact_type == interact_type)
# 分页查询
pagination = query.paginate(page=page, per_page=page_size)
logs = pagination.items
# 格式化结果
log_list = []
for log in logs:
log_list.append({
"log_id": log.id,
"username": log.user.username if log.user else "",
"interact_type": {"text": "文字", "voice": "语音"}[log.interact_type],
"input_content": log.input_content,
"output_content": log.output_content,
"operate_action": log.operate_action or "无",
"operate_result": {"success": "成功", "fail": "失败", "no_operate": "无操作"}[log.operate_result],
"terminal_type": {"pc": "电脑", "mobile": "手机", "pad": "平板"}[log.terminal_type],
"interact_time": log.interact_time.strftime("%Y-%m-%d %H:%M:%S")
})
return jsonify({
"success": True,
"data": log_list,
"total": pagination.total,
"page": page,
"page_size": page_size
})
在app.py中新增以下内容
from ai_assistant import ai_bp
注册AI智能助手蓝图
app.register_blueprint(ai_bp, url_prefix="/api")
- 第二步:实现语音交互功能,解放双手语音操作
-- coding: utf-8 --
voice_interact.py 语音交互引擎(识别/合成/指令执行)
import json
import speech_recognition as sr
import pyttsx3
from flask import Blueprint, request, jsonify
from flask_login import login_required, current_user
from audit_engine import audit_log
from ai_assistant import parse_natural_language, execute_board_operate, record_ai_log
from dotenv import load_dotenv
import os
加载环境变量
load_dotenv()
voice_bp = Blueprint("voice", name)
====================== 初始化语音引擎(TTS/识别) ======================
1. 文字转语音(TTS)引擎初始化
tts_engine = pyttsx3.init()
配置TTS参数(语速/音量/语音)
tts_engine.setProperty("rate", int(os.getenv("TTS_RATE", 150))) # 语速,默认150
tts_engine.setProperty("volume", float(os.getenv("TTS_VOLUME", 0.8))) # 音量,0-1
选择语音(0=英文,1=中文,按需调整)
voices = tts_engine.getProperty("voices")
tts_engine.setProperty("voice", voices[1].id if len(voices) > 1 else voices[0].id)
2. 语音识别引擎初始化
r = sr.Recognizer()
语音识别语言(中文)
RECOG_LANG = os.getenv("RECOG_LANG", "zh-CN")
语音指令白名单(高危操作需权限)
VOICE_WHITELIST = json.loads(os.getenv("VOICE_WHITELIST", '["query_data", "no_operate"]'))
====================== 核心功能:语音识别(麦克风/前端音频) ======================
def voice_to_text(audio_data=None):
"""
语音转文字
:param audio_data: 前端传入的音频数据(None则使用麦克风,仅服务端本地可用)
:return: 识别后的文字(str)
"""
try:
if audio_data:
处理前端传入的音频数据(实战中需解析wav/mp3格式)
audio = sr.AudioData(audio_data, 16000, 2)
else:
服务端本地麦克风识别(测试用)
with sr.Microphone() as source:
r.adjust_for_ambient_noise(source) # 消除环境噪音
audio = r.listen(source, timeout=10, phrase_time_limit=20)
# 调用百度/谷歌语音识别(谷歌离线,百度需API,新手先用谷歌)
# 谷歌离线识别
text = r.recognize_google(audio, language=RECOG_LANG)
return text, True
except sr.UnknownValueError:
return "无法识别语音内容,请重新说~", False
except sr.RequestError as e:
return f"语音识别服务异常:{str(e)}", False
except Exception as e:
return f"语音识别失败:{str(e)}", False
====================== 核心功能:文字转语音(本地播报/返回音频) ======================
def text_to_voice(text, save_path=None):
"""
文字转语音
:param text: 需播报的文字
:param save_path: 音频保存路径(None则直接播报)
:return: 操作结果(bool)
"""
try:
tts_engine.say(text)
if save_path:
保存为音频文件(供前端播放)
tts_engine.save_to_file(text, save_path)
tts_engine.runAndWait()
return True
except Exception as e:
print(f"文字转语音失败:{str(e)}")
return False
====================== 核心功能:语音指令权限校验 ======================
def check_voice_perm(operate_intent):
"""
语音指令权限校验:高危操作(如删除/同步)需单独权限,仅白名单可执行
:param operate_intent: 操作意图
:return: 校验结果(bool)
"""
管理员拥有所有语音指令权限
if current_user.has_perm("view_all_data"):
return True
普通用户仅可执行白名单内的操作
return operate_intent in VOICE_WHITELIST
====================== 核心功能:语音交互主逻辑 ======================
def voice_interact_main(audio_data=None):
"""语音交互主逻辑:识别→解析→执行→播报"""
1. 语音转文字
input_text, recog_success = voice_to_text(audio_data)
if not recog_success:
return input_text, "no_operate", "fail"
# 2. 解析自然语言
parse_result = parse_natural_language(input_text)
operate_intent = parse_result.get("operate_intent", "no_operate")
params = parse_result.get("params", {})
# 3. 语音指令权限校验
if not check_voice_perm(operate_intent):
return "你无权限执行该语音指令,请使用文字操作~", operate_intent, "fail"
# 4. 执行看板操作
output_content, operate_result = execute_board_operate(operate_intent, params)
# 5. 文字转语音播报结果(本地)
text_to_voice(output_content)
return output_content, operate_intent, operate_result
====================== 接口:语音识别(前端传入音频,实战核心) ======================
@voice_bp.route("/voice/recognize", methods=["POST"])
@login_required
@audit_log("query")
def voice_recognize():
"""语音识别接口(接收前端音频数据,转文字)"""
if "audio" not in request.files:
return jsonify({"success": False, "error": "未上传音频文件~"})
audio_file = request.files["audio"]
if audio_file.filename == "":
return jsonify({"success": False, "error": "音频文件为空~"})
try:
# 读取音频数据
audio_data = audio_file.read()
# 语音转文字
input_text, recog_success = voice_to_text(audio_data)
if not recog_success:
return jsonify({"success": False, "output": input_text})
# 调用AI助手解析执行
output_content, operate_intent, operate_result = execute_board_operate(operate_intent, parse_natural_language(input_text)["params"])
# 记录交互日志
record_ai_log("voice", input_text, output_content, operate_intent, operate_result)
# 返回结果(供前端TTS播放)
return jsonify({"success": True, "input_text": input_text, "output": output_content})
except Exception as e:
return jsonify({"success": False, "output": f"语音交互失败:{str(e)}"})
====================== 接口:本地语音交互(测试用,服务端麦克风) ======================
@voice_bp.route("/voice/chat/local", methods=["GET"])
@login_required
@audit_log("query")
def voice_chat_local():
"""本地语音交互(测试用,仅服务端本地运行可用)"""
try:
语音交互主逻辑
output_content, operate_intent, operate_result = voice_interact_main()
记录交互日志
record_ai_log("voice", "本地麦克风输入", output_content, operate_intent, operate_result)
return jsonify({"success": True, "output": output_content})
except Exception as e:
return jsonify({"success": False, "output": f"本地语音交互失败:{str(e)}"})
====================== 接口:文字转语音(前端调用,返回音频) ======================
@voice_bp.route("/voice/tts", methods=["POST"])
@login_required
@audit_log("query")
def voice_tts():
"""文字转语音,返回音频文件"""
data = request.get_json()
text = data.get("text", "").strip()
if not text:
return jsonify({"success": False, "error": "请输入需播报的文字~"})
try:
# 保存音频到临时路径
temp_path = f"/tmp/tts_{current_user.id}_{int(time.time())}.wav"
text_to_voice(text, save_path=temp_path)
# 返回音频文件
return send_file(
temp_path,
as_attachment=True,
download_name="tts_audio.wav",
mimetype="audio/wav"
)
except Exception as e:
return jsonify({"success": False, "error": f"文字转语音失败:{str(e)}"})
AI办公助手
×
点击麦克风/输入文字开始交互~
发送
- 第三步:开发企业知识库与自动化问答,实现常见问题智能回复
-- coding: utf-8 --
knowledge_base.py 企业知识库与自动化问答引擎
import json
import pandas as pd
from flask import Blueprint, request, jsonify, send_file
from flask_login import login_required, current_user
from models import db, CompanyKnowledge, QaStatistics
from audit_engine import audit_log
from fuzzywuzzy import fuzz, process
from dotenv import load_dotenv
import os
import redis
加载环境变量
load_dotenv()
kb_bp = Blueprint("knowledge_base", name)
初始化Redis(缓存知识库数据,提升匹配速度)
redis_client = redis.Redis(
host=os.getenv("REDIS_HOST", "localhost"),
port=int(os.getenv("REDIS_PORT", 6379)),
db=int(os.getenv("REDIS_DB", 2)),
decode_responses=True
)
知识库缓存键
KB_CACHE_KEY = "company_knowledge:all"
匹配相似度阈值(低于该值则认为未匹配到)
MATCH_THRESHOLD = int(os.getenv("MATCH_THRESHOLD", 70))
====================== 核心功能:加载知识库到Redis ======================
def load_kb_to_redis():
"""加载知识库数据到Redis,提升匹配速度"""
kbs = CompanyKnowledge.query.all()
kb_data = []
for kb in kbs:
解析角色ID
role_ids = json.loads(kb.role_ids)
kb_data.append({
"id": kb.id,
"question": kb.question,
"answer": kb.answer,
"question_type": kb.question_type,
"role_ids": role_ids,
"is_top": kb.is_top
})
存入Redis(永久缓存,更新知识库时刷新)
redis_client.set(KB_CACHE_KEY, json.dumps(kb_data))
return kb_data
====================== 核心功能:获取可查看的知识库数据 ======================
def get_available_kb():
"""获取当前用户可查看的知识库数据(按角色过滤)"""
从Redis获取知识库数据
kb_str = redis_client.get(KB_CACHE_KEY)
if not kb_str:
kb_data = load_kb_to_redis()
else:
kb_data = json.loads(kb_str)
管理员可查看所有,普通用户仅查看角色ID为空或包含自身角色的
user_role_id = current_user.role.id if current_user.role else 0
if current_user.has_perm("view_all_data"):
return kb_data
else:
return [
kb for kb in kb_data
if len(kb["role_ids"]) == 0 or user_role_id in kb["role_ids"]
]
====================== 核心功能:智能匹配问题(模糊匹配) ======================
def match_question(user_question):
"""
智能匹配知识库问题
:param user_question: 用户提问内容
:return: 匹配结果(dict):id/question/answer/match_score
"""
kb_data = get_available_kb()
if not kb_data:
return {"match_score": 0, "answer": "暂无知识库数据~"}
# 提取问题列表,用于模糊匹配
question_list = [kb["question"] for kb in kb_data]
# 模糊匹配(获取最高相似度的问题)
match_question, match_score = process.extractOne(user_question, question_list, scorer=fuzz.ratio)
# 低于阈值,认为未匹配到
if match_score< MATCH_THRESHOLD:
return {"match_score": match_score, "answer": "未匹配到相关问题,可尝试更换提问方式~"}
# 找到匹配的知识库数据
match_kb = [kb for kb in kb_data if kb["question"] == match_question][0]
# 增加点击次数
kb = CompanyKnowledge.query.get(match_kb["id"])
kb.click_count += 1
db.session.commit()
return {
"id": match_kb["id"],
"question": match_kb["question"],
"answer": match_kb["answer"],
"match_score": match_score
}
====================== 核心功能:记录问答统计 ======================
def record_qa_stat(knowledge_id, user_question, match_score, is_satisfied=None):
"""记录问答统计数据"""
qa_stat = QaStatistics(
knowledge_id=knowledge_id,
user_id=current_user.id,
question=user_question[:500],
match_score=match_score,
is_satisfied=is_satisfied
)
db.session.add(qa_stat)
db.session.commit()
====================== 接口:自动化问答(核心) ======================
@kb_bp.route("/kb/qa", methods=["POST"])
@login_required
@audit_log("query")
def kb_qa():
"""自动化问答接口(文字/语音提问均可调用)"""
data = request.get_json()
user_question = data.get("question", "").strip()
if not user_question:
return jsonify({"success": False, "error": "请输入提问内容~"})
# 智能匹配问题
match_result = match_question(user_question)
# 记录问答统计
record_qa_stat(
knowledge_id=match_result.get("id"),
user_question=user_question,
match_score=match_result.get("match_score", 0)
)
return jsonify({
"success": True,
"match_score": match_result.get("match_score", 0),
"answer": match_result["answer"]
})
====================== 接口:知识库管理(新增/编辑/删除) ======================
@kb_bp.route("/kb/add", methods=["POST"])
@login_required
@permission_required("view_all_data") # 仅管理员可维护
@audit_log("add")
def add_kb():
"""新增知识库内容"""
data = request.get_json()
required_fields = ["question", "answer", "question_type"]
for field in required_fields:
if not data.get(field):
return jsonify({"success": False, "error": f"缺少必填字段:{field}"})
# 检查问题是否已存在
if CompanyKnowledge.query.filter_by(question=data["question"]).first():
return jsonify({"success": False, "error": "该问题已存在于知识库~"})
try:
# 解析角色ID
role_ids = json.dumps(data.get("role_ids", []))
kb = CompanyKnowledge(
question=data["question"],
answer=data["answer"],
question_type=data["question_type"],
role_ids=role_ids,
is_top=data.get("is_top", 0),
create_by=current_user.id
)
db.session.add(kb)
db.session.commit()
# 刷新Redis缓存
load_kb_to_redis()
return jsonify({"success": True, "msg": "知识库内容新增成功~", "kb_id": kb.id})
except Exception as e:
db.session.rollback()
return jsonify({"success": False, "error": f"新增失败:{str(e)}"})
@kb_bp.route("/kb/delete/int:kb_id", methods=["DELETE"])
@login_required
@permission_required("view_all_data")
@audit_log("delete")
def delete_kb(kb_id):
"""删除知识库内容"""
kb = CompanyKnowledge.query.get(kb_id)
if not kb:
return jsonify({"success": False, "error": "知识库内容不存在~"})
try:
db.session.delete(kb)
db.session.commit()
# 刷新Redis缓存
load_kb_to_redis()
return jsonify({"success": True, "msg": "知识库内容删除成功~"})
except Exception as e:
db.session.rollback()
return jsonify({"success": False, "error": f"删除失败:{str(e)}"})
====================== 接口:知识库批量导入(Excel) ======================
@kb_bp.route("/kb/import", methods=["POST"])
@login_required
@permission_required("view_all_data")
@audit_log("add")
def import_kb():
"""批量导入知识库(Excel,列:question/answer/question_type/role_ids/is_top)"""
if "file" not in request.files:
return jsonify({"success": False, "error": "未上传Excel文件~"})
file = request.files["file"]
if not file.filename.endswith((".xlsx", ".xls")):
return jsonify({"success": False, "error": "仅支持Excel文件导入~"})
try:
# 读取Excel文件
df = pd.read_excel(file)
# 检查列是否存在
required_cols = ["question", "answer", "question_type"]
if not all(col in df.columns for col in required_cols):
return jsonify({"success": False, "error": f"Excel文件需包含列:{required_cols.join(',')}~"})
# 批量导入
for _, row in df.iterrows():
if not row["question"] or not row["answer"]:
continue
# 跳过已存在的问题
if CompanyKnowledge.query.filter_by(question=str(row["question"])).first():
continue
role_ids = json.dumps(eval(str(row.get("role_ids", "[]"))))
kb = CompanyKnowledge(
question=str(row["question"]),
answer=str(row["answer"]),
question_type=str(row["question_type"]),
role_ids=role_ids,
is_top=int(row.get("is_top", 0)),
create_by=current_user.id
)
db.session.add(kb)
db.session.commit()
# 刷新Redis缓存
load_kb_to_redis()
return jsonify({"success": True, "msg": f"批量导入成功,共导入{len(df)}条~"})
except Exception as e:
db.session.rollback()
return jsonify({"success": False, "error": f"批量导入失败:{str(e)}"})
====================== 接口:知识库批量导出(Excel) ======================
@kb_bp.route("/kb/export", methods=["GET"])
@login_required
@permission_required("view_all_data")
@audit_log("export")
def export_kb():
"""批量导出知识库为Excel"""
kbs = CompanyKnowledge.query.all()
if not kbs:
return jsonify({"success": False, "error": "暂无知识库数据~"})
# 构建导出数据
export_data = []
for kb in kbs:
export_data.append({
"id": kb.id,
"question": kb.question,
"answer": kb.answer,
"question_type": kb.question_type,
"role_ids": kb.role_ids,
"is_top": kb.is_top,
"click_count": kb.click_count,
"create_time": kb.create_time.strftime("%Y-%m-%d %H:%M:%S")
})
# 生成Excel
df = pd.DataFrame(export_data)
temp_path = f"/tmp/company_knowledge_{int(time.time())}.xlsx"
df.to_excel(temp_path, index=False)
# 返回文件
return send_file(
temp_path,
as_attachment=True,
download_name=f"企业知识库_{pd.Timestamp.now().strftime('%Y%m%d')}.xlsx",
mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
====================== 接口:问答统计查询 ======================
@kb_bp.route("/kb/qa/stat", methods=["GET"])
@login_required
@permission_required("view_all_data")
@audit_log("query")
def get_qa_stat():
"""查询问答统计数据(仅管理员可看)"""
page = int(request.args.get("page", 1))
page_size = int(request.args.get("page_size", 20))
start_time = request.args.get("start_time")
end_time = request.args.get("end_time")
query = QaStatistics.query.join(
CompanyKnowledge, QaStatistics.knowledge_id == CompanyKnowledge.id, isouter=True
).add_columns(
CompanyKnowledge.question, CompanyKnowledge.answer
).order_by(QaStatistics.ask_time.desc())
# 时间筛选
if start_time:
query = query.filter(QaStatistics.ask_time >= start_time)
if end_time:
query = query.filter(QaStatistics.ask_time <= end_time)
pagination = query.paginate(page=page, per_page=page_size)
stats = pagination.items
stat_list = []
for stat, kb_question, kb_answer in stats:
stat_list.append({
"stat_id": stat.id,
"user_id": stat.user_id,
"username": stat.user.username if stat.user else "",
"user_question": stat.question,
"kb_question": kb_question or "未匹配到",
"kb_answer": kb_answer or "",
"match_score": stat.match_score,
"is_satisfied": stat.is_satisfied if stat.is_satisfied is not None else "未评价",
"ask_time": stat.ask_time.strftime("%Y-%m-%d %H:%M:%S")
})
return jsonify({
"success": True,
"data": stat_list,
"total": pagination.total,
"page": page,
"page_size": page_size
})
在app.py中新增以下内容
from knowledge_base import kb_bp
from voice_interact import voice_bp
注册语音交互、知识库蓝图
app.register_blueprint(voice_bp, url_prefix="/api")
app.register_blueprint(kb_bp, url_prefix="/api/kb")
应用启动时加载知识库到Redis
with app.app_context():
from knowledge_base import load_kb_to_redis
load_kb_to_redis()
print("企业知识库已加载到Redis,自动化问答功能已就绪~")
四、功能联调与优化建议
-
功能联调
-
新手优化建议
五、总结