引言
在人工智能迅速发展的今天,聊天机器人已经成为企业与用户交互的重要工具。一个智能聊天机器人不仅能够提高客户服务效率,还能为用户提供24/7的即时响应。本文将详细介绍如何使用Python开发一个智能聊天机器人,从基础概念到实际部署,全面覆盖开发过程中的关键步骤和技术要点。
聊天机器人概述
聊天机器人(Chatbot)是一种能够模拟人类对话的计算机程序。根据实现方式的不同,聊天机器人可以分为以下几类:
- 基于规则的聊天机器人:通过预设的规则和模式匹配来响应用户输入
- 检索式聊天机器人:从已有的回复库中选择最合适的回答
- 生成式聊天机器人:能够生成新的、上下文相关的回复
- 混合型聊天机器人:结合上述多种方法的优点
本项目将重点关注混合型聊天机器人的开发,以获得更好的用户体验和更高的智能水平。
技术栈选择
为了开发一个功能完善的智能聊天机器人,我们需要选择合适的技术栈:
- 编程语言:Python(3.8+)
- 自然语言处理:NLTK, spaCy, Transformers
- 机器学习框架:TensorFlow/PyTorch
- 对话管理:Rasa
- Web框架:Flask/FastAPI
- 数据库:MongoDB/SQLite
- 部署:Docker, AWS/Azure
这些技术的选择基于它们在NLP领域的成熟度、社区支持以及与Python的良好集成性。
开发环境搭建
首先,我们需要搭建一个适合聊天机器人开发的环境:
# 创建虚拟环境
python -m venv chatbot_env
source chatbot_env/bin/activate # Linux/Mac
# 或
chatbot_env\Scripts\activate # Windows
# 安装必要的包
pip install nltk spacy transformers tensorflow flask pymongo python-dotenv
# 下载必要的语言模型
python -m spacy download zh_core_web_sm # 中文模型
python -m nltk.downloader punkt wordnet stopwords
基础架构设计
一个完整的聊天机器人系统通常包含以下组件:
- 用户接口层:负责接收用户输入并展示机器人回复
- 自然语言理解(NLU)模块:解析用户意图和提取关键实体
- 对话管理系统:维护对话状态,决定下一步行动
- 知识库:存储机器人可以访问的信息
- 自然语言生成(NLG)模块:生成自然、流畅的回复
下面是一个简化的架构图:
用户 <-> 用户接口 <-> NLU模块 <-> 对话管理系统 <-> 知识库
|
v
NLG模块
自然语言处理实现
意图识别
意图识别是理解用户想要做什么的关键步骤。我们可以使用基于Transformer的模型来实现:
from transformers import BertTokenizer, BertForSequenceClassification
import torch
# 加载预训练模型和分词器
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
model = BertForSequenceClassification.from_pretrained('bert-base-chinese', num_labels=10) # 假设有10种意图
def predict_intent(text):
inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)
outputs = model(**inputs)
predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
return predictions.argmax().item()
实体提取
实体提取帮助我们识别用户输入中的关键信息:
import spacy
# 加载中文模型
nlp = spacy.load("zh_core_web_sm")
def extract_entities(text):
doc = nlp(text)
entities = {}
for ent in doc.ents:
entities[ent.label_] = ent.text
return entities
对话管理系统
对话管理系统负责维护对话状态并决定下一步行动。我们可以使用状态机或基于规则的系统,也可以使用更复杂的基于机器学习的方法:
class DialogManager:
def __init__(self):
self.state = "greeting"
self.context = {}
def update_state(self, intent, entities):
if self.state == "greeting":
if intent == "query_info":
self.state = "providing_info"
self.context.update(entities)
elif intent == "greeting":
self.state = "greeting"
else:
self.state = "fallback"
elif self.state == "providing_info":
# 处理提供信息状态下的各种意图
pass
# 其他状态处理...
def get_response(self):
if self.state == "greeting":
return "您好!我是智能助手,有什么可以帮您的吗?"
elif self.state == "providing_info":
# 根据context生成相应的信息回复
return f"关于{self.context.get('topic', '您询问的问题')},我可以告诉您..."
elif self.state == "fallback":
return "抱歉,我没有理解您的意思,能否换个方式表达?"
# 其他回复...
知识库构建
知识库是聊天机器人回答专业问题的基础。我们可以使用向量数据库来存储和检索信息:
from sentence_transformers import SentenceTransformer
import faiss
import numpy as np
class KnowledgeBase:
def __init__(self):
self.model = SentenceTransformer('distiluse-base-multilingual-cased')
self.documents = []
self.index = None
def add_documents(self, documents):
self.documents.extend(documents)
embeddings = self.model.encode(documents)
dimension = embeddings.shape[1]
if self.index is None:
self.index = faiss.IndexFlatL2(dimension)
self.index.add(np.array(embeddings).astype('float32'))
def query(self, question, top_k=3):
question_embedding = self.model.encode([question])
distances, indices = self.index.search(np.array(question_embedding).astype('float32'), top_k)
results = []
for idx in indices[0]:
if idx < len(self.documents):
results.append(self.documents[idx])
return results
部署与优化
Web服务部署
使用Flask创建一个简单的API接口:
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/chat', methods=['POST'])
def chat():
user_message = request.json.get('message', '')
# 处理用户消息
intent = predict_intent(user_message)
entities = extract_entities(user_message)
# 更新对话状态
dialog_manager.update_state(intent, entities)
# 获取回复
response = dialog_manager.get_response()
return jsonify({'response': response})
if __name__ == '__main__':
app.run(debug=True)
Docker容器化
创建Dockerfile实现容器化部署:
FROM python:3.8-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
性能评估
评估聊天机器人性能的常用指标包括:
- 准确率:正确回答的比例
- 召回率:能够回答的问题比例
- 平均响应时间:生成回复所需的平均时间
- 用户满意度:通过用户反馈收集
- 对话完成率:成功完成用户意图的比例
可以通过A/B测试和用户调查来收集这些指标。
未来展望
智能聊天机器人技术仍在快速发展,未来可能的发展方向包括:
- 多模态交互:结合语音、图像等多种交互方式
- 情感识别:理解并回应用户的情感状态
- 个性化定制:根据用户偏好调整回复风格
- 持续学习:从对话中不断学习和改进
- 跨语言能力:支持多语言交互和翻译
结论
开发一个智能聊天机器人是一个复杂但有价值的项目。通过合理的架构设计、先进的NLP技术和持续的优化改进,可以构建出一个既实用又智能的对话系统。随着AI技术的不断进步,聊天机器人的能力将会越来越强大,应用场景也会更加广泛。
希望本文能为您的聊天机器人开发提供有益的指导和参考。
源代码
Directory Content Summary
Source Directory: ./chatbot_system
Directory Structure
chatbot_system/
app.py
app_with_kb.py
database.py
dialog_manager.py
knowledge_base.py
knowledge_base_api.py
knowledge_base_db.py
knowledge_base_integration.py
README.md
requirements.txt
test_chatbot.py
config/
dialog_policies.json
response_templates.json
knowledge/
knowledge_base.json
static/
index.html
knowledge_base.html
css/
knowledge_base.css
styles.css
js/
app.js
knowledge_base.js
File Contents
app.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
聊天机器人系统主应用
提供Web API接口,连接前端、对话管理系统和NLP模块
"""
import os
import json
from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
# 导入自定义模块
from dialog_manager import DialogManager
from knowledge_base import KnowledgeBase
from database import ChatbotDatabase
# 创建Flask应用
app = Flask(__name__, static_folder='static')
CORS(app) # 启用跨域请求支持
# 初始化组件
db = ChatbotDatabase()
knowledge_base = KnowledgeBase()
dialog_manager = DialogManager(db, knowledge_base)
# 加载知识库数据
knowledge_base.load_from_directory('knowledge')
# 路由:首页
@app.route('/')
def index():
return send_from_directory('static', 'index.html')
# 路由:聊天API
@app.route('/api/chat', methods=['POST'])
def chat():
data = request.json
user_id = data.get('user_id', 'default_user')
message = data.get('message', '')
if not message:
return jsonify({
'status': 'error',
'message': '消息不能为空'
}), 400
# 处理用户消息并获取回复
response = dialog_manager.process_message(user_id, message)
return jsonify({
'status': 'success',
'response': response
})
# 路由:获取对话历史
@app.route('/api/history/<user_id>', methods=['GET'])
def get_history(user_id):
limit = request.args.get('limit', 20, type=int)
history = db.get_conversation_history(user_id, limit)
return jsonify({
'status': 'success',
'history': history
})
# 路由:清除对话历史
@app.route('/api/history/<user_id>', methods=['DELETE'])
def clear_history(user_id):
db.clear_conversation_history(user_id)
return jsonify({
'status': 'success',
'message': '对话历史已清除'
})
# 路由:添加知识条目
@app.route('/api/knowledge', methods=['POST'])
def add_knowledge():
data = request.json
question = data.get('question', '')
answer = data.get('answer', '')
if not question or not answer:
return jsonify({
'status': 'error',
'message': '问题和答案不能为空'
}), 400
knowledge_base.add_document(question, answer)
return jsonify({
'status': 'success',
'message': '知识条目已添加'
})
# 路由:获取所有知识条目
@app.route('/api/knowledge', methods=['GET'])
def get_knowledge():
documents = knowledge_base.get_all_documents()
return jsonify({
'status': 'success',
'documents': documents
})
# 路由:系统状态
@app.route('/api/status', methods=['GET'])
def get_status():
return jsonify({
'status': 'success',
'system_status': {
'dialog_manager': dialog_manager.get_status(),
'knowledge_base': knowledge_base.get_status(),
'database': db.get_status()
}
})
# 主函数
if __name__ == '__main__':
# 获取端口,默认为5000
port = int(os.environ.get('PORT', 5000))
# 启动应用
app.run(host='0.0.0.0', port=port, debug=True)
app_with_kb.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
聊天机器人系统主应用 - 集成知识库
"""
import os
import json
import logging
from datetime import datetime
from flask import Flask, request, jsonify, send_from_directory
# 导入聊天机器人组件
try:
from nlp_module import NLPModule
nlp_available = True
except ImportError:
nlp_available = False
print("警告: NLP模块导入失败,将使用简化版NLP功能")
from dialog_manager import DialogManager
from database import Database
from knowledge_base_db import KnowledgeBaseDB
from knowledge_base_integration import KnowledgeBaseIntegration
from knowledge_base_api import kb_api
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger("聊天机器人系统")
# 创建Flask应用
app = Flask(__name__)
# 从环境变量获取配置
use_mongodb = os.environ.get("USE_MONGODB", "false").lower() == "true"
mongo_uri = os.environ.get("MONGO_URI")
# 初始化组件
db = Database(use_mongodb=use_mongodb, mongo_uri=mongo_uri)
# 初始化NLP模块
if nlp_available:
nlp = NLPModule()
else:
# 简化版NLP模块,用于测试
class SimpleNLP:
def __init__(self):
self.intents = {
"问候": ["你好", "您好", "早上好", "晚上好", "嗨", "hi", "hello"],
"查询信息": ["什么是", "如何", "怎么", "解释", "介绍", "说明"],
"结束对话": ["再见", "拜拜", "goodbye", "bye", "结束", "退出"]
}
def recognize_intent(self, text):
for intent, patterns in self.intents.items():
for pattern in patterns:
if pattern in text:
return intent, 0.8
return "查询信息", 0.3
def extract_entities(self, text):
entities = []
# 简单的关键词提取
keywords = [word for word in text if len(word) > 1]
if keywords:
entities = [{"type": "keyword", "value": kw} for kw in keywords[:3]]
return entities
def calculate_text_similarity(self, text1, text2):
# 简单的相似度计算
common_words = set(text1) & set(text2)
return len(common_words) / max(len(set(text1)), len(set(text2)), 1)
def analyze_sentiment(self, text):
# 简单的情感分析
positive_words = ["好", "喜欢", "棒", "优秀", "感谢", "谢谢"]
negative_words = ["不", "差", "糟", "失望", "讨厌", "烦"]
pos_score = sum(1 for word in positive_words if word in text)
neg_score = sum(1 for word in negative_words if word in text)
if pos_score > neg_score:
return "positive", (pos_score - neg_score) / (pos_score + neg_score + 1)
elif neg_score > pos_score:
return "negative", (neg_score - pos_score) / (pos_score + neg_score + 1)
else:
return "neutral", 0.5
nlp = SimpleNLP()
# 初始化知识库集成
kb_integration = KnowledgeBaseIntegration(nlp_module=nlp, use_mongodb=use_mongodb, mongo_uri=mongo_uri)
# 初始化对话管理器
dialog_manager = DialogManager(nlp, kb_integration, db)
# 注册知识库API蓝图
app.register_blueprint(kb_api)
# 静态文件路由
@app.route('/')
def index():
return send_from_directory('static', 'index.html')
@app.route('/knowledge')
def knowledge_base():
return send_from_directory('static', 'knowledge_base.html')
# API路由
@app.route('/api/chat', methods=['POST'])
def chat():
"""聊天API"""
try:
data = request.json
if not data:
return jsonify({"success": False, "message": "请求数据为空"}), 400
# 获取用户消息和ID
message = data.get('message', '')
user_id = data.get('user_id', 'default_user')
if not message:
return jsonify({"success": False, "message": "消息不能为空"}), 400
# 获取响应
response = dialog_manager.get_response(message, user_id)
# 获取会话状态
session = dialog_manager.get_session(user_id)
return jsonify({
"success": True,
"response": response,
"session": {
"state": session.get('current_state', 'unknown'),
"turn_count": session.get('turn_count', 0)
}
})
except Exception as e:
logger.error(f"处理聊天请求失败: {str(e)}")
return jsonify({"success": False, "message": f"处理请求失败: {str(e)}"}), 500
@app.route('/api/history', methods=['GET'])
def get_history():
"""获取聊天历史"""
try:
user_id = request.args.get('user_id', 'default_user')
limit = int(request.args.get('limit', 10))
history = db.get_chat_history(user_id, limit=limit)
return jsonify({
"success": True,
"history": history
})
except Exception as e:
logger.error(f"获取聊天历史失败: {str(e)}")
return jsonify({"success": False, "message": f"获取聊天历史失败: {str(e)}"}), 500
@app.route('/api/clear_history', methods=['POST'])
def clear_history():
"""清除聊天历史"""
try:
data = request.json
if not data:
return jsonify({"success": False, "message": "请求数据为空"}), 400
user_id = data.get('user_id', 'default_user')
# 清除聊天历史
db.clear_chat_history(user_id)
# 重置会话状态
dialog_manager.reset_session(user_id)
return jsonify({
"success": True,
"message": "聊天历史已清除"
})
except Exception as e:
logger.error(f"清除聊天历史失败: {str(e)}")
return jsonify({"success": False, "message": f"清除聊天历史失败: {str(e)}"}), 500
@app.route('/api/feedback', methods=['POST'])
def submit_feedback():
"""提交反馈"""
try:
data = request.json
if not data:
return jsonify({"success": False, "message": "请求数据为空"}), 400
user_id = data.get('user_id', 'default_user')
message_id = data.get('message_id')
feedback = data.get('feedback')
if not message_id or feedback is None:
return jsonify({"success": False, "message": "消息ID和反馈内容是必需的"}), 400
# 保存反馈
db.save_feedback(user_id, message_id, feedback)
# 如果是正面反馈,可以考虑将对话添加到知识库
if feedback > 0:
# 获取对话
message = db.get_message(message_id)
if message:
kb_integration.learn_from_conversation(
question=message.get('user_message', ''),
answer=message.get('bot_message', ''),
confidence=0.9 # 用户正面反馈,置信度高
)
return jsonify({
"success": True,
"message": "反馈提交成功"
})
except Exception as e:
logger.error(f"提交反馈失败: {str(e)}")
return jsonify({"success": False, "message": f"提交反馈失败: {str(e)}"}), 500
# 启动应用
if __name__ == '__main__':
# 确保目录存在
os.makedirs('knowledge', exist_ok=True)
os.makedirs('uploads', exist_ok=True)
os.makedirs('exports', exist_ok=True)
# 启动Flask应用
app.run(host='0.0.0.0', port=5000, debug=True)
database.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
数据库模块
负责存储和检索聊天历史、用户信息等数据
"""
import os
import json
import time
from datetime import datetime
from pymongo import MongoClient
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
class ChatbotDatabase:
"""聊天机器人数据库类"""
def __init__(self, use_mongodb=False):
"""
初始化数据库
Args:
use_mongodb: 是否使用MongoDB,如果为False则使用文件存储
"""
self.use_mongodb = use_mongodb
# 如果使用MongoDB,尝试连接
if self.use_mongodb:
try:
# 获取MongoDB连接信息
mongo_uri = os.environ.get('MONGO_URI', 'mongodb://localhost:27017/')
self.client = MongoClient(mongo_uri)
self.db = self.client.chatbot_db
self.conversations = self.db.conversations
self.users = self.db.users
print("已连接到MongoDB")
except Exception as e:
print(f"连接MongoDB失败: {e}")
print("将使用文件存储作为替代")
self.use_mongodb = False
# 如果不使用MongoDB,初始化文件存储
if not self.use_mongodb:
self.data_dir = os.path.join(os.path.dirname(__file__), 'data')
os.makedirs(self.data_dir, exist_ok=True)
# 加载或创建数据文件
self.conversations_file = os.path.join(self.data_dir, 'conversations.json')
self.users_file = os.path.join(self.data_dir, 'users.json')
self.conversations_data = self._load_json_file(self.conversations_file, [])
self.users_data = self._load_json_file(self.users_file, {})
def _load_json_file(self, file_path, default_value):
"""
加载JSON文件,如果不存在则返回默认值
Args:
file_path: 文件路径
default_value: 默认值
Returns:
加载的数据或默认值
"""
if os.path.exists(file_path):
try:
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"加载文件 {file_path} 失败: {e}")
return default_value
def _save_json_file(self, file_path, data):
"""
保存数据到JSON文件
Args:
file_path: 文件路径
data: 要保存的数据
"""
try:
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"保存文件 {file_path} 失败: {e}")
def save_conversation(self, user_id, message, response, intent, state, timestamp=None):
"""
保存对话记录
Args:
user_id: 用户ID
message: 用户消息
response: 机器人回复
intent: 识别的意图
state: 对话状态
timestamp: 时间戳,默认为当前时间
"""
# 创建对话记录
conversation = {
"user_id": user_id,
"message": message,
"response": response,
"intent": intent,
"state": state,
"timestamp": timestamp.isoformat() if timestamp else datetime.now().isoformat()
}
# 保存到数据库
if self.use_mongodb:
self.conversations.insert_one(conversation)
else:
self.conversations_data.append(conversation)
self._save_json_file(self.conversations_file, self.conversations_data)
def get_conversation_history(self, user_id, limit=20):
"""
获取用户对话历史
Args:
user_id: 用户ID
limit: 返回的最大记录数
Returns:
list: 对话历史记录
"""
if self.use_mongodb:
cursor = self.conversations.find(
{"user_id": user_id},
sort=[("timestamp", -1)],
limit=limit
)
return list(cursor)
else:
# 过滤并排序对话记录
user_conversations = [
conv for conv in self.conversations_data
if conv["user_id"] == user_id
]
# 按时间戳排序(降序)
user_conversations.sort(
key=lambda x: x["timestamp"],
reverse=True
)
return user_conversations[:limit]
def clear_conversation_history(self, user_id):
"""
清除用户对话历史
Args:
user_id: 用户ID
Returns:
int: 删除的记录数
"""
if self.use_mongodb:
result = self.conversations.delete_many({"user_id": user_id})
return result.deleted_count
else:
# 过滤出不属于该用户的对话记录
self.conversations_data = [
conv for conv in self.conversations_data
if conv["user_id"] != user_id
]
# 保存更新后的数据
self._save_json_file(self.conversations_file, self.conversations_data)
return len(self.conversations_data)
def save_user_info(self, user_id, info):
"""
保存用户信息
Args:
user_id: 用户ID
info: 用户信息字典
"""
# 添加更新时间
info["updated_at"] = datetime.now().isoformat()
if self.use_mongodb:
self.users.update_one(
{"user_id": user_id},
{"$set": info},
upsert=True
)
else:
# 更新用户信息
if user_id in self.users_data:
self.users_data[user_id].update(info)
else:
self.users_data[user_id] = info
# 保存更新后的数据
self._save_json_file(self.users_file, self.users_data)
def get_user_info(self, user_id):
"""
获取用户信息
Args:
user_id: 用户ID
Returns:
dict: 用户信息,如果不存在则返回空字典
"""
if self.use_mongodb:
user = self.users.find_one({"user_id": user_id})
return user or {}
else:
return self.users_data.get(user_id, {})
def get_all_users(self):
"""
获取所有用户
Returns:
list: 用户列表
"""
if self.use_mongodb:
return list(self.users.find())
else:
return [
{"user_id": user_id, **info}
for user_id, info in self.users_data.items()
]
def get_status(self):
"""
获取数据库状态
Returns:
dict: 状态信息
"""
if self.use_mongodb:
return {
"type": "MongoDB",
"conversation_count": self.conversations.count_documents({}),
"user_count": self.users.count_documents({})
}
else:
return {
"type": "File Storage",
"conversation_count": len(self.conversations_data),
"user_count": len(self.users_data)
}
dialog_manager.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
对话管理系统
负责管理对话状态、决策下一步行动、生成回复
"""
import os
import json
import time
import random
from datetime import datetime
# 尝试导入NLP模块,如果不存在则使用模拟版本
try:
import sys
sys.path.append('..')
from nlp_module import NLPProcessor
except ImportError:
# 创建模拟NLP处理器
class NLPProcessor:
def predict_intent(self, text):
intents = ["问候", "查询信息", "预订服务", "投诉反馈", "寻求帮助", "结束对话", "闲聊", "感谢", "确认", "拒绝"]
intent = random.choice(intents)
return {"intent": intent, "confidence": 0.8}
def extract_entities(self, text):
return {"person": [], "location": [], "time": [], "other": []}
def analyze_sentiment(self, text):
return {"sentiment": "积极", "score": 0.7}
class DialogManager:
"""对话管理系统类"""
def __init__(self, database, knowledge_base):
"""
初始化对话管理系统
Args:
database: 数据库实例
knowledge_base: 知识库实例
"""
self.database = database
self.knowledge_base = knowledge_base
self.nlp = NLPProcessor()
# 用户会话状态
self.sessions = {}
# 加载对话策略
self.load_dialog_policies()
# 加载回复模板
self.load_response_templates()
def load_dialog_policies(self):
"""加载对话策略配置"""
policy_file = os.path.join(os.path.dirname(__file__), 'config', 'dialog_policies.json')
if os.path.exists(policy_file):
with open(policy_file, 'r', encoding='utf-8') as f:
self.policies = json.load(f)
else:
# 默认策略
self.policies = {
"问候": {"next_states": ["提供帮助", "闲聊"], "priority": "提供帮助"},
"查询信息": {"next_states": ["提供信息", "请求更多信息"], "priority": "提供信息"},
"预订服务": {"next_states": ["确认预订", "请求更多信息"], "priority": "请求更多信息"},
"投诉反馈": {"next_states": ["道歉", "解决问题"], "priority": "道歉"},
"寻求帮助": {"next_states": ["提供帮助", "转人工"], "priority": "提供帮助"},
"结束对话": {"next_states": ["结束", "挽留"], "priority": "结束"},
"闲聊": {"next_states": ["闲聊", "引导任务"], "priority": "闲聊"},
"感谢": {"next_states": ["客气回应", "提供进一步帮助"], "priority": "客气回应"},
"确认": {"next_states": ["执行确认操作", "提供进一步帮助"], "priority": "执行确认操作"},
"拒绝": {"next_states": ["接受拒绝", "提供替代方案"], "priority": "提供替代方案"}
}
def load_response_templates(self):
"""加载回复模板"""
template_file = os.path.join(os.path.dirname(__file__), 'config', 'response_templates.json')
if os.path.exists(template_file):
with open(template_file, 'r', encoding='utf-8') as f:
self.templates = json.load(f)
else:
# 默认回复模板
self.templates = {
"问候": [
"您好!我是智能助手,有什么可以帮您的吗?",
"你好!很高兴为您服务,请问有什么需要帮助的吗?",
"您好,我是AI助手,请问需要什么帮助?"
],
"提供帮助": [
"我可以帮您查询信息、预订服务或回答问题,请告诉我您需要什么帮助?",
"有什么我能帮到您的吗?例如查询信息、预订服务等。",
"请问您需要什么帮助?我可以回答问题或提供各种服务。"
],
"提供信息": [
"根据您的查询,我找到了以下信息:{info}",
"以下是您查询的信息:{info}",
"关于您的问题,我可以告诉您:{info}"
],
"请求更多信息": [
"为了更好地帮助您,我需要一些额外信息。请问{question}?",
"能否提供更多细节?比如{question}?",
"请告诉我{question},这样我能更准确地帮助您。"
],
"确认预订": [
"已为您预订成功!预订详情:{details}",
"您的预订已确认。{details}",
"预订成功,详情如下:{details}"
],
"道歉": [
"非常抱歉给您带来不便。{apology}",
"对此我深表歉意。{apology}",
"很遗憾您遇到了问题,{apology}"
],
"解决问题": [
"针对您反馈的问题,我们可以{solution}",
"为解决您的问题,我建议{solution}",
"我们将通过以下方式解决您的问题:{solution}"
],
"转人工": [
"您的问题可能需要人工客服协助,是否需要为您转接?",
"这个问题可能需要专业人员处理,要转接人工客服吗?",
"看起来这个问题比较复杂,需要转接人工客服吗?"
],
"结束": [
"感谢您的使用,再见!",
"期待下次为您服务,再见!",
"再见,有需要随时找我!"
],
"挽留": [
"在您离开前,还有其他可以帮到您的吗?",
"还有什么我可以帮您的吗?",
"您确定现在要结束对话吗?我还可以为您提供更多帮助。"
],
"闲聊": [
"是的,{chat_response}",
"嗯,{chat_response}",
"{chat_response}"
],
"引导任务": [
"我们可以聊聊,不过我最擅长帮您完成具体的任务,比如查询信息或预订服务。",
"很高兴与您聊天。另外,我还可以帮您查询信息或预订服务,需要我帮忙吗?",
"闲聊之余,我还可以帮您完成很多任务,比如查询信息或预订服务,有需要吗?"
],
"客气回应": [
"不客气,这是我应该做的!",
"很高兴能帮到您!",
"您的满意是我最大的动力!"
],
"提供进一步帮助": [
"还有什么我可以帮您的吗?",
"还需要其他帮助吗?",
"我可以为您做些什么?"
],
"执行确认操作": [
"好的,我已经为您{action}",
"已完成,{action}",
"确认完成,{action}"
],
"接受拒绝": [
"好的,我理解。",
"没问题,尊重您的选择。",
"好的,如有需要随时告诉我。"
],
"提供替代方案": [
"没关系,我还可以为您提供以下替代方案:{alternatives}",
"理解您的考虑,以下是其他可能的选择:{alternatives}",
"或许这些替代方案更适合您:{alternatives}"
],
"fallback": [
"抱歉,我没有理解您的意思。能否换个方式表达?",
"对不起,我可能没有理解正确。能请您重新描述一下吗?",
"很抱歉,我没能理解您的意思。请问您能重新表述一下吗?"
]
}
def get_user_session(self, user_id):
"""
获取用户会话状态,如果不存在则创建新会话
Args:
user_id: 用户ID
Returns:
dict: 用户会话状态
"""
if user_id not in self.sessions:
self.sessions[user_id] = {
"state": "初始",
"context": {},
"last_active": time.time(),
"conversation_turns": 0
}
return self.sessions[user_id]
def update_session_state(self, session, intent, entities, sentiment):
"""
根据用户意图和实体更新会话状态
Args:
session: 用户会话状态
intent: 用户意图
entities: 提取的实体
sentiment: 情感分析结果
Returns:
str: 新的状态
"""
current_state = session["state"]
# 更新上下文
session["context"].update({
"last_intent": intent,
"entities": entities,
"sentiment": sentiment
})
# 增加对话轮次
session["conversation_turns"] += 1
# 更新最后活跃时间
session["last_active"] = time.time()
# 根据意图和当前状态决定下一个状态
if intent in self.policies:
policy = self.policies[intent]
next_states = policy["next_states"]
priority = policy["priority"]
# 如果优先状态可用,则使用优先状态
if priority in next_states:
new_state = priority
# 否则随机选择一个可用状态
elif next_states:
new_state = random.choice(next_states)
else:
new_state = current_state
else:
# 默认保持当前状态
new_state = current_state
# 更新会话状态
session["state"] = new_state
return new_state
def generate_response(self, state, context):
"""
根据状态和上下文生成回复
Args:
state: 当前状态
context: 上下文信息
Returns:
str: 生成的回复
"""
# 如果状态有对应的回复模板,则使用模板
if state in self.templates:
templates = self.templates[state]
template = random.choice(templates)
# 填充模板中的占位符
try:
# 根据上下文填充模板
if "{info}" in template and "query_result" in context:
template = template.replace("{info}", context["query_result"])
if "{question}" in template:
questions = [
"您需要的具体信息是什么",
"您想了解哪方面的内容",
"您的具体需求是什么"
]
template = template.replace("{question}", random.choice(questions))
if "{details}" in template and "booking_details" in context:
template = template.replace("{details}", context["booking_details"])
if "{apology}" in template:
apologies = [
"我们会尽快解决这个问题",
"我们会认真处理您的反馈",
"我们将立即着手解决"
]
template = template.replace("{apology}", random.choice(apologies))
if "{solution}" in template:
solutions = [
"为您重新安排服务",
"提供补偿方案",
"优先处理您的问题"
]
template = template.replace("{solution}", random.choice(solutions))
if "{chat_response}" in template:
chat_responses = [
"今天天气不错",
"人工智能正在快速发展",
"很高兴能和您聊天"
]
template = template.replace("{chat_response}", random.choice(chat_responses))
if "{action}" in template and "confirmed_action" in context:
template = template.replace("{action}", context["confirmed_action"])
if "{alternatives}" in template:
alternatives = [
"更经济的方案、更高级的服务",
"其他时间段的预订、其他类型的服务",
"自助服务选项、人工服务选项"
]
template = template.replace("{alternatives}", random.choice(alternatives))
except Exception as e:
print(f"模板填充错误: {e}")
# 发生错误时使用默认回复
template = "我理解您的需求,正在为您处理。"
return template
# 如果没有对应的模板,则使用默认回复
else:
return random.choice(self.templates["fallback"])
def process_message(self, user_id, message):
"""
处理用户消息并生成回复
Args:
user_id: 用户ID
message: 用户消息
Returns:
str: 生成的回复
"""
# 获取用户会话
session = self.get_user_session(user_id)
# NLP处理
intent_result = self.nlp.predict_intent(message)
intent = intent_result["intent"]
entities = self.nlp.extract_entities(message)
sentiment = self.nlp.analyze_sentiment(message)
# 查询知识库
knowledge_result = self.knowledge_base.query(message)
# 更新上下文
session["context"]["query_result"] = knowledge_result[0] if knowledge_result else "暂无相关信息"
# 更新会话状态
new_state = self.update_session_state(session, intent, entities, sentiment)
# 生成回复
response = self.generate_response(new_state, session["context"])
# 保存对话记录
self.database.save_conversation(
user_id=user_id,
message=message,
response=response,
intent=intent,
state=new_state,
timestamp=datetime.now()
)
return response
def get_status(self):
"""
获取对话管理系统状态
Returns:
dict: 状态信息
"""
return {
"active_sessions": len(self.sessions),
"loaded_policies": len(self.policies),
"loaded_templates": len(self.templates)
}
knowledge_base.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
知识库模块
负责存储和检索聊天机器人的知识
"""
import os
import json
import numpy as np
from datetime import datetime
# 尝试导入向量数据库依赖
try:
import faiss
from sentence_transformers import SentenceTransformer
VECTOR_DB_AVAILABLE = True
except ImportError:
VECTOR_DB_AVAILABLE = False
print("警告: faiss或sentence_transformers未安装,将使用简单的文本匹配作为替代")
class KnowledgeBase:
"""知识库类,负责存储和检索知识"""
def __init__(self, model_name="distiluse-base-multilingual-cased"):
"""
初始化知识库
Args:
model_name: 句子编码模型名称
"""
self.documents = []
self.index = None
self.vector_db_enabled = VECTOR_DB_AVAILABLE
# 如果向量数据库可用,初始化编码模型
if self.vector_db_enabled:
try:
self.model = SentenceTransformer(model_name)
print(f"已加载句子编码模型: {model_name}")
except Exception as e:
print(f"加载句子编码模型失败: {e}")
self.vector_db_enabled = False
def add_document(self, question, answer, metadata=None):
"""
添加文档到知识库
Args:
question: 问题文本
answer: 答案文本
metadata: 元数据字典
"""
# 创建文档
document = {
"id": len(self.documents),
"question": question,
"answer": answer,
"metadata": metadata or {},
"created_at": datetime.now().isoformat()
}
# 添加到文档列表
self.documents.append(document)
# 如果向量数据库可用,更新索引
if self.vector_db_enabled:
self._update_index()
return document["id"]
def _update_index(self):
"""更新向量索引"""
if not self.vector_db_enabled:
return
# 提取所有问题文本
questions = [doc["question"] for doc in self.documents]
# 编码问题
embeddings = self.model.encode(questions)
dimension = embeddings.shape[1]
# 创建或更新索引
if self.index is None:
self.index = faiss.IndexFlatL2(dimension)
else:
# 重置索引
self.index = faiss.IndexFlatL2(dimension)
# 添加向量到索引
self.index.add(np.array(embeddings).astype('float32'))
def query(self, query_text, top_k=3):
"""
查询知识库
Args:
query_text: 查询文本
top_k: 返回的最相关文档数量
Returns:
list: 相关答案列表
"""
if not self.documents:
return []
# 如果向量数据库可用,使用语义搜索
if self.vector_db_enabled and self.index is not None:
# 编码查询
query_embedding = self.model.encode([query_text])
# 搜索最相似的向量
distances, indices = self.index.search(
np.array(query_embedding).astype('float32'),
min(top_k, len(self.documents))
)
# 获取对应的答案
results = []
for idx in indices[0]:
if idx < len(self.documents):
results.append(self.documents[idx]["answer"])
return results
# 否则使用简单的文本匹配
else:
# 计算查询与每个问题的相似度(简单实现)
similarities = []
for doc in self.documents:
# 计算问题中与查询相同的词的数量
query_words = set(query_text.lower().split())
question_words = set(doc["question"].lower().split())
common_words = query_words.intersection(question_words)
similarity = len(common_words) / max(len(query_words), len(question_words))
similarities.append((similarity, doc["answer"]))
# 按相似度排序
similarities.sort(reverse=True)
# 返回前top_k个答案
return [item[1] for item in similarities[:top_k]]
def get_document_by_id(self, doc_id):
"""
通过ID获取文档
Args:
doc_id: 文档ID
Returns:
dict: 文档,如果不存在则返回None
"""
for doc in self.documents:
if doc["id"] == doc_id:
return doc
return None
def remove_document(self, doc_id):
"""
删除文档
Args:
doc_id: 文档ID
Returns:
bool: 是否成功删除
"""
for i, doc in enumerate(self.documents):
if doc["id"] == doc_id:
self.documents.pop(i)
# 更新索引
if self.vector_db_enabled:
self._update_index()
return True
return False
def get_all_documents(self):
"""
获取所有文档
Returns:
list: 文档列表
"""
return self.documents
def save_to_file(self, file_path):
"""
保存知识库到文件
Args:
file_path: 文件路径
"""
directory = os.path.dirname(file_path)
if directory and not os.path.exists(directory):
os.makedirs(directory)
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(self.documents, f, ensure_ascii=False, indent=2)
def load_from_file(self, file_path):
"""
从文件加载知识库
Args:
file_path: 文件路径
Returns:
bool: 是否成功加载
"""
if not os.path.exists(file_path):
return False
try:
with open(file_path, 'r', encoding='utf-8') as f:
self.documents = json.load(f)
# 更新索引
if self.vector_db_enabled:
self._update_index()
return True
except Exception as e:
print(f"加载知识库文件失败: {e}")
return False
def load_from_directory(self, directory_path):
"""
从目录加载知识库
Args:
directory_path: 目录路径
Returns:
int: 加载的文档数量
"""
if not os.path.exists(directory_path):
os.makedirs(directory_path)
return 0
count = 0
# 加载主知识库文件
kb_file = os.path.join(directory_path, 'knowledge_base.json')
if os.path.exists(kb_file):
if self.load_from_file(kb_file):
count = len(self.documents)
# 加载其他JSON文件
for filename in os.listdir(directory_path):
if filename.endswith('.json') and filename != 'knowledge_base.json':
file_path = os.path.join(directory_path, filename)
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 处理不同格式的知识文件
if isinstance(data, list):
for item in data:
if isinstance(item, dict) and 'question' in item and 'answer' in item:
self.add_document(item['question'], item['answer'], item.get('metadata'))
count += 1
elif isinstance(data, dict):
for question, answer in data.items():
self.add_document(question, answer)
count += 1
except Exception as e:
print(f"加载知识文件 {filename} 失败: {e}")
return count
def get_status(self):
"""
获取知识库状态
Returns:
dict: 状态信息
"""
return {
"document_count": len(self.documents),
"vector_db_enabled": self.vector_db_enabled,
"index_built": self.index is not None if self.vector_db_enabled else False
}
knowledge_base_api.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
知识库API模块:提供知识库的RESTful API接口
"""
import os
import json
import logging
from typing import List, Dict, Any, Optional
from flask import Blueprint, request, jsonify, send_file
from werkzeug.utils import secure_filename
from knowledge_base_db import KnowledgeBaseDB
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger("知识库API")
# 创建Blueprint
kb_api = Blueprint('knowledge_base_api', __name__)
# 初始化知识库数据库
# 从环境变量获取配置
use_mongodb = os.environ.get("USE_MONGODB", "false").lower() == "true"
mongo_uri = os.environ.get("MONGO_URI")
kb_db = KnowledgeBaseDB(use_mongodb=use_mongodb, mongo_uri=mongo_uri)
# 上传文件配置
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), "uploads")
ALLOWED_EXTENSIONS = {'json', 'txt', 'csv'}
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def allowed_file(filename):
"""检查文件扩展名是否允许"""
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@kb_api.route('/api/knowledge', methods=['GET'])
def get_knowledge():
"""获取知识库条目"""
try:
# 获取查询参数
knowledge_id = request.args.get('id')
if knowledge_id:
# 获取单个知识条目
try:
knowledge_id = int(knowledge_id)
knowledge = kb_db.get_knowledge(knowledge_id)
if knowledge:
return jsonify({"success": True, "data": knowledge})
else:
return jsonify({"success": False, "message": "未找到指定ID的知识"}), 404
except ValueError:
return jsonify({"success": False, "message": "知识ID必须是整数"}), 400
else:
# 搜索知识库
query = request.args.get('query', '')
category = request.args.get('category')
tags = request.args.getlist('tag')
page = int(request.args.get('page', 1))
page_size = int(request.args.get('page_size', 10))
# 计算偏移量
offset = (page - 1) * page_size
# 搜索知识
results = kb_db.search_knowledge(
query=query,
category=category,
tags=tags,
limit=page_size,
offset=offset
)
# 获取总数
total = kb_db.count_knowledge(query=query, category=category, tags=tags)
return jsonify({
"success": True,
"data": results,
"pagination": {
"page": page,
"page_size": page_size,
"total": total,
"total_pages": (total + page_size - 1) // page_size
}
})
except Exception as e:
logger.error(f"获取知识失败: {str(e)}")
return jsonify({"success": False, "message": f"获取知识失败: {str(e)}"}), 500
@kb_api.route('/api/knowledge', methods=['POST'])
def add_knowledge():
"""添加知识到知识库"""
try:
data = request.json
if not data:
return jsonify({"success": False, "message": "请求数据为空"}), 400
# 检查必要字段
if 'question' not in data or 'answer' not in data:
return jsonify({"success": False, "message": "问题和答案字段是必需的"}), 400
# 添加知识
knowledge = kb_db.add_knowledge(
question=data['question'],
answer=data['answer'],
metadata=data.get('metadata', {})
)
return jsonify({"success": True, "data": knowledge, "message": "知识添加成功"})
except Exception as e:
logger.error(f"添加知识失败: {str(e)}")
return jsonify({"success": False, "message": f"添加知识失败: {str(e)}"}), 500
@kb_api.route('/api/knowledge/<int:knowledge_id>', methods=['PUT'])
def update_knowledge(knowledge_id):
"""更新知识库条目"""
try:
data = request.json
if not data:
return jsonify({"success": False, "message": "请求数据为空"}), 400
# 更新知识
updated = kb_db.update_knowledge(
knowledge_id=knowledge_id,
question=data.get('question'),
answer=data.get('answer'),
metadata=data.get('metadata')
)
if updated:
return jsonify({"success": True, "data": updated, "message": "知识更新成功"})
else:
return jsonify({"success": False, "message": "未找到指定ID的知识"}), 404
except Exception as e:
logger.error(f"更新知识失败: {str(e)}")
return jsonify({"success": False, "message": f"更新知识失败: {str(e)}"}), 500
@kb_api.route('/api/knowledge/<int:knowledge_id>', methods=['DELETE'])
def delete_knowledge(knowledge_id):
"""删除知识库条目"""
try:
# 删除知识
success = kb_db.delete_knowledge(knowledge_id)
if success:
return jsonify({"success": True, "message": "知识删除成功"})
else:
return jsonify({"success": False, "message": "未找到指定ID的知识"}), 404
except Exception as e:
logger.error(f"删除知识失败: {str(e)}")
return jsonify({"success": False, "message": f"删除知识失败: {str(e)}"}), 500
@kb_api.route('/api/knowledge/categories', methods=['GET'])
def get_categories():
"""获取所有分类"""
try:
categories = kb_db.get_all_categories()
return jsonify({"success": True, "data": categories})
except Exception as e:
logger.error(f"获取分类失败: {str(e)}")
return jsonify({"success": False, "message": f"获取分类失败: {str(e)}"}), 500
@kb_api.route('/api/knowledge/tags', methods=['GET'])
def get_tags():
"""获取所有标签"""
try:
tags = kb_db.get_all_tags()
return jsonify({"success": True, "data": tags})
except Exception as e:
logger.error(f"获取标签失败: {str(e)}")
return jsonify({"success": False, "message": f"获取标签失败: {str(e)}"}), 500
@kb_api.route('/api/knowledge/import', methods=['POST'])
def import_knowledge():
"""导入知识库"""
try:
# 检查是否有文件上传
if 'file' not in request.files:
return jsonify({"success": False, "message": "未找到上传文件"}), 400
file = request.files['file']
if file.filename == '':
return jsonify({"success": False, "message": "未选择文件"}), 400
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file_path = os.path.join(UPLOAD_FOLDER, filename)
file.save(file_path)
# 读取文件内容
with open(file_path, 'r', encoding='utf-8') as f:
try:
data = json.load(f)
except json.JSONDecodeError:
return jsonify({"success": False, "message": "无效的JSON文件"}), 400
# 导入知识
if isinstance(data, list):
count = kb_db.import_knowledge(data)
return jsonify({"success": True, "message": f"成功导入 {count} 条知识"})
else:
return jsonify({"success": False, "message": "无效的知识库格式,应为JSON数组"}), 400
else:
return jsonify({"success": False, "message": f"不支持的文件类型,仅支持 {', '.join(ALLOWED_EXTENSIONS)}"}), 400
except Exception as e:
logger.error(f"导入知识失败: {str(e)}")
return jsonify({"success": False, "message": f"导入知识失败: {str(e)}"}), 500
@kb_api.route('/api/knowledge/export', methods=['GET'])
def export_knowledge():
"""导出知识库"""
try:
# 获取查询参数
query = request.args.get('query', '')
category = request.args.get('category')
tags = request.args.getlist('tag')
# 导出知识
data = kb_db.export_knowledge(query=query, category=category, tags=tags)
# 生成导出文件
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
export_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "exports")
os.makedirs(export_dir, exist_ok=True)
export_path = os.path.join(export_dir, f"knowledge_export_{timestamp}.json")
with open(export_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
return send_file(export_path, as_attachment=True, download_name=f"knowledge_export_{timestamp}.json")
except Exception as e:
logger.error(f"导出知识失败: {str(e)}")
return jsonify({"success": False, "message": f"导出知识失败: {str(e)}"}), 500
@kb_api.route('/api/knowledge/backup', methods=['POST'])
def backup_knowledge():
"""备份知识库"""
try:
backup_path = kb_db.backup_knowledge()
return jsonify({"success": True, "message": f"知识库备份成功", "backup_path": backup_path})
except Exception as e:
logger.error(f"备份知识库失败: {str(e)}")
return jsonify({"success": False, "message": f"备份知识库失败: {str(e)}"}), 500
@kb_api.route('/api/knowledge/restore', methods=['POST'])
def restore_knowledge():
"""从备份恢复知识库"""
try:
data = request.json
if not data or 'backup_path' not in data:
return jsonify({"success": False, "message": "请提供备份文件路径"}), 400
backup_path = data['backup_path']
clear_existing = data.get('clear_existing', False)
if not os.path.exists(backup_path):
return jsonify({"success": False, "message": "备份文件不存在"}), 404
count = kb_db.restore_knowledge(backup_path, clear_existing)
return jsonify({"success": True, "message": f"成功恢复 {count} 条知识"})
except Exception as e:
logger.error(f"恢复知识库失败: {str(e)}")
return jsonify({"success": False, "message": f"恢复知识库失败: {str(e)}"}), 500
3. Knowledge Base Frontend HTML
knowledge_base_db.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
知识库数据库模块:负责知识库的存储、检索和管理
"""
import os
import json
import time
import uuid
import logging
from typing import List, Dict, Any, Optional, Union
from datetime import datetime
try:
import pymongo
from pymongo import MongoClient
MONGODB_AVAILABLE = True
except ImportError:
MONGODB_AVAILABLE = False
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger("知识库数据库")
class KnowledgeBaseDB:
"""知识库数据库类,支持文件存储和MongoDB存储"""
def __init__(self, use_mongodb: bool = False, mongo_uri: str = None,
db_name: str = "chatbot", collection_name: str = "knowledge_base",
file_path: str = "knowledge/knowledge_base.json"):
"""
初始化知识库数据库
Args:
use_mongodb: 是否使用MongoDB存储
mongo_uri: MongoDB连接URI
db_name: MongoDB数据库名称
collection_name: MongoDB集合名称
file_path: 文件存储路径
"""
self.use_mongodb = use_mongodb and MONGODB_AVAILABLE
self.file_path = file_path
# 确保知识库文件目录存在
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
if self.use_mongodb:
try:
# 使用环境变量或参数中的MongoDB URI
self.mongo_uri = mongo_uri or os.environ.get("MONGO_URI", "mongodb://localhost:27017/")
self.client = MongoClient(self.mongo_uri)
self.db = self.client[db_name]
self.collection = self.db[collection_name]
# 创建索引
self.collection.create_index("id", unique=True)
self.collection.create_index("question")
self.collection.create_index([("metadata.category", pymongo.ASCENDING)])
self.collection.create_index([("metadata.tags", pymongo.ASCENDING)])
logger.info(f"已连接到MongoDB: {self.mongo_uri}")
except Exception as e:
logger.error(f"MongoDB连接失败: {str(e)}")
self.use_mongodb = False
logger.info("将使用文件存储作为备选")
if not self.use_mongodb:
logger.info(f"使用文件存储: {self.file_path}")
# 如果文件不存在,创建空的知识库文件
if not os.path.exists(self.file_path):
with open(self.file_path, 'w', encoding='utf-8') as f:
json.dump([], f, ensure_ascii=False, indent=2)
def _get_next_id(self) -> int:
"""获取下一个可用的ID"""
if self.use_mongodb:
# 查找最大ID
result = self.collection.find_one(sort=[("id", pymongo.DESCENDING)])
return (result["id"] + 1) if result else 0
else:
# 从文件中读取所有记录,找出最大ID
try:
with open(self.file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
return max([item.get("id", -1) for item in data] + [-1]) + 1
except (json.JSONDecodeError, FileNotFoundError):
return 0
def add_knowledge(self, question: str, answer: str,
metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
添加知识到知识库
Args:
question: 问题
answer: 答案
metadata: 元数据,如分类、标签等
Returns:
添加的知识条目
"""
if not question or not answer:
raise ValueError("问题和答案不能为空")
# 准备知识条目
knowledge_id = self._get_next_id()
timestamp = datetime.now().isoformat()
knowledge_item = {
"id": knowledge_id,
"question": question,
"answer": answer,
"metadata": metadata or {},
"created_at": timestamp,
"updated_at": timestamp
}
if self.use_mongodb:
try:
self.collection.insert_one(knowledge_item)
logger.info(f"已添加知识到MongoDB,ID: {knowledge_id}")
except Exception as e:
logger.error(f"添加知识到MongoDB失败: {str(e)}")
raise
else:
try:
# 读取现有数据
with open(self.file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 添加新知识
data.append(knowledge_item)
# 写回文件
with open(self.file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
logger.info(f"已添加知识到文件,ID: {knowledge_id}")
except Exception as e:
logger.error(f"添加知识到文件失败: {str(e)}")
raise
return knowledge_item
def update_knowledge(self, knowledge_id: int,
question: Optional[str] = None,
answer: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
"""
更新知识库中的条目
Args:
knowledge_id: 知识ID
question: 新问题(可选)
answer: 新答案(可选)
metadata: 新元数据(可选)
Returns:
更新后的知识条目,如果未找到则返回None
"""
# 准备更新字段
update_fields = {}
if question is not None:
update_fields["question"] = question
if answer is not None:
update_fields["answer"] = answer
if metadata is not None:
update_fields["metadata"] = metadata
if not update_fields:
logger.warning("没有提供要更新的字段")
return None
# 添加更新时间
update_fields["updated_at"] = datetime.now().isoformat()
if self.use_mongodb:
try:
result = self.collection.find_one_and_update(
{"id": knowledge_id},
{"$set": update_fields},
return_document=pymongo.ReturnDocument.AFTER
)
if result:
logger.info(f"已更新知识,ID: {knowledge_id}")
return result
else:
logger.warning(f"未找到要更新的知识,ID: {knowledge_id}")
return None
except Exception as e:
logger.error(f"更新知识失败: {str(e)}")
raise
else:
try:
# 读取现有数据
with open(self.file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 查找并更新知识
for i, item in enumerate(data):
if item.get("id") == knowledge_id:
data[i].update(update_fields)
updated_item = data[i]
# 写回文件
with open(self.file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
logger.info(f"已更新知识,ID: {knowledge_id}")
return updated_item
logger.warning(f"未找到要更新的知识,ID: {knowledge_id}")
return None
except Exception as e:
logger.error(f"更新知识失败: {str(e)}")
raise
def delete_knowledge(self, knowledge_id: int) -> bool:
"""
删除知识库中的条目
Args:
knowledge_id: 知识ID
Returns:
是否成功删除
"""
if self.use_mongodb:
try:
result = self.collection.delete_one({"id": knowledge_id})
success = result.deleted_count > 0
if success:
logger.info(f"已删除知识,ID: {knowledge_id}")
else:
logger.warning(f"未找到要删除的知识,ID: {knowledge_id}")
return success
except Exception as e:
logger.error(f"删除知识失败: {str(e)}")
raise
else:
try:
# 读取现有数据
with open(self.file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 查找并删除知识
original_length = len(data)
data = [item for item in data if item.get("id") != knowledge_id]
success = len(data) < original_length
if success:
# 写回文件
with open(self.file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
logger.info(f"已删除知识,ID: {knowledge_id}")
else:
logger.warning(f"未找到要删除的知识,ID: {knowledge_id}")
return success
except Exception as e:
logger.error(f"删除知识失败: {str(e)}")
raise
def get_knowledge(self, knowledge_id: int) -> Optional[Dict[str, Any]]:
"""
获取指定ID的知识
Args:
knowledge_id: 知识ID
Returns:
知识条目,如果未找到则返回None
"""
if self.use_mongodb:
try:
result = self.collection.find_one({"id": knowledge_id})
return result
except Exception as e:
logger.error(f"获取知识失败: {str(e)}")
raise
else:
try:
# 读取现有数据
with open(self.file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 查找知识
for item in data:
if item.get("id") == knowledge_id:
return item
return None
except Exception as e:
logger.error(f"获取知识失败: {str(e)}")
raise
def search_knowledge(self, query: str = None, category: str = None,
tags: List[str] = None, limit: int = 10,
offset: int = 0) -> List[Dict[str, Any]]:
"""
搜索知识库
Args:
query: 搜索关键词
category: 分类
tags: 标签列表
limit: 返回结果数量限制
offset: 结果偏移量
Returns:
知识条目列表
"""
if self.use_mongodb:
try:
# 构建查询条件
query_conditions = {}
if query:
query_conditions["$or"] = [
{"question": {"$regex": query, "$options": "i"}},
{"answer": {"$regex": query, "$options": "i"}}
]
if category:
query_conditions["metadata.category"] = category
if tags:
query_conditions["metadata.tags"] = {"$in": tags}
# 执行查询
cursor = self.collection.find(query_conditions)
# 应用分页
cursor = cursor.skip(offset).limit(limit)
# 转换为列表
results = list(cursor)
return results
except Exception as e:
logger.error(f"搜索知识失败: {str(e)}")
raise
else:
try:
# 读取现有数据
with open(self.file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 过滤数据
filtered_data = data
if query:
query = query.lower()
filtered_data = [
item for item in filtered_data
if query in item.get("question", "").lower() or
query in item.get("answer", "").lower()
]
if category:
filtered_data = [
item for item in filtered_data
if item.get("metadata", {}).get("category") == category
]
if tags:
filtered_data = [
item for item in filtered_data
if any(tag in item.get("metadata", {}).get("tags", []) for tag in tags)
]
# 应用分页
paginated_data = filtered_data[offset:offset + limit]
return paginated_data
except Exception as e:
logger.error(f"搜索知识失败: {str(e)}")
raise
def get_all_categories(self) -> List[str]:
"""
获取所有分类
Returns:
分类列表
"""
if self.use_mongodb:
try:
# 聚合查询获取所有不同的分类
pipeline = [
{"$group": {"_id": "$metadata.category"}},
{"$match": {"_id": {"$ne": None}}},
{"$sort": {"_id": 1}}
]
results = self.collection.aggregate(pipeline)
categories = [result["_id"] for result in results]
return categories
except Exception as e:
logger.error(f"获取分类失败: {str(e)}")
raise
else:
try:
# 读取现有数据
with open(self.file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 提取所有分类
categories = set()
for item in data:
category = item.get("metadata", {}).get("category")
if category:
categories.add(category)
return sorted(list(categories))
except Exception as e:
logger.error(f"获取分类失败: {str(e)}")
raise
def get_all_tags(self) -> List[str]:
"""
获取所有标签
Returns:
标签列表
"""
if self.use_mongodb:
try:
# 聚合查询获取所有不同的标签
pipeline = [
{"$unwind": "$metadata.tags"},
{"$group": {"_id": "$metadata.tags"}},
{"$match": {"_id": {"$ne": None}}},
{"$sort": {"_id": 1}}
]
results = self.collection.aggregate(pipeline)
tags = [result["_id"] for result in results]
return tags
except Exception as e:
logger.error(f"获取标签失败: {str(e)}")
raise
else:
try:
# 读取现有数据
with open(self.file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 提取所有标签
tags = set()
for item in data:
item_tags = item.get("metadata", {}).get("tags", [])
if item_tags:
tags.update(item_tags)
return sorted(list(tags))
except Exception as e:
logger.error(f"获取标签失败: {str(e)}")
raise
def count_knowledge(self, query: str = None, category: str = None,
tags: List[str] = None) -> int:
"""
计算知识条目数量
Args:
query: 搜索关键词
category: 分类
tags: 标签列表
Returns:
知识条目数量
"""
if self.use_mongodb:
try:
# 构建查询条件
query_conditions = {}
if query:
query_conditions["$or"] = [
{"question": {"$regex": query, "$options": "i"}},
{"answer": {"$regex": query, "$options": "i"}}
]
if category:
query_conditions["metadata.category"] = category
if tags:
query_conditions["metadata.tags"] = {"$in": tags}
# 执行计数查询
count = self.collection.count_documents(query_conditions)
return count
except Exception as e:
logger.error(f"计数知识失败: {str(e)}")
raise
else:
try:
# 使用搜索函数获取所有匹配的结果(不分页)
results = self.search_knowledge(query, category, tags, limit=float('inf'))
return len(results)
except Exception as e:
logger.error(f"计数知识失败: {str(e)}")
raise
def import_knowledge(self, knowledge_list: List[Dict[str, Any]]) -> int:
"""
批量导入知识
Args:
knowledge_list: 知识条目列表
Returns:
成功导入的条目数量
"""
if not knowledge_list:
return 0
imported_count = 0
if self.use_mongodb:
try:
# 获取当前最大ID
next_id = self._get_next_id()
# 准备导入数据
timestamp = datetime.now().isoformat()
to_import = []
for i, item in enumerate(knowledge_list):
# 确保每个条目都有ID
knowledge_item = item.copy()
knowledge_item["id"] = next_id + i
# 确保有创建和更新时间
if "created_at" not in knowledge_item:
knowledge_item["created_at"] = timestamp
if "updated_at" not in knowledge_item:
knowledge_item["updated_at"] = timestamp
to_import.append(knowledge_item)
# 批量插入
if to_import:
result = self.collection.insert_many(to_import)
imported_count = len(result.inserted_ids)
logger.info(f"已导入 {imported_count} 条知识到MongoDB")
except Exception as e:
logger.error(f"导入知识到MongoDB失败: {str(e)}")
raise
else:
try:
# 读取现有数据
with open(self.file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 获取当前最大ID
next_id = self._get_next_id()
# 准备导入数据
timestamp = datetime.now().isoformat()
for i, item in enumerate(knowledge_list):
# 确保每个条目都有ID
knowledge_item = item.copy()
knowledge_item["id"] = next_id + i
# 确保有创建和更新时间
if "created_at" not in knowledge_item:
knowledge_item["created_at"] = timestamp
if "updated_at" not in knowledge_item:
knowledge_item["updated_at"] = timestamp
data.append(knowledge_item)
imported_count += 1
# 写回文件
with open(self.file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
logger.info(f"已导入 {imported_count} 条知识到文件")
except Exception as e:
logger.error(f"导入知识到文件失败: {str(e)}")
raise
return imported_count
def export_knowledge(self, query: str = None, category: str = None,
tags: List[str] = None) -> List[Dict[str, Any]]:
"""
导出知识
Args:
query: 搜索关键词
category: 分类
tags: 标签列表
Returns:
知识条目列表
"""
# 使用搜索函数获取所有匹配的结果(不分页)
return self.search_knowledge(query, category, tags, limit=float('inf'))
def backup_knowledge(self, backup_path: str = None) -> str:
"""
备份知识库
Args:
backup_path: 备份文件路径,如果为None则自动生成
Returns:
备份文件路径
"""
# 如果未指定备份路径,则自动生成
if backup_path is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_dir = os.path.join(os.path.dirname(self.file_path), "backups")
os.makedirs(backup_dir, exist_ok=True)
backup_path = os.path.join(backup_dir, f"knowledge_backup_{timestamp}.json")
# 导出所有知识
all_knowledge = self.export_knowledge()
# 写入备份文件
with open(backup_path, 'w', encoding='utf-8') as f:
json.dump(all_knowledge, f, ensure_ascii=False, indent=2)
logger.info(f"已备份 {len(all_knowledge)} 条知识到 {backup_path}")
return backup_path
def restore_knowledge(self, backup_path: str, clear_existing: bool = False) -> int:
"""
从备份恢复知识库
Args:
backup_path: 备份文件路径
clear_existing: 是否清除现有数据
Returns:
恢复的条目数量
"""
# 读取备份文件
with open(backup_path, 'r', encoding='utf-8') as f:
backup_data = json.load(f)
# 清除现有数据
if clear_existing:
if self.use_mongodb:
self.collection.delete_many({})
else:
with open(self.file_path, 'w', encoding='utf-8') as f:
json.dump([], f, ensure_ascii=False, indent=2)
# 导入备份数据
imported_count = self.import_knowledge(backup_data)
logger.info(f"已从 {backup_path} 恢复 {imported_count} 条知识")
return imported_count
2. Knowledge Base API Module
knowledge_base_integration.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
知识库集成模块:将知识库与聊天机器人系统集成
"""
import os
import logging
from typing import List, Dict, Any, Optional, Tuple
from knowledge_base_db import KnowledgeBaseDB
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger("知识库集成")
class KnowledgeBaseIntegration:
"""知识库集成类,提供与聊天机器人系统的集成接口"""
def __init__(self, nlp_module=None, use_mongodb: bool = False,
mongo_uri: str = None, similarity_threshold: float = 0.6):
"""
初始化知识库集成
Args:
nlp_module: NLP模块实例,用于计算文本相似度
use_mongodb: 是否使用MongoDB存储
mongo_uri: MongoDB连接URI
similarity_threshold: 相似度阈值,低于此值的匹配将被过滤
"""
self.nlp_module = nlp_module
self.kb_db = KnowledgeBaseDB(use_mongodb=use_mongodb, mongo_uri=mongo_uri)
self.similarity_threshold = similarity_threshold
logger.info("知识库集成模块初始化完成")
def query(self, question: str, max_results: int = 3) -> List[Dict[str, Any]]:
"""
查询知识库
Args:
question: 用户问题
max_results: 最大返回结果数
Returns:
匹配的知识列表,按相似度降序排序
"""
logger.info(f"查询知识库: {question}")
# 从知识库获取所有可能相关的知识
# 这里简单使用关键词匹配,实际应用中可能需要更复杂的检索策略
results = self.kb_db.search_knowledge(query=question, limit=10)
# 如果没有NLP模块,直接返回结果
if not self.nlp_module:
logger.warning("NLP模块未提供,无法计算相似度,直接返回搜索结果")
return results[:max_results]
# 计算问题与知识库中问题的相似度
scored_results = []
for item in results:
similarity = self.nlp_module.calculate_text_similarity(question, item['question'])
item['score'] = similarity
scored_results.append(item)
# 按相似度降序排序
scored_results.sort(key=lambda x: x['score'], reverse=True)
# 过滤低于阈值的结果
filtered_results = [item for item in scored_results if item['score'] >= self.similarity_threshold]
logger.info(f"找到 {len(filtered_results)} 条相关知识")
return filtered_results[:max_results]
def get_answer(self, question: str) -> Tuple[Optional[str], float]:
"""
获取问题的最佳答案
Args:
question: 用户问题
Returns:
(最佳答案, 相似度得分),如果没有找到答案则返回 (None, 0)
"""
results = self.query(question, max_results=1)
if results:
best_match = results[0]
return best_match['answer'], best_match.get('score', 0)
return None, 0
def add_to_knowledge_base(self, question: str, answer: str,
category: str = None, tags: List[str] = None) -> Dict[str, Any]:
"""
添加知识到知识库
Args:
question: 问题
answer: 答案
category: 分类
tags: 标签列表
Returns:
添加的知识条目
"""
metadata = {}
if category:
metadata['category'] = category
if tags:
metadata['tags'] = tags
return self.kb_db.add_knowledge(question=question, answer=answer, metadata=metadata)
def learn_from_conversation(self, question: str, answer: str,
confidence: float = 0.8) -> Optional[Dict[str, Any]]:
"""
从对话中学习,将高质量的问答对添加到知识库
Args:
question: 用户问题
answer: 系统回答
confidence: 系统对回答的置信度
Returns:
添加的知识条目,如果未添加则返回None
"""
# 只有当置信度高于阈值时才添加到知识库
if confidence >= 0.8:
logger.info(f"从对话中学习新知识: {question}")
# 检查是否已存在类似问题
if self.nlp_module:
existing = self.query(question, max_results=1)
if existing and existing[0].get('score', 0) > 0.9:
logger.info(f"知识库中已存在类似问题,不添加")
return None
# 添加到知识库
return self.add_to_knowledge_base(
question=question,
answer=answer,
category="对话学习",
tags=["自动学习"]
)
return None
def get_related_knowledge(self, context: str, max_results: int = 3) -> List[Dict[str, Any]]:
"""
根据上下文获取相关知识
Args:
context: 对话上下文
max_results: 最大返回结果数
Returns:
相关知识列表
"""
# 如果有NLP模块,可以提取上下文中的关键信息
keywords = []
if self.nlp_module:
entities = self.nlp_module.extract_entities(context)
keywords = [entity['value'] for entity in entities if 'value' in entity]
# 如果没有提取到关键词,使用整个上下文
if not keywords:
return self.query(context, max_results=max_results)
# 使用关键词查询
all_results = []
for keyword in keywords:
results = self.kb_db.search_knowledge(query=keyword, limit=5)
all_results.extend(results)
# 去重
unique_results = []
seen_ids = set()
for item in all_results:
if item['id'] not in seen_ids:
seen_ids.add(item['id'])
unique_results.append(item)
# 如果有NLP模块,计算相似度并排序
if self.nlp_module:
for item in unique_results:
# 使用问题和答案计算与上下文的相似度
text = item['question'] + " " + item['answer']
similarity = self.nlp_module.calculate_text_similarity(context, text)
item['score'] = similarity
# 按相似度降序排序
unique_results.sort(key=lambda x: x['score'], reverse=True)
return unique_results[:max_results]
7. Main App Integration
README.md
# 智能聊天机器人系统
一个完整的智能聊天机器人系统,包含自然语言处理、对话管理、知识库和用户界面等组件。
## 功能特点
- **自然语言处理**:意图识别、实体提取、情感分析和文本相似度计算
- **对话管理**:多轮对话处理、上下文管理和状态跟踪
- **知识库**:基于向量的相似度搜索和文本匹配
- **数据库集成**:支持MongoDB或文件存储
- **用户友好界面**:响应式Web界面,支持聊天、知识库管理和设置
## 系统架构
系统由以下主要组件构成:
1. **NLP模块** (`nlp_module.py`):处理自然语言理解任务
2. **对话管理器** (`dialog_manager.py`):管理对话状态和生成响应
3. **知识库** (`knowledge_base.py`):存储和检索知识
4. **数据库** (`database.py`):管理对话历史和用户信息
5. **Web应用** (`app.py`):提供RESTful API接口
6. **前端界面**:HTML/CSS/JavaScript实现的用户界面
## 安装指南
### 环境要求
- Python 3.8+
- MongoDB (可选,也支持文件存储)
### 安装步骤
1. 克隆或下载代码库
2. 安装依赖包
```bash
pip install -r requirements.txt
- 配置环境变量(可选)
# MongoDB连接(可选)
export MONGO_URI=mongodb://localhost:27017/
使用方法
启动服务器
python app.py
服务器默认在 http://localhost:5000 启动
使用Web界面
- 打开浏览器访问 http://localhost:5000
- 在聊天界面输入问题与机器人交互
- 使用知识库管理页面添加或修改知识
- 在设置页面配置用户偏好
API接口
-
聊天接口:
POST /api/chat
{ "message": "你好,请问什么是人工智能?", "user_id": "user123" }
-
知识库查询:
GET /api/knowledge?query=人工智能
-
添加知识:
POST /api/knowledge
{ "question": "什么是机器学习?", "answer": "机器学习是人工智能的一个子领域...", "metadata": { "category": "技术知识", "tags": ["机器学习", "AI"] } }
-
历史记录:
GET /api/history?user_id=user123
配置文件
系统使用以下配置文件:
config/dialog_policies.json
: 对话策略配置config/response_templates.json
: 响应模板配置
扩展开发
添加新的意图
- 在NLP模块中添加新的意图识别模式
- 在对话策略配置中添加新意图的处理流程
- 在响应模板中添加相应的回复模板
自定义知识库
- 在
knowledge
目录下添加或修改知识库文件 - 使用Web界面的知识库管理功能添加新知识
故障排除
- MongoDB连接问题:检查MongoDB服务是否运行,连接字符串是否正确
- NLP模型加载失败:确保已安装所有依赖,并下载了必要的模型文件
- API请求失败:检查请求格式和参数是否正确
许可证
MIT License
### requirements.txt
```text/plain
flask==2.0.1
flask-cors==3.0.10
pymongo==4.0.1
python-dotenv==0.19.1
transformers==4.12.5
torch==1.10.0
jieba==0.42.1
scikit-learn==1.0.1
numpy==1.21.4
requests==2.26.0
faiss-cpu==1.7.1
sentence-transformers==2.1.0
test_chatbot.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
测试脚本:用于测试聊天机器人系统的各个组件
"""
import os
import sys
import json
import time
import argparse
from datetime import datetime
# 添加项目根目录到系统路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 导入聊天机器人组件
from dialog_manager import DialogManager
from knowledge_base import KnowledgeBase
from database import Database
try:
from nlp_module import NLPModule
nlp_available = True
except ImportError:
print("警告: NLP模块导入失败,将使用简化版NLP功能")
nlp_available = False
# 简化版NLP模块,用于测试
class SimpleNLP:
def __init__(self):
self.intents = {
"问候": ["你好", "您好", "早上好", "晚上好", "嗨", "hi", "hello"],
"查询信息": ["什么是", "如何", "怎么", "解释", "介绍", "说明"],
"结束对话": ["再见", "拜拜", "goodbye", "bye", "结束", "退出"]
}
def recognize_intent(self, text):
for intent, patterns in self.intents.items():
for pattern in patterns:
if pattern in text:
return intent, 0.8
return "查询信息", 0.3
def extract_entities(self, text):
entities = []
# 简单的关键词提取
keywords = [word for word in text if len(word) > 1]
if keywords:
entities = [{"type": "keyword", "value": kw} for kw in keywords[:3]]
return entities
def calculate_text_similarity(self, text1, text2):
# 简单的相似度计算
common_words = set(text1) & set(text2)
return len(common_words) / max(len(set(text1)), len(set(text2)), 1)
def analyze_sentiment(self, text):
# 简单的情感分析
positive_words = ["好", "喜欢", "棒", "优秀", "感谢", "谢谢"]
negative_words = ["不", "差", "糟", "失望", "讨厌", "烦"]
pos_score = sum(1 for word in positive_words if word in text)
neg_score = sum(1 for word in negative_words if word in text)
if pos_score > neg_score:
return "positive", (pos_score - neg_score) / (pos_score + neg_score + 1)
elif neg_score > pos_score:
return "negative", (neg_score - pos_score) / (pos_score + neg_score + 1)
else:
return "neutral", 0.5
class ChatbotTester:
"""聊天机器人测试类"""
def __init__(self, use_file_storage=True):
"""初始化测试环境"""
print("初始化聊天机器人测试环境...")
# 加载配置文件
self.load_configs()
# 初始化组件
self.db = Database(use_mongodb=not use_file_storage)
self.kb = KnowledgeBase(self.db)
# 使用完整NLP模块或简化版
if nlp_available:
self.nlp = NLPModule()
else:
self.nlp = SimpleNLP()
self.dialog_manager = DialogManager(self.nlp, self.kb, self.db)
# 测试用户ID
self.test_user_id = f"test_user_{int(time.time())}"
print(f"测试环境初始化完成,测试用户ID: {self.test_user_id}")
def load_configs(self):
"""加载配置文件"""
try:
with open("config/dialog_policies.json", "r", encoding="utf-8") as f:
self.dialog_policies = json.load(f)
with open("config/response_templates.json", "r", encoding="utf-8") as f:
self.response_templates = json.load(f)
print("配置文件加载成功")
except Exception as e:
print(f"加载配置文件失败: {str(e)}")
self.dialog_policies = {}
self.response_templates = {}
def test_nlp_module(self, text):
"""测试NLP模块"""
print("\n===== NLP模块测试 =====")
# 测试意图识别
intent, confidence = self.nlp.recognize_intent(text)
print(f"意图识别结果: {intent} (置信度: {confidence:.2f})")
# 测试实体提取
entities = self.nlp.extract_entities(text)
print(f"实体提取结果: {entities}")
# 测试情感分析
sentiment, score = self.nlp.analyze_sentiment(text)
print(f"情感分析结果: {sentiment} (分数: {score:.2f})")
return intent, entities, sentiment
def test_knowledge_base(self, query):
"""测试知识库"""
print("\n===== 知识库测试 =====")
# 测试知识检索
results = self.kb.search(query)
if results:
print(f"找到 {len(results)} 条相关知识:")
for i, result in enumerate(results[:3], 1): # 只显示前3条
print(f"{i}. 问题: {result['question']}")
print(f" 答案: {result['answer'][:100]}..." if len(result['answer']) > 100 else f" 答案: {result['answer']}")
print(f" 相关度: {result.get('score', 'N/A')}")
print()
else:
print("未找到相关知识")
return results
def test_dialog_manager(self, text):
"""测试对话管理器"""
print("\n===== 对话管理器测试 =====")
# 获取响应
response = self.dialog_manager.get_response(text, self.test_user_id)
print(f"用户输入: {text}")
print(f"系统响应: {response}")
# 获取当前对话状态
session = self.dialog_manager.get_session(self.test_user_id)
print(f"当前对话状态: {session.get('current_state', 'unknown')}")
print(f"对话轮次: {session.get('turn_count', 0)}")
return response, session
def test_database(self):
"""测试数据库"""
print("\n===== 数据库测试 =====")
# 测试保存对话历史
history_id = self.db.save_chat_history(
self.test_user_id,
"测试问题",
"测试回答",
{"intent": "测试意图", "entities": []}
)
print(f"保存对话历史成功,ID: {history_id}")
# 测试获取对话历史
history = self.db.get_chat_history(self.test_user_id, limit=5)
print(f"获取到 {len(history)} 条对话历史")
return history
def run_interactive_test(self):
"""运行交互式测试"""
print("\n===== 开始交互式测试 =====")
print("输入 'exit' 或 'quit' 结束测试")
while True:
user_input = input("\n请输入测试文本: ").strip()
if user_input.lower() in ['exit', 'quit', '退出', '结束']:
break
# 测试NLP模块
intent, entities, sentiment = self.test_nlp_module(user_input)
# 测试知识库
kb_results = self.test_knowledge_base(user_input)
# 测试对话管理器
response, session = self.test_dialog_manager(user_input)
# 记录测试结果
print("\n----- 测试结果摘要 -----")
print(f"识别意图: {intent}")
print(f"情感倾向: {sentiment}")
print(f"知识匹配: {'成功' if kb_results else '未找到'}")
print(f"对话状态: {session.get('current_state', 'unknown')}")
print(f"系统响应: {response}")
def run_benchmark_test(self, test_cases=None):
"""运行基准测试"""
if test_cases is None:
# 默认测试用例
test_cases = [
"你好,请问你是谁?",
"什么是人工智能?",
"如何评估聊天机器人的性能?",
"谢谢你的回答",
"再见"
]
print("\n===== 开始基准测试 =====")
print(f"测试用例数量: {len(test_cases)}")
results = []
start_time = time.time()
for i, test_case in enumerate(test_cases, 1):
print(f"\n测试用例 {i}/{len(test_cases)}: {test_case}")
case_start = time.time()
# 测试对话管理器
response, session = self.test_dialog_manager(test_case)
case_time = time.time() - case_start
results.append({
"input": test_case,
"response": response,
"state": session.get("current_state", "unknown"),
"time": case_time
})
print(f"处理时间: {case_time:.3f}秒")
total_time = time.time() - start_time
avg_time = total_time / len(test_cases)
print("\n===== 基准测试结果 =====")
print(f"总测试用例: {len(test_cases)}")
print(f"总处理时间: {total_time:.3f}秒")
print(f"平均处理时间: {avg_time:.3f}秒/用例")
return results
def main():
"""主函数"""
parser = argparse.ArgumentParser(description="聊天机器人测试工具")
parser.add_argument("--mode", choices=["interactive", "benchmark"], default="interactive",
help="测试模式: interactive(交互式) 或 benchmark(基准测试)")
parser.add_argument("--storage", choices=["file", "mongo"], default="file",
help="存储类型: file(文件) 或 mongo(MongoDB)")
args = parser.parse_args()
# 创建测试实例
tester = ChatbotTester(use_file_storage=(args.storage == "file"))
# 根据模式运行测试
if args.mode == "interactive":
tester.run_interactive_test()
else:
results = tester.run_benchmark_test()
# 保存基准测试结果
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
result_file = f"benchmark_results_{timestamp}.json"
with open(result_file, "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"基准测试结果已保存到: {result_file}")
if __name__ == "__main__":
main()
config\dialog_policies.json
{
"问候": {
"next_states": ["提供帮助", "闲聊"],
"priority": "提供帮助"
},
"查询信息": {
"next_states": ["提供信息", "请求更多信息"],
"priority": "提供信息"
},
"预订服务": {
"next_states": ["确认预订", "请求更多信息"],
"priority": "请求更多信息"
},
"投诉反馈": {
"next_states": ["道歉", "解决问题"],
"priority": "道歉"
},
"寻求帮助": {
"next_states": ["提供帮助", "转人工"],
"priority": "提供帮助"
},
"结束对话": {
"next_states": ["结束", "挽留"],
"priority": "结束"
},
"闲聊": {
"next_states": ["闲聊", "引导任务"],
"priority": "闲聊"
},
"感谢": {
"next_states": ["客气回应", "提供进一步帮助"],
"priority": "客气回应"
},
"确认": {
"next_states": ["执行确认操作", "提供进一步帮助"],
"priority": "执行确认操作"
},
"拒绝": {
"next_states": ["接受拒绝", "提供替代方案"],
"priority": "提供替代方案"
}
}
config\response_templates.json
{
"问候": [
"您好!我是智能助手,有什么可以帮您的吗?",
"你好!很高兴为您服务,请问有什么需要帮助的吗?",
"您好,我是AI助手,请问需要什么帮助?"
],
"提供帮助": [
"我可以帮您查询信息、预订服务或回答问题,请告诉我您需要什么帮助?",
"有什么我能帮到您的吗?例如查询信息、预订服务等。",
"请问您需要什么帮助?我可以回答问题或提供各种服务。"
],
"提供信息": [
"根据您的查询,我找到了以下信息:{info}",
"以下是您查询的信息:{info}",
"关于您的问题,我可以告诉您:{info}"
],
"请求更多信息": [
"为了更好地帮助您,我需要一些额外信息。请问{question}?",
"能否提供更多细节?比如{question}?",
"请告诉我{question},这样我能更准确地帮助您。"
],
"确认预订": [
"已为您预订成功!预订详情:{details}",
"您的预订已确认。{details}",
"预订成功,详情如下:{details}"
],
"道歉": [
"非常抱歉给您带来不便。{apology}",
"对此我深表歉意。{apology}",
"很遗憾您遇到了问题,{apology}"
],
"解决问题": [
"针对您反馈的问题,我们可以{solution}",
"为解决您的问题,我建议{solution}",
"我们将通过以下方式解决您的问题:{solution}"
],
"转人工": [
"您的问题可能需要人工客服协助,是否需要为您转接?",
"这个问题可能需要专业人员处理,要转接人工客服吗?",
"看起来这个问题比较复杂,需要转接人工客服吗?"
],
"结束": [
"感谢您的使用,再见!",
"期待下次为您服务,再见!",
"再见,有需要随时找我!"
],
"挽留": [
"在您离开前,还有其他可以帮到您的吗?",
"还有什么我可以帮您的吗?",
"您确定现在要结束对话吗?我还可以为您提供更多帮助。"
],
"闲聊": [
"是的,{chat_response}",
"嗯,{chat_response}",
"{chat_response}"
],
"引导任务": [
"我们可以聊聊,不过我最擅长帮您完成具体的任务,比如查询信息或预订服务。",
"很高兴与您聊天。另外,我还可以帮您查询信息或预订服务,需要我帮忙吗?",
"闲聊之余,我还可以帮您完成很多任务,比如查询信息或预订服务,有需要吗?"
],
"客气回应": [
"不客气,这是我应该做的!",
"很高兴能帮到您!",
"您的满意是我最大的动力!"
],
"提供进一步帮助": [
"还有什么我可以帮您的吗?",
"还需要其他帮助吗?",
"我可以为您做些什么?"
],
"执行确认操作": [
"好的,我已经为您{action}",
"已完成,{action}",
"确认完成,{action}"
],
"接受拒绝": [
"好的,我理解。",
"没问题,尊重您的选择。",
"好的,如有需要随时告诉我。"
],
"提供替代方案": [
"没关系,我还可以为您提供以下替代方案:{alternatives}",
"理解您的考虑,以下是其他可能的选择:{alternatives}",
"或许这些替代方案更适合您:{alternatives}"
],
"fallback": [
"抱歉,我没有理解您的意思。能否换个方式表达?",
"对不起,我可能没有理解正确。能请您重新描述一下吗?",
"很抱歉,我没能理解您的意思。请问您能重新表述一下吗?"
]
}
knowledge\knowledge_base.json
[
{
"id": 0,
"question": "什么是人工智能?",
"answer": "人工智能(Artificial Intelligence,简称AI)是计算机科学的一个分支,致力于创造能够模拟人类智能行为的机器和系统。它包括机器学习、自然语言处理、计算机视觉等多个领域,目标是使计算机能够理解、学习、推理和解决问题。",
"metadata": {
"category": "基础知识",
"tags": ["AI", "人工智能", "技术"]
},
"created_at": "2025-03-06T15:00:00.000Z"
},
{
"id": 1,
"question": "什么是聊天机器人?",
"answer": "聊天机器人是一种能够模拟人类对话的计算机程序,它通过文本或语音与用户进行交互。聊天机器人可以基于规则、检索或生成式方法工作,常用于客户服务、信息查询和个人助手等场景。现代聊天机器人通常结合了自然语言处理和机器学习技术,以提供更自然、更智能的对话体验。",
"metadata": {
"category": "基础知识",
"tags": ["聊天机器人", "对话系统", "人机交互"]
},
"created_at": "2025-03-06T15:05:00.000Z"
},
{
"id": 2,
"question": "自然语言处理是什么?",
"answer": "自然语言处理(Natural Language Processing,简称NLP)是人工智能的一个分支,专注于计算机与人类语言之间的交互。它使计算机能够理解、解释和生成人类语言,涉及文本分类、情感分析、命名实体识别、机器翻译等任务。NLP技术广泛应用于搜索引擎、聊天机器人、语音助手和文本分析等领域。",
"metadata": {
"category": "技术知识",
"tags": ["NLP", "自然语言处理", "文本分析"]
},
"created_at": "2025-03-06T15:10:00.000Z"
},
{
"id": 3,
"question": "BERT模型是什么?",
"answer": "BERT(Bidirectional Encoder Representations from Transformers)是由Google开发的预训练语言模型,于2018年发布。它的主要创新在于双向训练机制,能够同时考虑文本的左右上下文,从而更好地理解语言的语义。BERT在多种NLP任务上取得了突破性的成果,包括问答、情感分析和命名实体识别等,成为了现代NLP技术的重要基础。",
"metadata": {
"category": "技术知识",
"tags": ["BERT", "预训练模型", "Transformer"]
},
"created_at": "2025-03-06T15:15:00.000Z"
},
{
"id": 4,
"question": "如何评估聊天机器人的性能?",
"answer": "评估聊天机器人性能通常从多个维度进行:1)准确性:机器人正确理解和回答问题的能力;2)相关性:回复与用户查询的相关程度;3)连贯性:保持对话流畅和上下文一致的能力;4)多样性:生成多样化而非重复回复的能力;5)用户满意度:通过用户反馈收集的主观评价;6)任务完成率:成功帮助用户完成特定任务的比例。常用的评估方法包括人工评估、自动化指标(如BLEU、ROUGE)和A/B测试等。",
"metadata": {
"category": "评估方法",
"tags": ["性能评估", "指标", "用户体验"]
},
"created_at": "2025-03-06T15:20:00.000Z"
},
{
"id": 5,
"question": "什么是意图识别?",
"answer": "意图识别是自然语言处理中的一项关键任务,旨在确定用户在对话或查询中的目的或意图。例如,当用户说"今天天气怎么样"时,系统会识别出用户的意图是"查询天气"。意图识别通常使用分类算法实现,如支持向量机、神经网络或预训练语言模型。在聊天机器人和对话系统中,准确的意图识别是理解用户需求和提供相关回复的基础。",
"metadata": {
"category": "技术知识",
"tags": ["意图识别", "NLU", "对话系统"]
},
"created_at": "2025-03-06T15:25:00.000Z"
},
{
"id": 6,
"question": "实体提取是什么?",
"answer": "实体提取(Entity Extraction)是从非结构化文本中识别和提取特定类型信息的过程,如人名、地点、组织、日期、时间等。这是自然语言处理中的一项基础任务,也称为命名实体识别(Named Entity Recognition,NER)。在聊天机器人中,实体提取帮助系统理解用户输入中的关键信息,例如从"我想预订明天下午2点从北京到上海的机票"中提取出时间(明天下午2点)、出发地(北京)和目的地(上海)等实体。",
"metadata": {
"category": "技术知识",
"tags": ["实体提取", "NER", "信息提取"]
},
"created_at": "2025-03-06T15:30:00.000Z"
},
{
"id": 7,
"question": "什么是对话状态跟踪?",
"answer": "对话状态跟踪(Dialogue State Tracking,DST)是对话系统中的一个关键组件,负责维护和更新对话的当前状态。它记录用户在对话过程中表达的意图、提供的信息和系统的响应,以便系统能够基于完整的上下文做出决策。例如,在一个订票对话中,DST会跟踪用户已提供的信息(如出发地、目的地、日期)和尚未提供的信息,帮助系统决定下一步应该询问什么或执行什么操作。",
"metadata": {
"category": "技术知识",
"tags": ["对话状态", "上下文管理", "对话系统"]
},
"created_at": "2025-03-06T15:35:00.000Z"
},
{
"id": 8,
"question": "如何处理聊天机器人中的多轮对话?",
"answer": "处理多轮对话需要几个关键技术:1)上下文管理:保存和更新对话历史,使系统能够理解当前输入与之前交流的关系;2)指代消解:解决代词(如"它"、"这个")指向的实体;3)对话状态跟踪:维护用户意图和已提供信息的状态;4)对话策略:基于当前状态决定下一步行动;5)回复生成:生成考虑上下文的自然回复。现代方法通常使用基于Transformer的模型(如BERT、GPT)来处理整个对话历史,或使用专门的对话管理框架(如Rasa)来协调这些组件。",
"metadata": {
"category": "技术知识",
"tags": ["多轮对话", "上下文理解", "对话管理"]
},
"created_at": "2025-03-06T15:40:00.000Z"
},
{
"id": 9,
"question": "知识图谱如何应用于聊天机器人?",
"answer": "知识图谱在聊天机器人中的应用主要体现在:1)增强问答能力:通过结构化知识提供准确、详细的回答;2)实体链接:将用户提及的实体映射到知识图谱中的节点;3)关系推理:基于实体间的关系推断新信息;4)对话引导:根据知识图谱中的关联信息主动引导对话方向;5)个性化交互:利用用户相关的知识节点提供定制化服务。例如,一个旅游聊天机器人可以使用包含景点、酒店、交通等信息的知识图谱,帮助用户规划行程并回答相关问题。",
"metadata": {
"category": "技术应用",
"tags": ["知识图谱", "语义网络", "结构化知识"]
},
"created_at": "2025-03-06T15:45:00.000Z"
}
]
static\index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能聊天机器人</title>
<link rel="stylesheet" href="css/styles.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css">
</head>
<body>
<div class="container-fluid">
<div class="row vh-100">
<!-- 侧边栏 -->
<div class="col-md-3 col-lg-2 sidebar">
<div class="sidebar-header">
<h3>智能聊天助手</h3>
</div>
<div class="sidebar-menu">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link active" href="#" id="new-chat">
<i class="bi bi-plus-circle"></i> 新对话
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" id="history">
<i class="bi bi-clock-history"></i> 历史记录
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" id="knowledge-base">
<i class="bi bi-book"></i> 知识库管理
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" id="settings">
<i class="bi bi-gear"></i> 设置
</a>
</li>
</ul>
</div>
<div class="sidebar-footer">
<p>版本: 1.0.0</p>
<p>© 2025 智能聊天机器人</p>
</div>
</div>
<!-- 主内容区 -->
<div class="col-md-9 col-lg-10 main-content">
<!-- 聊天界面 -->
<div id="chat-container" class="chat-container">
<div class="chat-header">
<h4>与AI助手的对话</h4>
<div class="chat-actions">
<button class="btn btn-sm btn-outline-secondary" id="clear-chat">
<i class="bi bi-trash"></i> 清空对话
</button>
</div>
</div>
<div class="chat-messages" id="chat-messages">
<!-- 欢迎消息 -->
<div class="message bot-message">
<div class="message-avatar">
<i class="bi bi-robot"></i>
</div>
<div class="message-content">
<p>您好!我是智能助手,有什么可以帮您的吗?</p>
</div>
</div>
</div>
<div class="chat-input">
<form id="chat-form">
<div class="input-group">
<input type="text" id="user-input" class="form-control" placeholder="输入您的问题..." autocomplete="off">
<button type="submit" class="btn btn-primary">
<i class="bi bi-send"></i>
</button>
</div>
</form>
</div>
</div>
<!-- 知识库管理界面 -->
<div id="knowledge-container" class="knowledge-container d-none">
<div class="knowledge-header">
<h4>知识库管理</h4>
<div class="knowledge-actions">
<button class="btn btn-sm btn-primary" id="add-knowledge">
<i class="bi bi-plus"></i> 添加知识
</button>
</div>
</div>
<div class="knowledge-content">
<div class="card">
<div class="card-header">
知识条目列表
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>ID</th>
<th>问题</th>
<th>答案</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="knowledge-list">
<!-- 知识条目将通过JavaScript动态添加 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- 设置界面 -->
<div id="settings-container" class="settings-container d-none">
<div class="settings-header">
<h4>设置</h4>
</div>
<div class="settings-content">
<div class="card">
<div class="card-header">
系统设置
</div>
<div class="card-body">
<form id="settings-form">
<div class="mb-3">
<label for="user-id" class="form-label">用户ID</label>
<input type="text" class="form-control" id="user-id" value="default_user">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="use-mongodb">
<label class="form-check-label" for="use-mongodb">使用MongoDB数据库</label>
</div>
<div class="mb-3">
<label for="mongodb-uri" class="form-label">MongoDB URI</label>
<input type="text" class="form-control" id="mongodb-uri" placeholder="mongodb://localhost:27017/">
</div>
<button type="submit" class="btn btn-primary">保存设置</button>
</form>
</div>
</div>
</div>
</div>
<!-- 历史记录界面 -->
<div id="history-container" class="history-container d-none">
<div class="history-header">
<h4>对话历史</h4>
</div>
<div class="history-content">
<div class="card">
<div class="card-header">
历史对话列表
</div>
<div class="card-body">
<div class="list-group" id="history-list">
<!-- 历史记录将通过JavaScript动态添加 -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 模态框:添加知识 -->
<div class="modal fade" id="add-knowledge-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">添加知识条目</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="add-knowledge-form">
<div class="mb-3">
<label for="knowledge-question" class="form-label">问题</label>
<input type="text" class="form-control" id="knowledge-question" required>
</div>
<div class="mb-3">
<label for="knowledge-answer" class="form-label">答案</label>
<textarea class="form-control" id="knowledge-answer" rows="3" required></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="save-knowledge">保存</button>
</div>
</div>
</div>
</div>
<!-- 加载脚本 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/app.js"></script>
</body>
</html>
static\knowledge_base.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>知识库管理系统</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="css/knowledge_base.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="#">
<i class="bi bi-database-fill"></i> 知识库管理系统
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link active" href="knowledge_base.html">知识库</a>
</li>
<li class="nav-item">
<a class="nav-link" href="index.html">聊天机器人</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container-fluid mt-3">
<div class="row">
<!-- 左侧边栏 -->
<div class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
<div class="position-sticky pt-3">
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>知识库管理</span>
</h6>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link active" href="#" id="nav-knowledge-list">
<i class="bi bi-list-ul"></i> 知识列表
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" id="nav-add-knowledge">
<i class="bi bi-plus-circle"></i> 添加知识
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" id="nav-categories">
<i class="bi bi-folder"></i> 分类管理
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" id="nav-tags">
<i class="bi bi-tags"></i> 标签管理
</a>
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>数据操作</span>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item">
<a class="nav-link" href="#" id="nav-import">
<i class="bi bi-upload"></i> 导入知识
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" id="nav-export">
<i class="bi bi-download"></i> 导出知识
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" id="nav-backup">
<i class="bi bi-save"></i> 备份/恢复
</a>
</li>
</ul>
</div>
</div>
<!-- 主内容区 -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<!-- 知识列表页 -->
<div class="page" id="page-knowledge-list">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">知识列表</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<button type="button" class="btn btn-sm btn-primary" id="btn-add-knowledge">
<i class="bi bi-plus"></i> 添加知识
</button>
</div>
</div>
<!-- 搜索栏 -->
<div class="row mb-3">
<div class="col-md-6">
<div class="input-group">
<input type="text" class="form-control" id="search-input" placeholder="搜索知识...">
<button class="btn btn-outline-secondary" type="button" id="btn-search">
<i class="bi bi-search"></i>
</button>
</div>
</div>
<div class="col-md-3">
<select class="form-select" id="category-filter">
<option value="">所有分类</option>
</select>
</div>
<div class="col-md-3">
<select class="form-select" id="tag-filter">
<option value="">所有标签</option>
</select>
</div>
</div>
<!-- 知识列表表格 -->
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">问题</th>
<th scope="col">分类</th>
<th scope="col">标签</th>
<th scope="col">更新时间</th>
<th scope="col">操作</th>
</tr>
</thead>
<tbody id="knowledge-list">
<!-- 知识列表内容将通过JavaScript动态生成 -->
</tbody>
</table>
</div>
<!-- 分页控件 -->
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center" id="pagination">
<!-- 分页将通过JavaScript动态生成 -->
</ul>
</nav>
</div>
<!-- 添加/编辑知识页 -->
<div class="page d-none" id="page-add-knowledge">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2" id="form-title">添加知识</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<button type="button" class="btn btn-sm btn-outline-secondary" id="btn-back-to-list">
<i class="bi bi-arrow-left"></i> 返回列表
</button>
</div>
</div>
<form id="knowledge-form">
<input type="hidden" id="knowledge-id">
<div class="mb-3">
<label for="question" class="form-label">问题</label>
<input type="text" class="form-control" id="question" required>
</div>
<div class="mb-3">
<label for="answer" class="form-label">答案</label>
<textarea class="form-control" id="answer" rows="6" required></textarea>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="category" class="form-label">分类</label>
<div class="input-group">
<input type="text" class="form-control" id="category" list="category-list">
<datalist id="category-list">
<!-- 分类列表将通过JavaScript动态生成 -->
</datalist>
</div>
</div>
<div class="col-md-6">
<label for="tags" class="form-label">标签 (用逗号分隔)</label>
<div class="input-group">
<input type="text" class="form-control" id="tags" list="tag-list">
<datalist id="tag-list">
<!-- 标签列表将通过JavaScript动态生成 -->
</datalist>
</div>
</div>
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary" id="btn-save">保存</button>
<button type="button" class="btn btn-outline-secondary" id="btn-cancel">取消</button>
</div>
</form>
</div>
<!-- 分类管理页 -->
<div class="page d-none" id="page-categories">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">分类管理</h1>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">分类列表</div>
<div class="card-body">
<ul class="list-group" id="category-list-items">
<!-- 分类列表将通过JavaScript动态生成 -->
</ul>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">分类统计</div>
<div class="card-body">
<canvas id="category-chart"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- 标签管理页 -->
<div class="page d-none" id="page-tags">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">标签管理</h1>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">标签列表</div>
<div class="card-body">
<div id="tag-cloud">
<!-- 标签云将通过JavaScript动态生成 -->
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">标签统计</div>
<div class="card-body">
<canvas id="tag-chart"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- 导入知识页 -->
<div class="page d-none" id="page-import">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">导入知识</h1>
</div>
<div class="card">
<div class="card-body">
<form id="import-form">
<div class="mb-3">
<label for="import-file" class="form-label">选择导入文件</label>
<input class="form-control" type="file" id="import-file" accept=".json,.txt,.csv">
<div class="form-text">支持的文件格式: JSON, TXT, CSV</div>
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary" id="btn-import">导入</button>
</div>
</form>
<div class="alert alert-info" role="alert">
<h4 class="alert-heading">导入说明</h4>
<p>JSON格式要求:数组格式,每个元素包含question、answer和metadata字段。</p>
<p>示例:</p>
<pre><code>[
{
"question": "什么是人工智能?",
"answer": "人工智能是...",
"metadata": {
"category": "技术",
"tags": ["AI", "技术"]
}
}
]</code></pre>
</div>
</div>
</div>
</div>
<!-- 导出知识页 -->
<div class="page d-none" id="page-export">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">导出知识</h1>
</div>
<div class="card">
<div class="card-body">
<form id="export-form">
<div class="mb-3">
<label class="form-label">导出范围</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="export-scope" id="export-all" value="all" checked>
<label class="form-check-label" for="export-all">
导出全部知识
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="export-scope" id="export-filtered" value="filtered">
<label class="form-check-label" for="export-filtered">
导出筛选后的知识
</label>
</div>
</div>
<div class="mb-3" id="export-filter-options" style="display: none;">
<div class="row">
<div class="col-md-4">
<input type="text" class="form-control" id="export-query" placeholder="搜索关键词">
</div>
<div class="col-md-4">
<select class="form-select" id="export-category">
<option value="">所有分类</option>
</select>
</div>
<div class="col-md-4">
<select class="form-select" id="export-tag">
<option value="">所有标签</option>
</select>
</div>
</div>
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary" id="btn-export">导出</button>
</div>
</form>
</div>
</div>
</div>
<!-- 备份/恢复页 -->
<div class="page d-none" id="page-backup">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">备份/恢复</h1>
</div>
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">创建备份</div>
<div class="card-body">
<p>创建知识库的完整备份,以便将来恢复。</p>
<button type="button" class="btn btn-primary" id="btn-create-backup">创建备份</button>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">恢复备份</div>
<div class="card-body">
<form id="restore-form">
<div class="mb-3">
<label for="backup-file" class="form-label">选择备份文件</label>
<input class="form-control" type="file" id="backup-file" accept=".json">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="clear-existing">
<label class="form-check-label" for="clear-existing">清除现有数据</label>
</div>
<div class="mb-3">
<button type="submit" class="btn btn-warning" id="btn-restore">恢复备份</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">备份历史</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>备份文件</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="backup-list">
<!-- 备份列表将通过JavaScript动态生成 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- 查看知识详情模态框 -->
<div class="modal fade" id="view-knowledge-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="view-knowledge-title">知识详情</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<h5>问题</h5>
<p id="view-question"></p>
</div>
<div class="mb-3">
<h5>答案</h5>
<div id="view-answer"></div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<h5>分类</h5>
<p id="view-category"></p>
</div>
<div class="col-md-6">
<h5>标签</h5>
<div id="view-tags"></div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<h5>创建时间</h5>
<p id="view-created-at"></p>
</div>
<div class="col-md-6">
<h5>更新时间</h5>
<p id="view-updated-at"></p>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" id="btn-edit-knowledge">编辑</button>
</div>
</div>
</div>
</div>
<!-- 确认删除模态框 -->
<div class="modal fade" id="confirm-delete-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">确认删除</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>确定要删除这条知识吗?此操作不可撤销。</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" id="btn-confirm-delete">删除</button>
</div>
</div>
</div>
</div>
<!-- 提示模态框 -->
<div class="modal fade" id="alert-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="alert-title">提示</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p id="alert-message"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">确定</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
<script src="js/knowledge_base.js"></script>
</body>
</html>
4. Knowledge Base CSS
static\css\knowledge_base.css
/* 知识库管理系统样式 */
/* 侧边栏样式 */
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100;
padding: 48px 0 0;
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
}
@media (max-width: 767.98px) {
.sidebar {
position: static;
padding-top: 1.5rem;
}
}
.sidebar-sticky {
position: relative;
top: 0;
height: calc(100vh - 48px);
padding-top: .5rem;
overflow-x: hidden;
overflow-y: auto;
}
.sidebar .nav-link {
font-weight: 500;
color: #333;
}
.sidebar .nav-link.active {
color: #2470dc;
}
.sidebar .nav-link:hover {
color: #0d6efd;
}
.sidebar-heading {
font-size: .75rem;
text-transform: uppercase;
}
/* 主内容区样式 */
main {
padding-top: 1.5rem;
}
@media (min-width: 768px) {
main {
padding-left: 2rem;
padding-right: 2rem;
}
}
/* 表格样式 */
.table th {
background-color: #f8f9fa;
}
.table td {
vertical-align: middle;
}
.table .question-cell {
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 标签样式 */
.badge-tag {
background-color: #e9ecef;
color: #495057;
margin-right: 5px;
margin-bottom: 5px;
display: inline-block;
}
/* 标签云样式 */
#tag-cloud {
text-align: center;
padding: 15px;
}
.tag-item {
display: inline-block;
margin: 5px;
padding: 5px 10px;
background-color: #f8f9fa;
border-radius: 15px;
cursor: pointer;
transition: all 0.3s ease;
}
.tag-item:hover {
background-color: #0d6efd;
color: white;
}
/* 分页样式 */
.pagination {
margin-top: 20px;
}
/* 表单样式 */
.form-label {
font-weight: 500;
}
textarea.form-control {
min-height: 150px;
}
/* 卡片样式 */
.card {
margin-bottom: 20px;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.card-header {
background-color: #f8f9fa;
font-weight: 500;
}
/* 模态框样式 */
.modal-body h5 {
color: #6c757d;
font-size: 1rem;
margin-bottom: 0.5rem;
}
#view-answer {
background-color: #f8f9fa;
padding: 10px;
border-radius: 5px;
white-space: pre-line;
}
#view-tags .badge {
margin-right: 5px;
}
/* 导入/导出页面样式 */
pre {
background-color: #f8f9fa;
padding: 10px;
border-radius: 5px;
font-size: 0.875rem;
}
code {
color: #d63384;
}
/* 动画效果 */
.fade-in {
animation: fadeIn 0.5s;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
5. Knowledge Base JavaScript
static\css\styles.css
/* 全局样式 */
:root {
--primary-color: #4a6baf;
--secondary-color: #6c757d;
--accent-color: #3a5795;
--light-color: #f8f9fa;
--dark-color: #343a40;
--success-color: #28a745;
--info-color: #17a2b8;
--warning-color: #ffc107;
--danger-color: #dc3545;
--font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
font-family: var(--font-family);
background-color: #f5f7fb;
color: #333;
margin: 0;
padding: 0;
overflow-x: hidden;
}
/* 侧边栏样式 */
.sidebar {
background-color: var(--primary-color);
color: white;
height: 100vh;
padding: 0;
display: flex;
flex-direction: column;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
}
.sidebar-header {
padding: 20px 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-header h3 {
margin: 0;
font-size: 1.5rem;
}
.sidebar-menu {
flex-grow: 1;
padding: 15px 0;
}
.sidebar-menu .nav-link {
color: rgba(255, 255, 255, 0.8);
padding: 10px 15px;
transition: all 0.3s ease;
}
.sidebar-menu .nav-link:hover,
.sidebar-menu .nav-link.active {
color: white;
background-color: rgba(255, 255, 255, 0.1);
}
.sidebar-menu .nav-link i {
margin-right: 10px;
}
.sidebar-footer {
padding: 15px;
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.6);
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
/* 主内容区样式 */
.main-content {
height: 100vh;
padding: 0;
position: relative;
}
/* 聊天界面样式 */
.chat-container {
display: flex;
flex-direction: column;
height: 100%;
}
.chat-header {
padding: 15px 20px;
background-color: white;
border-bottom: 1px solid #e9ecef;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header h4 {
margin: 0;
}
.chat-messages {
flex-grow: 1;
padding: 20px;
overflow-y: auto;
background-color: #f5f7fb;
}
.message {
display: flex;
margin-bottom: 20px;
max-width: 80%;
}
.bot-message {
align-self: flex-start;
}
.user-message {
align-self: flex-end;
flex-direction: row-reverse;
margin-left: auto;
}
.message-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--primary-color);
color: white;
display: flex;
justify-content: center;
align-items: center;
margin-right: 10px;
}
.user-message .message-avatar {
background-color: var(--accent-color);
margin-right: 0;
margin-left: 10px;
}
.message-content {
background-color: white;
padding: 10px 15px;
border-radius: 10px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
max-width: calc(100% - 50px);
}
.user-message .message-content {
background-color: var(--primary-color);
color: white;
}
.message-content p {
margin: 0;
}
.chat-input {
padding: 15px 20px;
background-color: white;
border-top: 1px solid #e9ecef;
}
.chat-input .form-control {
border-radius: 20px;
padding-left: 15px;
}
.chat-input .btn {
border-radius: 20px;
margin-left: 10px;
}
/* 知识库界面样式 */
.knowledge-container,
.settings-container,
.history-container {
height: 100%;
padding: 20px;
overflow-y: auto;
}
.knowledge-header,
.settings-header,
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.knowledge-content,
.settings-content,
.history-content {
background-color: white;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
/* 响应式调整 */
@media (max-width: 768px) {
.sidebar {
position: fixed;
z-index: 1000;
width: 250px;
left: -250px;
transition: left 0.3s ease;
}
.sidebar.show {
left: 0;
}
.main-content {
width: 100%;
}
.message {
max-width: 90%;
}
}
/* 动画效果 */
.fade-in {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 打字动画效果 */
.typing-indicator {
display: flex;
padding: 10px;
}
.typing-indicator span {
height: 8px;
width: 8px;
background-color: #bbb;
border-radius: 50%;
margin: 0 2px;
display: inline-block;
animation: typing 1.4s infinite ease-in-out both;
}
.typing-indicator span:nth-child(1) {
animation-delay: 0s;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0% {
transform: scale(1);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}
static\js\app.js
/**
* 聊天机器人前端应用
* 负责处理用户界面交互和与后端API通信
*/
// 全局变量
const API_BASE_URL = '/api';
let userId = localStorage.getItem('userId') || 'default_user';
let currentView = 'chat'; // 当前视图:chat, knowledge, settings, history
// DOM元素
const chatForm = document.getElementById('chat-form');
const userInput = document.getElementById('user-input');
const chatMessages = document.getElementById('chat-messages');
const clearChatBtn = document.getElementById('clear-chat');
const newChatBtn = document.getElementById('new-chat');
const historyBtn = document.getElementById('history');
const knowledgeBaseBtn = document.getElementById('knowledge-base');
const settingsBtn = document.getElementById('settings');
const addKnowledgeBtn = document.getElementById('add-knowledge');
const saveKnowledgeBtn = document.getElementById('save-knowledge');
const settingsForm = document.getElementById('settings-form');
// 容器
const chatContainer = document.getElementById('chat-container');
const knowledgeContainer = document.getElementById('knowledge-container');
const settingsContainer = document.getElementById('settings-container');
const historyContainer = document.getElementById('history-container');
// 模态框
const addKnowledgeModal = new bootstrap.Modal(document.getElementById('add-knowledge-modal'));
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
// 加载用户设置
loadUserSettings();
// 加载聊天历史
loadChatHistory();
// 注册事件监听器
registerEventListeners();
});
/**
* 加载用户设置
*/
function loadUserSettings() {
// 从localStorage加载设置
document.getElementById('user-id').value = userId;
document.getElementById('use-mongodb').checked = localStorage.getItem('useMongoDb') === 'true';
document.getElementById('mongodb-uri').value = localStorage.getItem('mongoDbUri') || 'mongodb://localhost:27017/';
}
/**
* 注册事件监听器
*/
function registerEventListeners() {
// 发送消息
chatForm.addEventListener('submit', handleChatSubmit);
// 清空聊天
clearChatBtn.addEventListener('click', clearChat);
// 导航菜单
newChatBtn.addEventListener('click', () => switchView('chat'));
historyBtn.addEventListener('click', () => switchView('history'));
knowledgeBaseBtn.addEventListener('click', () => {
switchView('knowledge');
loadKnowledgeBase();
});
settingsBtn.addEventListener('click', () => switchView('settings'));
// 知识库管理
addKnowledgeBtn.addEventListener('click', () => addKnowledgeModal.show());
saveKnowledgeBtn.addEventListener('click', saveKnowledge);
// 设置表单
settingsForm.addEventListener('submit', saveSettings);
}
/**
* 切换视图
* @param {string} view - 视图名称
*/
function switchView(view) {
// 更新当前视图
currentView = view;
// 隐藏所有容器
chatContainer.classList.add('d-none');
knowledgeContainer.classList.add('d-none');
settingsContainer.classList.add('d-none');
historyContainer.classList.add('d-none');
// 显示选定的容器
switch (view) {
case 'chat':
chatContainer.classList.remove('d-none');
break;
case 'knowledge':
knowledgeContainer.classList.remove('d-none');
break;
case 'settings':
settingsContainer.classList.remove('d-none');
break;
case 'history':
historyContainer.classList.remove('d-none');
loadChatHistory();
break;
}
// 更新导航菜单活动项
document.querySelectorAll('.sidebar-menu .nav-link').forEach(link => {
link.classList.remove('active');
});
// 设置当前活动项
let activeLink;
switch (view) {
case 'chat':
activeLink = newChatBtn;
break;
case 'knowledge':
activeLink = knowledgeBaseBtn;
break;
case 'settings':
activeLink = settingsBtn;
break;
case 'history':
activeLink = historyBtn;
break;
}
if (activeLink) {
activeLink.classList.add('active');
}
}
/**
* 处理聊天表单提交
* @param {Event} event - 表单提交事件
*/
async function handleChatSubmit(event) {
event.preventDefault();
const message = userInput.value.trim();
if (!message) return;
// 清空输入框
userInput.value = '';
// 添加用户消息到聊天界面
addMessage(message, 'user');
// 显示机器人正在输入的指示器
showTypingIndicator();
try {
// 发送消息到后端
const response = await sendMessage(message);
// 移除输入指示器
removeTypingIndicator();
// 添加机器人回复到聊天界面
if (response && response.status === 'success') {
addMessage(response.response, 'bot');
} else {
addMessage('抱歉,处理您的消息时出现了问题。请稍后再试。', 'bot');
}
} catch (error) {
console.error('发送消息失败:', error);
// 移除输入指示器
removeTypingIndicator();
// 显示错误消息
addMessage('抱歉,连接服务器时出现了问题。请检查您的网络连接或稍后再试。', 'bot');
}
}
/**
* 发送消息到后端
* @param {string} message - 用户消息
* @returns {Promise<Object>} - 响应对象
*/
async function sendMessage(message) {
const response = await fetch(`${API_BASE_URL}/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
user_id: userId,
message: message
})
});
return await response.json();
}
/**
* 添加消息到聊天界面
* @param {string} text - 消息文本
* @param {string} sender - 发送者('user' 或 'bot')
*/
function addMessage(text, sender) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${sender}-message fade-in`;
const avatarDiv = document.createElement('div');
avatarDiv.className = 'message-avatar';
const icon = document.createElement('i');
icon.className = sender === 'user' ? 'bi bi-person' : 'bi bi-robot';
avatarDiv.appendChild(icon);
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
const paragraph = document.createElement('p');
paragraph.textContent = text;
contentDiv.appendChild(paragraph);
messageDiv.appendChild(avatarDiv);
messageDiv.appendChild(contentDiv);
chatMessages.appendChild(messageDiv);
// 滚动到底部
chatMessages.scrollTop = chatMessages.scrollHeight;
}
/**
* 显示机器人正在输入的指示器
*/
function showTypingIndicator() {
const indicatorDiv = document.createElement('div');
indicatorDiv.className = 'message bot-message fade-in';
indicatorDiv.id = 'typing-indicator';
const avatarDiv = document.createElement('div');
avatarDiv.className = 'message-avatar';
const icon = document.createElement('i');
icon.className = 'bi bi-robot';
avatarDiv.appendChild(icon);
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
const typingDiv = document.createElement('div');
typingDiv.className = 'typing-indicator';
for (let i = 0; i < 3; i++) {
const dot = document.createElement('span');
typingDiv.appendChild(dot);
}
contentDiv.appendChild(typingDiv);
indicatorDiv.appendChild(avatarDiv);
indicatorDiv.appendChild(contentDiv);
chatMessages.appendChild(indicatorDiv);
// 滚动到底部
chatMessages.scrollTop = chatMessages.scrollHeight;
}
/**
* 移除机器人正在输入的指示器
*/
function removeTypingIndicator() {
const indicator = document.getElementById('typing-indicator');
if (indicator) {
indicator.remove();
}
}
/**
* 清空聊天记录
*/
async function clearChat() {
if (confirm('确定要清空当前对话吗?')) {
try {
// 调用后端API清除历史
const response = await fetch(`${API_BASE_URL}/history/${userId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.status === 'success') {
// 清空聊天界面
chatMessages.innerHTML = '';
// 添加欢迎消息
addMessage('您好!我是智能助手,有什么可以帮您的吗?', 'bot');
} else {
alert('清空对话失败');
}
} catch (error) {
console.error('清空对话失败:', error);
alert('清空对话失败,请检查网络连接');
}
}
}
/**
* 加载聊天历史
*/
async function loadChatHistory() {
if (currentView !== 'history') return;
const historyList = document.getElementById('history-list');
historyList.innerHTML = '<div class="text-center"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div></div>';
try {
const response = await fetch(`${API_BASE_URL}/history/${userId}`);
const result = await response.json();
if (result.status === 'success') {
historyList.innerHTML = '';
if (result.history && result.history.length > 0) {
// 按日期分组
const groupedHistory = groupHistoryByDate(result.history);
// 渲染分组历史
for (const [date, messages] of Object.entries(groupedHistory)) {
const dateHeader = document.createElement('h6');
dateHeader.className = 'mt-3 mb-2 text-muted';
dateHeader.textContent = date;
historyList.appendChild(dateHeader);
for (const message of messages) {
const item = document.createElement('div');
item.className = 'list-group-item list-group-item-action';
const header = document.createElement('div');
header.className = 'd-flex justify-content-between align-items-center';
const time = new Date(message.timestamp).toLocaleTimeString();
header.innerHTML = `
<small class="text-muted">${time}</small>
<span class="badge bg-primary">${message.intent}</span>
`;
const userMsg = document.createElement('p');
userMsg.className = 'mb-1 mt-2';
userMsg.innerHTML = `<strong>用户:</strong> ${message.message}`;
const botMsg = document.createElement('p');
botMsg.className = 'mb-0 text-muted';
botMsg.innerHTML = `<strong>机器人:</strong> ${message.response}`;
item.appendChild(header);
item.appendChild(userMsg);
item.appendChild(botMsg);
historyList.appendChild(item);
}
}
} else {
historyList.innerHTML = '<div class="text-center p-3 text-muted">暂无对话历史</div>';
}
} else {
historyList.innerHTML = '<div class="text-center p-3 text-danger">加载历史失败</div>';
}
} catch (error) {
console.error('加载历史失败:', error);
historyList.innerHTML = '<div class="text-center p-3 text-danger">加载历史失败,请检查网络连接</div>';
}
}
/**
* 按日期分组历史记录
* @param {Array} history - 历史记录数组
* @returns {Object} - 按日期分组的历史记录
*/
function groupHistoryByDate(history) {
const grouped = {};
for (const message of history) {
const date = new Date(message.timestamp).toLocaleDateString();
if (!grouped[date]) {
grouped[date] = [];
}
grouped[date].push(message);
}
return grouped;
}
/**
* 加载知识库
*/
async function loadKnowledgeBase() {
const knowledgeList = document.getElementById('knowledge-list');
knowledgeList.innerHTML = '<tr><td colspan="5" class="text-center"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div></td></tr>';
try {
const response = await fetch(`${API_BASE_URL}/knowledge`);
const result = await response.json();
if (result.status === 'success') {
knowledgeList.innerHTML = '';
if (result.documents && result.documents.length > 0) {
for (const doc of result.documents) {
const row = document.createElement('tr');
// 截断长文本
const truncateText = (text, maxLength = 50) => {
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
};
row.innerHTML = `
<td>${doc.id}</td>
<td>${truncateText(doc.question)}</td>
<td>${truncateText(doc.answer)}</td>
<td>${new Date(doc.created_at).toLocaleString()}</td>
<td>
<button class="btn btn-sm btn-outline-primary view-knowledge" data-id="${doc.id}">
<i class="bi bi-eye"></i>
</button>
<button class="btn btn-sm btn-outline-danger delete-knowledge" data-id="${doc.id}">
<i class="bi bi-trash"></i>
</button>
</td>
`;
knowledgeList.appendChild(row);
}
// 添加查看和删除事件监听器
document.querySelectorAll('.view-knowledge').forEach(btn => {
btn.addEventListener('click', () => viewKnowledge(btn.dataset.id));
});
document.querySelectorAll('.delete-knowledge').forEach(btn => {
btn.addEventListener('click', () => deleteKnowledge(btn.dataset.id));
});
} else {
knowledgeList.innerHTML = '<tr><td colspan="5" class="text-center">知识库为空</td></tr>';
}
} else {
knowledgeList.innerHTML = '<tr><td colspan="5" class="text-center text-danger">加载知识库失败</td></tr>';
}
} catch (error) {
console.error('加载知识库失败:', error);
knowledgeList.innerHTML = '<tr><td colspan="5" class="text-center text-danger">加载知识库失败,请检查网络连接</td></tr>';
}
}
/**
* 保存知识条目
*/
async function saveKnowledge() {
const question = document.getElementById('knowledge-question').value.trim();
const answer = document.getElementById('knowledge-answer').value.trim();
if (!question || !answer) {
alert('问题和答案不能为空');
return;
}
try {
const response = await fetch(`${API_BASE_URL}/knowledge`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
question: question,
answer: answer
})
});
const result = await response.json();
if (result.status === 'success') {
// 关闭模态框
addKnowledgeModal.hide();
// 清空表单
document.getElementById('knowledge-question').value = '';
document.getElementById('knowledge-answer').value = '';
// 重新加载知识库
loadKnowledgeBase();
// 显示成功消息
alert('知识条目已添加');
} else {
alert('添加知识条目失败');
}
} catch (error) {
console.error('添加知识条目失败:', error);
alert('添加知识条目失败,请检查网络连接');
}
}
/**
* 查看知识条目
* @param {string} id - 知识条目ID
*/
function viewKnowledge(id) {
// TODO: 实现查看知识条目详情
alert(`查看知识条目 ${id} 的详情`);
}
/**
* 删除知识条目
* @param {string} id - 知识条目ID
*/
function deleteKnowledge(id) {
// TODO: 实现删除知识条目
if (confirm(`确定要删除知识条目 ${id} 吗?`)) {
alert(`删除知识条目 ${id}`);
}
}
/**
* 保存用户设置
* @param {Event} event - 表单提交事件
*/
function saveSettings(event) {
event.preventDefault();
// 获取设置值
const newUserId = document.getElementById('user-id').value.trim();
const useMongoDb = document.getElementById('use-mongodb').checked;
const mongoDbUri = document.getElementById('mongodb-uri').value.trim();
// 验证用户ID
if (!newUserId) {
alert('用户ID不能为空');
return;
}
// 保存到localStorage
localStorage.setItem('userId', newUserId);
localStorage.setItem('useMongoDb', useMongoDb);
localStorage.setItem('mongoDbUri', mongoDbUri);
// 更新全局变量
userId = newUserId;
// 显示成功消息
alert('设置已保存');
}
/**
* 处理API错误
* @param {Response} response - Fetch API响应对象
*/
async function handleApiError(response) {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
return response;
}
static\js\knowledge_base.js
/**
* 知识库管理系统前端JavaScript
*/
// 全局变量
let currentPage = 1;
let pageSize = 10;
let totalPages = 1;
let currentKnowledge = null;
let deleteKnowledgeId = null;
let allCategories = [];
let allTags = [];
let categoryChart = null;
let tagChart = null;
// DOM加载完成后执行
document.addEventListener('DOMContentLoaded', function() {
// 初始化页面
initPage();
// 加载知识列表
loadKnowledgeList();
// 加载分类和标签
loadCategories();
loadTags();
// 注册事件监听器
registerEventListeners();
});
/**
* 初始化页面
*/
function initPage() {
// 默认显示知识列表页
showPage('page-knowledge-list');
// 激活导航项
document.getElementById('nav-knowledge-list').classList.add('active');
}
/**
* 注册事件监听器
*/
function registerEventListeners() {
// 导航菜单点击事件
document.getElementById('nav-knowledge-list').addEventListener('click', function(e) {
e.preventDefault();
activateNavItem(this);
showPage('page-knowledge-list');
loadKnowledgeList();
});
document.getElementById('nav-add-knowledge').addEventListener('click', function(e) {
e.preventDefault();
activateNavItem(this);
showPage('page-add-knowledge');
resetKnowledgeForm();
document.getElementById('form-title').textContent = '添加知识';
});
document.getElementById('nav-categories').addEventListener('click', function(e) {
e.preventDefault();
activateNavItem(this);
showPage('page-categories');
loadCategoryList();
renderCategoryChart();
});
document.getElementById('nav-tags').addEventListener('click', function(e) {
e.preventDefault();
activateNavItem(this);
showPage('page-tags');
renderTagCloud();
renderTagChart();
});
document.getElementById('nav-import').addEventListener('click', function(e) {
e.preventDefault();
activateNavItem(this);
showPage('page-import');
});
document.getElementById('nav-export').addEventListener('click', function(e) {
e.preventDefault();
activateNavItem(this);
showPage('page-export');
loadExportOptions();
});
document.getElementById('nav-backup').addEventListener('click', function(e) {
e.preventDefault();
activateNavItem(this);
showPage('page-backup');
loadBackupList();
});
// 添加知识按钮点击事件
document.getElementById('btn-add-knowledge').addEventListener('click', function() {
showPage('page-add-knowledge');
resetKnowledgeForm();
document.getElementById('form-title').textContent = '添加知识';
activateNavItem(document.getElementById('nav-add-knowledge'));
});
// 返回列表按钮点击事件
document.getElementById('btn-back-to-list').addEventListener('click', function() {
showPage('page-knowledge-list');
activateNavItem(document.getElementById('nav-knowledge-list'));
});
// 知识表单提交事件
document.getElementById('knowledge-form').addEventListener('submit', function(e) {
e.preventDefault();
saveKnowledge();
});
// 取消按钮点击事件
document.getElementById('btn-cancel').addEventListener('click', function() {
showPage('page-knowledge-list');
activateNavItem(document.getElementById('nav-knowledge-list'));
});
// 搜索按钮点击事件
document.getElementById('btn-search').addEventListener('click', function() {
currentPage = 1;
loadKnowledgeList();
});
// 搜索框回车事件
document.getElementById('search-input').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
currentPage = 1;
loadKnowledgeList();
}
});
// 分类筛选器变化事件
document.getElementById('category-filter').addEventListener('change', function() {
currentPage = 1;
loadKnowledgeList();
});
// 标签筛选器变化事件
document.getElementById('tag-filter').addEventListener('change', function() {
currentPage = 1;
loadKnowledgeList();
});
// 编辑知识按钮点击事件(在模态框中)
document.getElementById('btn-edit-knowledge').addEventListener('click', function() {
const modal = bootstrap.Modal.getInstance(document.getElementById('view-knowledge-modal'));
modal.hide();
showPage('page-add-knowledge');
activateNavItem(document.getElementById('nav-add-knowledge'));
fillKnowledgeForm(currentKnowledge);
document.getElementById('form-title').textContent = '编辑知识';
});
// 确认删除按钮点击事件
document.getElementById('btn-confirm-delete').addEventListener('click', function() {
deleteKnowledge(deleteKnowledgeId);
});
// 导出范围选择事件
document.querySelectorAll('input[name="export-scope"]').forEach(function(radio) {
radio.addEventListener('change', function() {
const filterOptions = document.getElementById('export-filter-options');
if (this.value === 'filtered') {
filterOptions.style.display = 'block';
} else {
filterOptions.style.display = 'none';
}
});
});
// 导出表单提交事件
document.getElementById('export-form').addEventListener('submit', function(e) {
e.preventDefault();
exportKnowledge();
});
// 导入表单提交事件
document.getElementById('import-form').addEventListener('submit', function(e) {
e.preventDefault();
importKnowledge();
});
// 创建备份按钮点击事件
document.getElementById('btn-create-backup').addEventListener('click', function() {
createBackup();
});
// 恢复备份表单提交事件
document.getElementById('restore-form').addEventListener('submit', function(e) {
e.preventDefault();
restoreBackup();
});
}
/**
* 激活导航项
* @param {HTMLElement} navItem - 导航项元素
*/
function activateNavItem(navItem) {
// 移除所有导航项的激活状态
document.querySelectorAll('.nav-link').forEach(function(item) {
item.classList.remove('active');
});
// 激活当前导航项
navItem.classList.add('active');
}
/**
* 显示指定页面
* @param {string} pageId - 页面ID
*/
function showPage(pageId) {
// 隐藏所有页面
document.querySelectorAll('.page').forEach(function(page) {
page.classList.add('d-none');
});
// 显示指定页面
document.getElementById(pageId).classList.remove('d-none');
}
/**
* 加载知识列表
*/
function loadKnowledgeList() {
// 获取筛选条件
const query = document.getElementById('search-input').value;
const category = document.getElementById('category-filter').value;
const tag = document.getElementById('tag-filter').value;
// 构建API URL
let url = `/api/knowledge?page=${currentPage}&page_size=${pageSize}`;
if (query) url += `&query=${encodeURIComponent(query)}`;
if (category) url += `&category=${encodeURIComponent(category)}`;
if (tag) url += `&tag=${encodeURIComponent(tag)}`;
// 发送请求
fetch(url)
.then(response => response.json())
.then(data => {
if (data.success) {
// 渲染知识列表
renderKnowledgeList(data.data);
// 更新分页
if (data.pagination) {
totalPages = data.pagination.total_pages;
renderPagination(data.pagination);
}
} else {
showAlert('错误', data.message || '加载知识列表失败');
}
})
.catch(error => {
console.error('加载知识列表失败:', error);
showAlert('错误', '加载知识列表失败,请检查网络连接');
});
}
/**
* 渲染知识列表
* @param {Array} knowledgeList - 知识列表数据
*/
function renderKnowledgeList(knowledgeList) {
const tableBody = document.getElementById('knowledge-list');
tableBody.innerHTML = '';
if (knowledgeList.length === 0) {
// 没有数据
const row = document.createElement('tr');
row.innerHTML = `<td colspan="6" class="text-center">没有找到知识条目</td>`;
tableBody.appendChild(row);
return;
}
// 渲染每一行
knowledgeList.forEach(knowledge => {
const row = document.createElement('tr');
// 格式化更新时间
const updatedAt = new Date(knowledge.updated_at);
const formattedDate = updatedAt.toLocaleDateString() + ' ' + updatedAt.toLocaleTimeString();
// 获取分类
const category = knowledge.metadata && knowledge.metadata.category ? knowledge.metadata.category : '-';
// 获取标签
let tagsHtml = '-';
if (knowledge.metadata && knowledge.metadata.tags && knowledge.metadata.tags.length > 0) {
tagsHtml = knowledge.metadata.tags.map(tag =>
`<span class="badge bg-light text-dark badge-tag">${tag}</span>`
).join('');
}
row.innerHTML = `
<td>${knowledge.id}</td>
<td class="question-cell">${knowledge.question}</td>
<td>${category}</td>
<td>${tagsHtml}</td>
<td>${formattedDate}</td>
<td>
<button class="btn btn-sm btn-outline-primary btn-view" data-id="${knowledge.id}">
<i class="bi bi-eye"></i>
</button>
<button class="btn btn-sm btn-outline-secondary btn-edit" data-id="${knowledge.id}">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-danger btn-delete" data-id="${knowledge.id}">
<i class="bi bi-trash"></i>
</button>
</td>
`;
tableBody.appendChild(row);
});
// 添加查看按钮点击事件
document.querySelectorAll('.btn-view').forEach(button => {
button.addEventListener('click', function() {
const id = parseInt(this.getAttribute('data-id'));
viewKnowledge(id);
});
});
// 添加编辑按钮点击事件
document.querySelectorAll('.btn-edit').forEach(button => {
button.addEventListener('click', function() {
const id = parseInt(this.getAttribute('data-id'));
editKnowledge(id);
});
});
// 添加删除按钮点击事件
document.querySelectorAll('.btn-delete').forEach(button => {
button.addEventListener('click', function() {
const id = parseInt(this.getAttribute('data-id'));
confirmDeleteKnowledge(id);
});
});
}
/**
* 渲染分页控件
* @param {Object} pagination - 分页信息
*/
function renderPagination(pagination) {
const paginationElement = document.getElementById('pagination');
paginationElement.innerHTML = '';
// 如果只有一页,不显示分页
if (pagination.total_pages <= 1) {
return;
}
// 上一页按钮
const prevLi = document.createElement('li');
prevLi.className = `page-item ${pagination.page <= 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `<a class="page-link" href="#" data-page="${pagination.page - 1}">上一页</a>`;
paginationElement.appendChild(prevLi);
// 页码按钮
let startPage = Math.max(1, pagination.page - 2);
let endPage = Math.min(pagination.total_pages, startPage + 4);
if (endPage - startPage < 4) {
startPage = Math.max(1, endPage - 4);
}
for (let i = startPage; i <= endPage; i++) {
const li = document.createElement('li');
li.className = `page-item ${i === pagination.page ? 'active' : ''}`;
li.innerHTML = `<a class="page-link" href="#" data-page="${i}">${i}</a>`;
paginationElement.appendChild(li);
}
// 下一页按钮
const nextLi = document.createElement('li');
nextLi.className = `page-item ${pagination.page >= pagination.total_pages ? 'disabled' : ''}`;
nextLi.innerHTML = `<a class="page-link" href="#" data-page="${pagination.page + 1}">下一页</a>`;
paginationElement.appendChild(nextLi);
// 添加页码点击事件
document.querySelectorAll('.page-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const page = parseInt(this.getAttribute('data-page'));
if (page >= 1 && page <= pagination.total_pages) {
currentPage = page;
loadKnowledgeList();
}
});
});
}
/**
* 加载分类列表
*/
function loadCategories() {
fetch('/api/knowledge/categories')
.then(response => response.json())
.then(data => {
if (data.success) {
allCategories = data.data;
// 更新分类筛选器
const categoryFilter = document.getElementById('category-filter');
categoryFilter.innerHTML = '<option value="">所有分类</option>';
allCategories.forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
categoryFilter.appendChild(option);
});
// 更新分类数据列表
const categoryList = document.getElementById('category-list');
categoryList.innerHTML = '';
allCategories.forEach(category => {
const option = document.createElement('option');
option.value = category;
categoryList.appendChild(option);
});
// 更新导出页面的分类选择器
const exportCategory = document.getElementById('export-category');
exportCategory.innerHTML = '<option value="">所有分类</option>';
allCategories.forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
exportCategory.appendChild(option);
});
}
})
.catch(error => {
console.error('加载分类失败:', error);
});
}
/**
* 加载标签列表
*/
function loadTags() {
fetch('/api/knowledge/tags')
.then(response => response.json())
.then(data => {
if (data.success) {
allTags = data.data;
// 更新标签筛选器
const tagFilter = document.getElementById('tag-filter');
tagFilter.innerHTML = '<option value="">所有标签</option>';
allTags.forEach(tag => {
const option = document.createElement('option');
option.value = tag;
option.textContent = tag;
tagFilter.appendChild(option);
});
// 更新标签数据列表
const tagList = document.getElementById('tag-list');
tagList.innerHTML = '';
allTags.forEach(tag => {
const option = document.createElement('option');
option.value = tag;
tagList.appendChild(option);
});
// 更新导出页面的标签选择器
const exportTag = document.getElementById('export-tag');
exportTag.innerHTML = '<option value="">所有标签</option>';
allTags.forEach(tag => {
const option = document.createElement('option');
option.value = tag;
option.textContent = tag;
exportTag.appendChild(option);
});
}
})
.catch(error => {
console.error('加载标签失败:', error);
});
}
/**
* 查看知识详情
* @param {number} id - 知识ID
*/
function viewKnowledge(id) {
fetch(`/api/knowledge?id=${id}`)
.then(response => response.json())
.then(data => {
if (data.success) {
currentKnowledge = data.data;
// 填充模态框内容
document.getElementById('view-question').textContent = currentKnowledge.question;
document.getElementById('view-answer').textContent = currentKnowledge.answer;
// 显示分类
const category = currentKnowledge.metadata && currentKnowledge.metadata.category
? currentKnowledge.metadata.category
: '-';
document.getElementById('view-category').textContent = category;
// 显示标签
const tagsElement = document.getElementById('view-tags');
tagsElement.innerHTML = '';
if (currentKnowledge.metadata && currentKnowledge.metadata.tags && currentKnowledge.metadata.tags.length > 0) {
currentKnowledge.metadata.tags.forEach(tag => {
const badge = document.createElement('span');
badge.className = 'badge bg-light text-dark me-1';
badge.textContent = tag;
tagsElement.appendChild(badge);
});
} else {
tagsElement.textContent = '-';
}
// 显示创建和更新时间
const createdAt = new Date(currentKnowledge.created_at);
const updatedAt = new Date(currentKnowledge.updated_at);
document.getElementById('view-created-at').textContent = createdAt.toLocaleString();
document.getElementById('view-updated-at').textContent = updatedAt.toLocaleString();
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('view-knowledge-modal'));
modal.show();
} else {
showAlert('错误', data.message || '获取知识详情失败');
}
})
.catch(error => {
console.error('获取知识详情失败:', error);
showAlert('错误', '获取知识详情失败,请检查网络连接');
});
}
/**
* 编辑知识
* @param {number} id - 知识ID
*/
function editKnowledge(id) {
fetch(`/api/knowledge?id=${id}`)
.then(response => response.json())
.then(data => {
if (data.success) {
// 切换到编辑页面
showPage('page-add-knowledge');
activateNavItem(document.getElementById('nav-add-knowledge'));
// 填充表单
fillKnowledgeForm(data.data);
document.getElementById('form-title').textContent = '编辑知识';
} else {
showAlert('错误', data.message || '获取知识详情失败');
}
})
.catch(error => {
console.error('获取知识详情失败:', error);
showAlert('错误', '获取知识详情失败,请检查网络连接');
});
}
/**
* 填充知识表单
* @param {Object} knowledge - 知识对象
*/
function fillKnowledgeForm(knowledge) {
document.getElementById('knowledge-id').value = knowledge.id;
document.getElementById('question').value = knowledge.question;
document.getElementById('answer').value = knowledge.answer;
// 填充分类
if (knowledge.metadata && knowledge.metadata.category) {
document.getElementById('category').value = knowledge.metadata.category;
} else {
document.getElementById('category').value = '';
}
// 填充标签
if (knowledge.metadata && knowledge.metadata.tags && knowledge.metadata.tags.length > 0) {
document.getElementById('tags').value = knowledge.metadata.tags.join(', ');
} else {
document.getElementById('tags').value = '';
}
}
/**
* 重置知识表单
*/
function resetKnowledgeForm() {
document.getElementById('knowledge-form').reset();
document.getElementById('knowledge-id').value = '';
}
/**
* 保存知识
*/
function saveKnowledge() {
// 获取表单数据
const id = document.getElementById('knowledge-id').value;
const question = document.getElementById('question').value;
const answer = document.getElementById('answer').value;
const category = document.getElementById('category').value;
const tagsString = document.getElementById('tags').value;
// 解析标签
const tags = tagsString.split(',')
.map(tag => tag.trim())
.filter(tag => tag.length > 0);
// 构建知识对象
const knowledge = {
question: question,
answer: answer,
metadata: {
category: category,
tags: tags
}
};
// 确定是添加还是更新
const isUpdate = id !== '';
const url = isUpdate ? `/api/knowledge/${id}` : '/api/knowledge';
const method = isUpdate ? 'PUT' : 'POST';
// 发送请求
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(knowledge)
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('成功', isUpdate ? '知识更新成功' : '知识添加成功');
// 返回列表页并刷新
showPage('page-knowledge-list');
activateNavItem(document.getElementById('nav-knowledge-list'));
loadKnowledgeList();
} else {
showAlert('错误', data.message || (isUpdate ? '更新知识失败' : '添加知识失败'));
}
})
.catch(error => {
console.error(isUpdate ? '更新知识失败:' : '添加知识失败:', error);
showAlert('错误', (isUpdate ? '更新知识失败' : '添加知识失败') + ',请检查网络连接');
});
}
/**
* 确认删除知识
* @param {number} id - 知识ID
*/
function confirmDeleteKnowledge(id) {
deleteKnowledgeId = id;
const modal = new bootstrap.Modal(document.getElementById('confirm-delete-modal'));
modal.show();
}
/**
* 删除知识
* @param {number} id - 知识ID
*/
function deleteKnowledge(id) {
fetch(`/api/knowledge/${id}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
// 关闭确认模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('confirm-delete-modal'));
modal.hide();
if (data.success) {
showAlert('成功', '知识删除成功');
loadKnowledgeList();
} else {
showAlert('错误', data.message || '删除知识失败');
}
})
.catch(error => {
console.error('删除知识失败:', error);
showAlert('错误', '删除知识失败,请检查网络连接');
// 关闭确认模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('confirm-delete-modal'));
modal.hide();
});
}
/**
* 加载分类列表页面
*/
function loadCategoryList() {
const categoryListElement = document.getElementById('category-list-items');
categoryListElement.innerHTML = '';
if (allCategories.length === 0) {
categoryListElement.innerHTML = '<li class="list-group-item">没有分类数据</li>';
return;
}
// 为每个分类创建一个列表项
allCategories.forEach(category => {
const li = document.createElement('li');
li.className = 'list-group-item d-flex justify-content-between align-items-center';
// 获取该分类的知识数量
fetch(`/api/knowledge?category=${encodeURIComponent(category)}&page=1&page_size=1`)
.then(response => response.json())
.then(data => {
const count = data.pagination ? data.pagination.total : 0;
li.innerHTML = `
${category}
<span class="badge bg-primary rounded-pill">${count}</span>
`;
})
.catch(error => {
console.error('获取分类数量失败:', error);
li.innerHTML = `
${category}
<span class="badge bg-secondary rounded-pill">?</span>
`;
});
categoryListElement.appendChild(li);
});
}
/**
* 渲染分类图表
*/
function renderCategoryChart() {
const ctx = document.getElementById('category-chart').getContext('2d');
// 如果已存在图表,先销毁
if (categoryChart) {
categoryChart.destroy();
}
// 准备数据
const categories = [];
const counts = [];
// 获取每个分类的知识数量
const promises = allCategories.map(category => {
return fetch(`/api/knowledge?category=${encodeURIComponent(category)}&page=1&page_size=1`)
.then(response => response.json())
.then(data => {
const count = data.pagination ? data.pagination.total : 0;
categories.push(category);
counts.push(count);
});
});
Promise.all(promises)
.then(() => {
// 创建图表
categoryChart = new Chart(ctx, {
type: 'bar',
data: {
labels: categories,
datasets: [{
label: '知识数量',
data: counts,
backgroundColor: 'rgba(13, 110, 253, 0.7)',
borderColor: 'rgba(13, 110, 253, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
text: '各分类知识数量统计'
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
}
}
});
})
.catch(error => {
console.error('渲染分类图表失败:', error);
});
}
/**
* 渲染标签云
*/
function renderTagCloud() {
const tagCloudElement = document.getElementById('tag-cloud');
tagCloudElement.innerHTML = '';
if (allTags.length === 0) {
tagCloudElement.innerHTML = '<p class="text-center">没有标签数据</p>';
return;
}
// 获取每个标签的知识数量
const promises = allTags.map(tag => {
return fetch(`/api/knowledge?tag=${encodeURIComponent(tag)}&page=1&page_size=1`)
.then(response => response.json())
.then(data => {
const count = data.pagination ? data.pagination.total : 0;
// 根据数量确定字体大小
let fontSize = 14;
if (count > 10) fontSize = 24;
else if (count > 5) fontSize = 20;
else if (count > 2) fontSize = 16;
// 创建标签元素
const tagElement = document.createElement('div');
tagElement.className = 'tag-item';
tagElement.textContent = tag;
tagElement.style.fontSize = `${fontSize}px`;
// 点击标签时筛选知识列表
tagElement.addEventListener('click', function() {
document.getElementById('tag-filter').value = tag;
showPage('page-knowledge-list');
activateNavItem(document.getElementById('nav-knowledge-list'));
currentPage = 1;
loadKnowledgeList();
});
tagCloudElement.appendChild(tagElement);
});
});
Promise.all(promises)
.catch(error => {
console.error('渲染标签云失败:', error);
tagCloudElement.innerHTML = '<p class="text-center text-danger">加载标签数据失败</p>';
});
}
/**
* 渲染标签图表
*/
function renderTagChart() {
const ctx = document.getElementById('tag-chart').getContext('2d');
// 如果已存在图表,先销毁
if (tagChart) {
tagChart.destroy();
}
// 准备数据
const tags = [];
const counts = [];
// 获取每个标签的知识数量
const promises = allTags.map(tag => {
return fetch(`/api/knowledge?tag=${encodeURIComponent(tag)}&page=1&page_size=1`)
.then(response => response.json())
.then(data => {
const count = data.pagination ? data.pagination.total : 0;
tags.push(tag);
counts.push(count);
});
});
Promise.all(promises)
.then(() => {
// 创建图表
tagChart = new Chart(ctx, {
type: 'pie',
data: {
labels: tags,
datasets: [{
data: counts,
backgroundColor: [
'rgba(255, 99, 132, 0.7)',
'rgba(54, 162, 235, 0.7)',
'rgba(255, 206, 86, 0.7)',
'rgba(75, 192, 192, 0.7)',
'rgba(153, 102, 255, 0.7)',
'rgba(255, 159, 64, 0.7)',
'rgba(199, 199, 199, 0.7)',
'rgba(83, 102, 255, 0.7)',
'rgba(40, 159, 64, 0.7)',
'rgba(210, 199, 199, 0.7)'
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)',
'rgba(199, 199, 199, 1)',
'rgba(83, 102, 255, 1)',
'rgba(40, 159, 64, 1)',
'rgba(210, 199, 199, 1)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'right',
},
title: {
display: true,
text: '各标签知识数量统计'
}
}
}
});
})
.catch(error => {
console.error('渲染标签图表失败:', error);
});
}
/**
* 导入知识
*/
function importKnowledge() {
const fileInput = document.getElementById('import-file');
if (!fileInput.files || fileInput.files.length === 0) {
showAlert('错误', '请选择要导入的文件');
return;
}
const file = fileInput.files[0];
// 检查文件类型
if (!file.name.endsWith('.json') && !file.name.endsWith('.txt') && !file.name.endsWith('.csv')) {
showAlert('错误', '不支持的文件类型,请选择 JSON、TXT 或 CSV 文件');
return;
}
// 创建表单数据
const formData = new FormData();
formData.append('file', file);
// 发送请求
fetch('/api/knowledge/import', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('成功', data.message || '知识导入成功');
fileInput.value = '';
} else {
showAlert('错误', data.message || '知识导入失败');
}
})
.catch(error => {
console.error('导入知识失败:', error);
showAlert('错误', '导入知识失败,请检查网络连接');
});
}
/**
* 加载导出选项
*/
function loadExportOptions() {
// 已在加载分类和标签时填充了选择器
}
/**
* 导出知识
*/
function exportKnowledge() {
// 获取导出范围
const scope = document.querySelector('input[name="export-scope"]:checked').value;
// 构建URL
let url = '/api/knowledge/export';
if (scope === 'filtered') {
const query = document.getElementById('export-query').value;
const category = document.getElementById('export-category').value;
const tag = document.getElementById('export-tag').value;
if (query) url += `?query=${encodeURIComponent(query)}`;
if (category) url += `${url.includes('?') ? '&' : '?'}category=${encodeURIComponent(category)}`;
if (tag) url += `${url.includes('?') ? '&' : '?'}tag=${encodeURIComponent(tag)}`;
}
// 触发下载
window.location.href = url;
}
/**
* 加载备份列表
*/
function loadBackupList() {
// 这个功能需要后端提供API支持
// 由于简化版本中没有实现备份列表API,这里仅显示一个提示
document.getElementById('backup-list').innerHTML = `
<tr>
<td colspan="3" class="text-center">请使用创建备份功能生成新的备份</td>
</tr>
`;
}
/**
* 创建备份
*/
function createBackup() {
fetch('/api/knowledge/backup', {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('成功', `知识库备份成功,备份文件:${data.backup_path}`);
} else {
showAlert('错误', data.message || '创建备份失败');
}
})
.catch(error => {
console.error('创建备份失败:', error);
showAlert('错误', '创建备份失败,请检查网络连接');
});
}
/**
* 恢复备份
*/
function restoreBackup() {
const fileInput = document.getElementById('backup-file');
if (!fileInput.files || fileInput.files.length === 0) {
showAlert('错误', '请选择备份文件');
return;
}
const file = fileInput.files[0];
// 检查文件类型
if (!file.name.endsWith('.json')) {
showAlert('错误', '不支持的文件类型,请选择 JSON 备份文件');
return;
}
// 读取文件内容
const reader = new FileReader();
reader.onload = function(e) {
try {
// 解析JSON
const data = JSON.parse(e.target.result);
// 确认是否清除现有数据
const clearExisting = document.getElementById('clear-existing').checked;
// 发送恢复请求
fetch('/api/knowledge/restore', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
backup_data: data,
clear_existing: clearExisting
})
})
.then(response => response.json())
.then(result => {
if (result.success) {
showAlert('成功', result.message || '知识库恢复成功');
fileInput.value = '';
} else {
showAlert('错误', result.message || '知识库恢复失败');
}
})
.catch(error => {
console.error('恢复备份失败:', error);
showAlert('错误', '恢复备份失败,请检查网络连接');
});
} catch (error) {
console.error('解析备份文件失败:', error);
showAlert('错误', '无效的备份文件格式');
}
};
reader.readAsText(file);
}
/**
* 显示提示框
* @param {string} title - 标题
* @param {string} message - 消息内容
*/
function showAlert(title, message) {
const alertTitle = document.getElementById('alert-title');
const alertMessage = document.getElementById('alert-message');
alertTitle.textContent = title;
alertMessage.textContent = message;
const modal = new bootstrap.Modal(document.getElementById('alert-modal'));
modal.show();
}