├── scene_processor ├── impl │ ├── __init__.py │ └── common_processor.py ├── __init__.py └── scene_processor.py ├── utils ├── __init__.py ├── app_init.py ├── file_utils.py ├── date_utils.py ├── prompt_utils.py ├── data_format_utils.py └── helpers.py ├── models ├── __init__.py └── chatbot_model.py ├── scene_config ├── __init__.py ├── scene_prompts.py └── scene_templates.json ├── images ├── demo.gif ├── workflow.png └── slot_multi-turn-flow.png ├── requirements.txt ├── frontend ├── src │ ├── index.js │ ├── components │ │ ├── KnowledgeCitation.jsx │ │ ├── SlotFiller.jsx │ │ ├── MessageBubble.jsx │ │ ├── AIChatBox.jsx │ │ └── ChatBox.jsx │ ├── App.css │ ├── config │ │ └── config.js │ ├── App.jsx │ ├── utils │ │ ├── intentMap.js │ │ └── businessBot.js │ └── api │ │ └── aiApi.js ├── .env.example ├── public │ └── index.html ├── package.json └── README.md ├── .env.example ├── CONTRIBUTING.md ├── start_frontend.sh ├── start_backend.sh ├── package.json ├── config ├── log_config.py └── __init__.py ├── app_config.py ├── start_dev.cmd ├── 快速开始.md ├── start_dev.sh ├── README.md ├── .gitignore ├── 前后端联调说明.md ├── demo ├── user_input.html.bak └── user_input.html ├── app.py ├── LICENSE └── log.txt /scene_processor/impl/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | print("Utils package initialized.") -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | print("Models package initialized.") -------------------------------------------------------------------------------- /scene_config/__init__.py: -------------------------------------------------------------------------------- 1 | print("SceneConfig package initialized.") -------------------------------------------------------------------------------- /scene_processor/__init__.py: -------------------------------------------------------------------------------- 1 | print("SceneProcessor package initialized.") -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/answerlink/IntelliQ/HEAD/images/demo.gif -------------------------------------------------------------------------------- /images/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/answerlink/IntelliQ/HEAD/images/workflow.png -------------------------------------------------------------------------------- /images/slot_multi-turn-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/answerlink/IntelliQ/HEAD/images/slot_multi-turn-flow.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.1.1 2 | requests==2.26.0 3 | Werkzeug==2.3.7 4 | flask_cors==3.0.10 5 | dashscope==1.13.6 6 | python-dotenv==1.0.0 -------------------------------------------------------------------------------- /utils/app_init.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | from config.log_config import setup_logging 3 | 4 | 5 | def before_init(): 6 | """ 7 | app主程序启动初始化 8 | """ 9 | # 在程序主入口处调用设置日志的函数 10 | setup_logging() 11 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import 'antd/dist/reset.css'; 5 | 6 | const root = ReactDOM.createRoot(document.getElementById('root')); 7 | root.render(); -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | # 前端环境变量配置模板 2 | # React 应用的环境变量必须以 REACT_APP_ 开头 3 | 4 | # 后端API地址 5 | REACT_APP_API_BASE_URL=http://localhost:5050 6 | 7 | # 环境标识 (development/production) 8 | REACT_APP_ENV=development 9 | 10 | # 是否启用开发模式日志 (true/false) 11 | REACT_APP_DEBUG=true 12 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 电信业务AI问答系统 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /scene_processor/scene_processor.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | class SceneProcessor: 3 | def process(self, user_input, context): 4 | """ 5 | 处理用户输入和上下文,返回处理结果 6 | :param user_input: 用户的输入 7 | :param context: 对话的上下文 8 | :return: 处理结果 9 | """ 10 | raise NotImplementedError 11 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # 后端配置 2 | FLASK_ENV=development 3 | FLASK_DEBUG=True 4 | BACKEND_HOST=localhost 5 | BACKEND_PORT=5050 6 | 7 | # 前端配置 8 | FRONTEND_HOST=localhost 9 | FRONTEND_PORT=3000 10 | 11 | # API配置 12 | API_PREFIX=/api 13 | 14 | # CORS配置 (逗号分隔的允许跨域的地址) 15 | CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 16 | 17 | # 其他配置 18 | LOG_LEVEL=DEBUG 19 | -------------------------------------------------------------------------------- /utils/file_utils.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | import json 3 | import logging 4 | 5 | 6 | def load_file_to_obj(filepath): 7 | try: 8 | with open(filepath, 'r', encoding='utf-8') as file: 9 | return json.load(file) 10 | except (FileNotFoundError, json.JSONDecodeError) as e: 11 | logging.error(f"Error loading scene prompts: {e}") 12 | return {} 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献指南 2 | 3 | 欢迎对 IntelliQ 项目做出贡献!以下是参与贡献的一些指导原则。 4 | 5 | ## 报告问题 6 | 7 | 遇到问题时,请首先检查问题跟踪器以确保问题尚未被报告。如果您发现新问题,请提出一个清晰且详细的问题描述。 8 | 9 | ## 提交更改 10 | 11 | 欲提交您的更改,请遵循以下流程: 12 | 13 | 1. 在项目中创建一个新的分支。 14 | 2. 进行您的更改。 15 | 3. 如果可能,请添加测试。 16 | 4. 提交您的更改,并编写清晰的提交消息。 17 | 5. 创建一个Pull Request。 18 | 19 | ## 开发流程 20 | 21 | 我们欢迎任何形式的贡献,无论是修复bug、增加新功能还是改进文档。 22 | 23 | ## 代码规范 24 | 25 | 请遵循 PEP 8 代码风格指南。 26 | 27 | 感谢您的贡献! 28 | -------------------------------------------------------------------------------- /frontend/src/components/KnowledgeCitation.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function KnowledgeCitation({ citations }) { 4 | if (!citations || citations.length === 0) return null; 5 | return ( 6 |
7 |
知识库引用:
8 | 13 |
14 | ); 15 | } -------------------------------------------------------------------------------- /start_frontend.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # CRM助手 - 前端服务启动脚本 4 | 5 | echo "🌐 启动CRM助手前端服务..." 6 | 7 | # 进入前端目录 8 | cd frontend 9 | 10 | # 检查是否存在.env文件 11 | if [ ! -f ".env" ]; then 12 | echo "⚠️ 未找到前端.env文件,从.env.example复制..." 13 | if [ -f ".env.example" ]; then 14 | cp .env.example .env 15 | echo "✅ 已创建前端.env文件" 16 | fi 17 | fi 18 | 19 | # 安装依赖 20 | if [ ! -d "node_modules" ]; then 21 | echo "📦 安装前端依赖..." 22 | npm install 23 | fi 24 | 25 | # 启动服务 26 | echo "🚀 启动前端服务 (端口3000)..." 27 | npm start -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #f5f6fa; 3 | font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif; 4 | } 5 | 6 | .ant-layout { 7 | min-height: 100vh; 8 | } 9 | 10 | .ant-tabs-card .ant-tabs-content { 11 | background: none; 12 | } 13 | 14 | @media (max-width: 600px) { 15 | .ant-layout-content { 16 | padding: 8px !important; 17 | } 18 | .ant-layout-header { 19 | flex-direction: column !important; 20 | align-items: flex-start !important; 21 | height: auto !important; 22 | padding: 8px 12px !important; 23 | } 24 | } -------------------------------------------------------------------------------- /start_backend.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # CRM助手 - 后端服务启动脚本 4 | 5 | echo "🖥️ 启动CRM助手后端服务..." 6 | 7 | # 检查是否存在.env文件 8 | if [ ! -f ".env" ]; then 9 | echo "⚠️ 未找到.env文件,从.env.example复制..." 10 | if [ -f ".env.example" ]; then 11 | cp .env.example .env 12 | echo "✅ 已创建.env文件" 13 | fi 14 | fi 15 | 16 | # 检查Python虚拟环境 17 | if [ ! -d "venv" ]; then 18 | echo "📦 创建Python虚拟环境..." 19 | python3 -m venv venv 20 | fi 21 | 22 | # 激活虚拟环境 23 | echo "🔧 激活Python虚拟环境..." 24 | source venv/bin/activate 25 | 26 | # 安装依赖 27 | echo "📋 安装Python依赖..." 28 | pip install -r requirements.txt 29 | 30 | # 启动服务 31 | echo "🚀 启动后端服务 (端口5050)..." 32 | python app.py -------------------------------------------------------------------------------- /frontend/src/config/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 前端应用配置 3 | */ 4 | 5 | // 从环境变量获取配置,提供默认值 6 | const config = { 7 | // API配置 8 | apiBaseUrl: process.env.REACT_APP_API_BASE_URL || 'http://localhost:5050', 9 | 10 | // 环境配置 11 | env: process.env.REACT_APP_ENV || 'development', 12 | 13 | // 调试模式 14 | debug: process.env.REACT_APP_DEBUG === 'true', 15 | 16 | // 是否为开发环境 17 | isDevelopment: process.env.NODE_ENV === 'development', 18 | 19 | // 是否为生产环境 20 | isProduction: process.env.NODE_ENV === 'production', 21 | 22 | // API路径前缀 23 | apiPrefix: '/api' 24 | }; 25 | 26 | // 开发环境下打印配置信息 27 | if (config.debug && config.isDevelopment) { 28 | console.log('📋 前端配置信息:', config); 29 | } 30 | 31 | export default config; -------------------------------------------------------------------------------- /utils/date_utils.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | from datetime import datetime, timedelta 3 | 4 | 5 | def get_current_date(): 6 | current_date = datetime.now() 7 | return current_date.strftime("%Y-%m-%d") 8 | 9 | 10 | def get_current_and_future_dates(days=7): 11 | """ 12 | 计算当前日期和未来指定天数后的日期。 13 | 14 | :param days: 从当前日期起的天数,默认为7天。 15 | :return: 当前日期和未来日期的字符串(格式:YYYY-MM-DD)。 16 | 17 | # 使用示例 18 | # current_date, future_date = get_current_and_future_dates() 19 | # print("当前日期:", current_date) 20 | # print("7天后日期:", future_date) 21 | """ 22 | current_date = datetime.now() 23 | future_date = current_date + timedelta(days=days) 24 | 25 | return current_date.strftime("%Y-%m-%d"), future_date.strftime("%Y-%m-%d") 26 | 27 | 28 | -------------------------------------------------------------------------------- /utils/prompt_utils.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | import json 3 | 4 | from scene_config import scene_prompts 5 | from utils.date_utils import get_current_date 6 | from utils.helpers import get_slot_query_user_json, get_slot_update_json 7 | 8 | 9 | def get_slot_update_message(scene_name, dynamic_example, slot_template, user_input): 10 | message = scene_prompts.slot_update.format(scene_name, get_current_date(), dynamic_example, json.dumps(get_slot_update_json(slot_template), ensure_ascii=False), user_input) 11 | return message 12 | 13 | 14 | def get_slot_query_user_message(scene_name, slot, user_input): 15 | message = scene_prompts.slot_query_user.format(scene_name, json.dumps(get_slot_query_user_json(slot), ensure_ascii=False), user_input) 16 | return message 17 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "telecom-ai-frontend", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "antd": "^5.9.1", 7 | "axios": "^1.4.0", 8 | "react": "^18.2.0", 9 | "react-dom": "^18.2.0", 10 | "react-router-dom": "^6.14.1", 11 | "react-scripts": "^5.0.1" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test", 17 | "eject": "react-scripts eject" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crm-assistant", 3 | "version": "1.0.0", 4 | "description": "CRM助手 - 基于LLM的多轮问答系统", 5 | "scripts": { 6 | "start": "npm run dev", 7 | "dev": "./start_dev.sh", 8 | "backend": "./start_backend.sh", 9 | "frontend": "./start_frontend.sh", 10 | "test": "python test_connection.py", 11 | "install-all": "pip install -r requirements.txt && cd frontend && npm install" 12 | }, 13 | "keywords": [ 14 | "crm", 15 | "ai", 16 | "chatbot", 17 | "llm", 18 | "react", 19 | "flask" 20 | ], 21 | "author": "CRM Team", 22 | "license": "Apache-2.0", 23 | "engines": { 24 | "node": ">=16.0.0", 25 | "python": ">=3.8.0" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/answerlink/IntelliQ.git" 30 | } 31 | } -------------------------------------------------------------------------------- /config/log_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | def setup_logging(): 5 | # 设置日志的配置 6 | logging.basicConfig( 7 | level=logging.DEBUG, # 设置日志级别为 DEBUG,也可以设置为 INFO, WARNING, ERROR, CRITICAL 8 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', # 设置日志格式 9 | filename='app.log', # 日志输出到文件,不设置这个参数则输出到标准输出(控制台) 10 | filemode='w' # 'w' 表示写模式,'a' 表示追加模式 11 | ) 12 | 13 | # 如果还想要将日志输出到控制台,可以添加一个 StreamHandler 14 | console_handler = logging.StreamHandler() 15 | console_handler.setLevel(logging.DEBUG) # 设置控制台的日志级别 16 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') 17 | console_handler.setFormatter(formatter) 18 | logging.getLogger('').addHandler(console_handler) 19 | logging.debug('logging start...') 20 | 21 | 22 | # 示例:记录一条信息 23 | # logging.info("This is an info message") 24 | -------------------------------------------------------------------------------- /scene_config/scene_prompts.py: -------------------------------------------------------------------------------- 1 | slot_update = """你是一个信息抽取机器人。 2 | 当前问答场景是:【{}】 3 | 当前日期是:{} 4 | 5 | JSON中每个元素代表一个参数信息: 6 | ''' 7 | name是参数名称 8 | desc是参数注释,可以做为参数信息的补充 9 | ''' 10 | 11 | 需求: 12 | #01 根据用户输入内容提取有用的信息到value值,严格提取,没有提及就丢弃该元素,禁止将“未提及”写入value 13 | #02 返回JSON结果,只需要name和value收到 14 | 15 | 返回样例: 16 | ``` 17 | {} 18 | ``` 19 | 20 | JSON:{} 21 | 输入:{} 22 | 答: 23 | """ 24 | 25 | slot_query_user = """你是一个专业的客服。 26 | 当前问答场景是:【{}】 27 | 28 | JSON中每个元素代表一个参数信息: 29 | ''' 30 | name表示参数名称 31 | desc表示参数的描述,你要根据描述引导用户补充参数value值 32 | ''' 33 | 34 | 需求: 35 | #01 一次最多只向用户问两个参数 36 | #02 回答以"请问"开头 37 | 38 | JSON:{} 39 | 向用户提问: 40 | """ 41 | 42 | no_scene_response = """你是一个专业的电信客服助手。 43 | 你可以处理的场景有: 44 | {1} 45 | 46 | 首先请礼貌拒绝用户的要求(如有),并说明这在你能力之外。 47 | 然后引导用户明确表达他们的需求。 48 | 用户输入:{0} 49 | """ 50 | 51 | scene_switch_detection = """你是一个场景意图判断助手。 52 | 当前正在处理的场景是:【{}】 53 | 54 | 请判断用户当前的输入是否表达了切换场景的意图。 55 | - 如果用户想要切换到其他场景或开始新的任务,回复:1 56 | - 如果用户没有明确表达切换场景的意图,回复:0 57 | 58 | 用户输入:{} 59 | 请回复(只回复0或1):""" -------------------------------------------------------------------------------- /utils/data_format_utils.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | import re 3 | 4 | 5 | def extract_float(s): 6 | # 提取字符串中的第一个浮点型数字 7 | float_pattern = r'-?\d+(?:\.\d+)?' 8 | found = re.findall(float_pattern, s) 9 | if not found: # 如果没有找到任何数字 10 | return 0.0 11 | else: 12 | return [float(num) for num in found][0] 13 | 14 | 15 | def extract_floats(s): 16 | # 提取字符串中的所有浮点型数字 17 | float_pattern = r'-?\d+(?:\.\d+)?' 18 | found = re.findall(float_pattern, s) 19 | if not found: # 如果没有找到任何数字 20 | return [0.0] 21 | else: 22 | return [float(num) for num in found] 23 | 24 | 25 | def extract_continuous_digits(text): 26 | # 使用正则表达式找到所有连续的数字 27 | continuous_digits = re.findall(r'\d+', text) 28 | return continuous_digits 29 | 30 | 31 | def clean_json_string(json_str): 32 | # 首先移除非 JSON 字符,然后查找 JSON 对象或数组 33 | cleaned_str = re.search(r'(\{.*\}|\[.*\])', json_str) 34 | if cleaned_str: 35 | return cleaned_str.group() 36 | else: 37 | return None 38 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # 电信业务AI问答系统前端 2 | 3 | ## 项目简介 4 | 本前端实现为运营商AI智能体产品的Web端问答交互界面,支持多轮对话、多业务办理流程模拟,与后端API无缝集成。 5 | 6 | ## 目录结构 7 | - public/ # 静态资源与index.html 8 | - src/ 9 | - App.jsx # 主应用入口,tab多对话 10 | - index.js # React入口 11 | - App.css # 全局样式 12 | - components/ # 主要UI组件 13 | - ChatBox.jsx # 聊天主窗口 14 | - MessageBubble.jsx # 聊天气泡 15 | - SlotFiller.jsx # 卡槽补全区 16 | - KnowledgeCitation.jsx # 知识库引用区 17 | - api/ 18 | - aiApi.js # 封装后端API请求 19 | 20 | ## 功能特性 21 | - 多tab聊天对话,支持历史记录不丢失 22 | - 新对话一键添加 23 | - 聊天输入框支持快捷发送 24 | - 聊天气泡AI进度、知识库引用区 25 | - 自动卡槽补全与业务流程联动(mock_slots自动适配) 26 | - 响应式适配桌面与移动端 27 | 28 | ## 启动方式 29 | ```bash 30 | cd frontend 31 | npm install 32 | npm start 33 | ``` 34 | 默认端口3000,需后端API服务(如Flask)已启动。 35 | 36 | ## API联调说明 37 | - 主要通过`/api/faq`、`/api/mock_slots`等接口与后端联动。 38 | - 可在`api/aiApi.js`中自定义API路径。 39 | - 所有接口需结构化返回,详见后端文档。 40 | 41 | ## 依赖 42 | - React 18 43 | - Ant Design 5 44 | - Axios 45 | - react-router-dom 46 | 47 | ## 其他 48 | 如需扩展业务办理流程、知识库引用、卡槽补全等功能,请参考`components/`和`api/`目录代码。 -------------------------------------------------------------------------------- /app_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | # 加载环境变量 5 | load_dotenv() 6 | 7 | class Config: 8 | """应用配置类""" 9 | 10 | # 后端配置 11 | FLASK_ENV = os.getenv('FLASK_ENV', 'development') 12 | FLASK_DEBUG = os.getenv('FLASK_DEBUG', 'True').lower() == 'true' 13 | BACKEND_HOST = os.getenv('BACKEND_HOST', 'localhost') 14 | BACKEND_PORT = int(os.getenv('BACKEND_PORT', 5050)) 15 | 16 | # 前端配置 17 | FRONTEND_HOST = os.getenv('FRONTEND_HOST', 'localhost') 18 | FRONTEND_PORT = int(os.getenv('FRONTEND_PORT', 3000)) 19 | 20 | # API配置 21 | API_PREFIX = os.getenv('API_PREFIX', '/api') 22 | 23 | # CORS配置 24 | CORS_ORIGINS = "*" 25 | 26 | # 其他配置 27 | LOG_LEVEL = os.getenv('LOG_LEVEL', 'DEBUG') 28 | 29 | @property 30 | def backend_url(self): 31 | """获取后端完整URL""" 32 | return f"http://{self.BACKEND_HOST}:{self.BACKEND_PORT}" 33 | 34 | @property 35 | def frontend_url(self): 36 | """获取前端完整URL""" 37 | return f"http://{self.FRONTEND_HOST}:{self.FRONTEND_PORT}" 38 | 39 | # 全局配置实例 40 | config = Config() -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- 1 | print("Config package initialized.") 2 | 3 | DEBUG = True 4 | 5 | # MODEL ------------------------------------------------------------------------ 6 | 7 | # 模型支持OpenAI规范接口 8 | GPT_URL = 'https://api.siliconflow.cn/v1/chat/completions' 9 | MODEL = 'Qwen/Qwen3-32B' 10 | API_KEY = 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 11 | SYSTEM_PROMPT = 'You are a helpful assistant.' 12 | 13 | # MODEL ------------------------------------------------------------------------ 14 | 15 | # API CONFIGURATION ------------------------------------------------------------------------ 16 | 17 | # API基础地址 18 | API_BASE_URL = 'http://xxxxxxx:xxxx' 19 | 20 | # 场景配置API地址(已弃用) 21 | SCENE_CONFIG_API_URL = f'{API_BASE_URL}/api/mock_slots' 22 | 23 | # 场景处理API地址模板 24 | SCENE_API_URL_TEMPLATE = f'{API_BASE_URL}/api/{{scene_name}}' 25 | 26 | # API请求超时时间(秒) 27 | API_TIMEOUT = 10 28 | 29 | # API CONFIGURATION ------------------------------------------------------------------------ 30 | 31 | # CONFIGURATION ------------------------------------------------------------------------ 32 | 33 | # 意图相关性判断阈值0-1(已废弃,保留用于兼容性) 34 | RELATED_INTENT_THRESHOLD = 0.5 35 | 36 | # 聊天记录数量(发送给LLM的历史消息条数) 37 | CHAT_HISTORY_COUNT = 3 38 | 39 | # 无场景识别的默认响应 40 | NO_SCENE_RESPONSE = "您好,请问您需要办理什么业务?" 41 | 42 | # API结果处理提示词 43 | API_RESULT_PROMPT = "以下是查询的结果,请向用户解释,禁止使用markdown:\n\n{api_result}:" 44 | 45 | # CONFIGURATION ------------------------------------------------------------------------ 46 | -------------------------------------------------------------------------------- /frontend/src/components/SlotFiller.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Form, Input, Button } from 'antd'; 3 | 4 | const SlotFiller = ({ slotInfo, pendingSlots, onSubmit }) => { 5 | const [form] = Form.useForm(); 6 | 7 | // 防御性处理 8 | if (!slotInfo || !slotInfo.slots) return null; 9 | 10 | useEffect(() => { 11 | if (pendingSlots && slotInfo && slotInfo.slots) { 12 | const defaults = {}; 13 | Object.entries(slotInfo.slots) 14 | .filter(([key]) => pendingSlots.includes(key)) 15 | .forEach(([key, slot]) => { 16 | defaults[key] = slot.example; 17 | }); 18 | form.resetFields(); 19 | form.setFieldsValue(defaults); 20 | } 21 | }, [pendingSlots, slotInfo, form]); 22 | 23 | const handleFinish = values => { 24 | if (onSubmit) onSubmit(values); 25 | }; 26 | 27 | return ( 28 |
29 | {Object.entries(slotInfo.slots) 30 | .filter(([key]) => pendingSlots.includes(key)) 31 | .map(([key, slot]) => ( 32 | 38 | 39 | 40 | ))} 41 | 42 | 43 | 44 |
45 | ); 46 | }; 47 | 48 | export default SlotFiller; -------------------------------------------------------------------------------- /frontend/src/components/MessageBubble.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function MessageBubble({ message, msg, isStreaming }) { 4 | // 兼容不同的参数名 5 | const msgData = message || msg; 6 | const isAI = msgData.role === 'ai'; 7 | 8 | return ( 9 |
14 |
23 |
24 | {isAI ? 'AI助手' : '你'} 25 |
26 |
27 | {msgData.text} 28 | {isStreaming && ( 29 | 38 | )} 39 |
40 | {msgData.status && ( 41 |
47 | {msgData.status} 48 |
49 | )} 50 |
51 | 58 |
59 | ); 60 | } -------------------------------------------------------------------------------- /start_dev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 >nul 3 | title CRM助手 - 开发环境启动 4 | 5 | echo 🚀 CRM助手开发环境启动中... 6 | 7 | :: 检查是否存在.env文件 8 | if not exist ".env" ( 9 | echo ⚠️ 未找到.env文件,从.env.example复制... 10 | if exist ".env.example" ( 11 | copy ".env.example" ".env" >nul 12 | echo ✅ 已创建.env文件 13 | ) else ( 14 | echo ❌ 未找到.env.example文件,请手动创建.env文件 15 | pause 16 | exit /b 1 17 | ) 18 | ) 19 | 20 | :: 检查前端.env文件 21 | if not exist "frontend\.env" ( 22 | echo ⚠️ 未找到前端.env文件,从前端.env.example复制... 23 | if exist "frontend\.env.example" ( 24 | copy "frontend\.env.example" "frontend\.env" >nul 25 | echo ✅ 已创建前端.env文件 26 | ) else ( 27 | echo ❌ 未找到前端.env.example文件,请手动创建前端.env文件 28 | pause 29 | exit /b 1 30 | ) 31 | ) 32 | 33 | :: 检查Python虚拟环境 34 | if not exist "venv" ( 35 | echo 📦 创建Python虚拟环境... 36 | python -m venv venv 37 | if errorlevel 1 ( 38 | echo ❌ 创建虚拟环境失败,请检查Python安装 39 | pause 40 | exit /b 1 41 | ) 42 | ) 43 | 44 | :: 激活虚拟环境 45 | echo 🔧 激活Python虚拟环境... 46 | call venv\Scripts\activate.bat 47 | 48 | :: 安装Python依赖 49 | echo 📋 安装Python依赖... 50 | pip install -r requirements.txt 51 | if errorlevel 1 ( 52 | echo ❌ 安装Python依赖失败 53 | pause 54 | exit /b 1 55 | ) 56 | 57 | :: 检查Node.js依赖 58 | if not exist "frontend\node_modules" ( 59 | echo 📦 安装前端依赖... 60 | cd frontend 61 | npm install 62 | if errorlevel 1 ( 63 | echo ❌ 安装前端依赖失败 64 | pause 65 | exit /b 1 66 | ) 67 | cd .. 68 | ) 69 | 70 | echo. 71 | echo 🎯 准备启动服务... 72 | echo 后端地址: http://localhost:5050 73 | echo 前端地址: http://localhost:3000 74 | echo API文档: http://localhost:5050/api/health 75 | echo. 76 | echo 💡 提示: 关闭窗口以停止服务 77 | echo. 78 | 79 | :: 启动后端服务(新窗口) 80 | start "后端服务 - CRM助手" cmd /k "cd /d %cd% && call venv\Scripts\activate.bat && python app.py" 81 | 82 | :: 等待后端启动 83 | timeout /t 3 /nobreak >nul 84 | 85 | :: 启动前端服务(新窗口) 86 | start "前端服务 - CRM助手" cmd /k "cd /d %cd%\frontend && npm start" 87 | 88 | echo ✅ 启动完成! 89 | echo 💻 后端和前端服务已在新窗口中启动 90 | echo 🌐 请在浏览器中访问: http://localhost:3000 91 | echo. 92 | 93 | pause -------------------------------------------------------------------------------- /快速开始.md: -------------------------------------------------------------------------------- 1 | # 🚀 CRM助手 - 快速开始指南 2 | 3 | ## 📦 一键启动(推荐) 4 | 5 | ### macOS/Linux 6 | ```bash 7 | ./start_dev.sh 8 | ``` 9 | 10 | ### Windows 11 | ```cmd 12 | start_dev.cmd 13 | ``` 14 | 15 | ## 🌐 访问地址 16 | 17 | | 服务 | 地址 | 说明 | 18 | |------|------|------| 19 | | 前端界面 | http://localhost:3000 | React应用 | 20 | | 后端API | http://localhost:5050 | Flask服务 | 21 | | 健康检查 | http://localhost:5050/api/health | 后端状态 | 22 | 23 | ## 🧪 验证联调 24 | 25 | 启动后端后,测试联调状态: 26 | ```bash 27 | source venv/bin/activate 28 | python test_connection.py 29 | ``` 30 | 31 | ## 📁 项目结构 32 | 33 | ``` 34 | crm_assistant/ 35 | ├── app.py # 后端主服务 36 | ├── config.py # 配置管理 37 | ├── .env # 后端环境变量 38 | ├── .env.example # 后端环境模板 39 | ├── requirements.txt # Python依赖 40 | ├── start_dev.sh # 一键启动(macOS/Linux) 41 | ├── start_dev.cmd # 一键启动(Windows) 42 | ├── start_backend.sh # 后端启动脚本 43 | ├── start_frontend.sh # 前端启动脚本 44 | ├── test_connection.py # 联调测试脚本 45 | ├── frontend/ 46 | │ ├── src/ 47 | │ │ ├── config/config.js # 前端配置 48 | │ │ └── api/aiApi.js # API接口 49 | │ ├── .env # 前端环境变量 50 | │ ├── .env.example # 前端环境模板 51 | │ └── package.json # 前端依赖 52 | └── 前后端联调说明.md # 详细文档 53 | ``` 54 | 55 | ## ⚙️ 主要配置 56 | 57 | ### 后端配置 (.env) 58 | ```env 59 | BACKEND_PORT=5050 60 | FRONTEND_PORT=3000 61 | CORS_ORIGINS=http://localhost:3000 62 | ``` 63 | 64 | ### 前端配置 (frontend/.env) 65 | ```env 66 | REACT_APP_API_BASE_URL=http://localhost:5050 67 | REACT_APP_DEBUG=true 68 | ``` 69 | 70 | ## 🔧 常见问题 71 | 72 | **Q: 端口被占用?** 73 | ```bash 74 | # 查看占用 75 | lsof -i :5050 76 | # 杀掉进程 77 | kill -9 PID 78 | ``` 79 | 80 | **Q: 跨域错误?** 81 | - 检查CORS_ORIGINS配置 82 | - 确认前后端地址一致 83 | 84 | **Q: 依赖安装失败?** 85 | ```bash 86 | # Python依赖 87 | pip install -r requirements.txt 88 | 89 | # 前端依赖 90 | cd frontend && npm install 91 | ``` 92 | 93 | ## 📞 获取帮助 94 | 95 | 1. 查看 [前后端联调说明.md](./前后端联调说明.md) 96 | 2. 运行测试脚本 `python test_connection.py` 97 | 3. 查看控制台错误信息 98 | 4. 联系开发团队 99 | 100 | --- 101 | 102 | **�� 现在您可以开始使用CRM助手了!** -------------------------------------------------------------------------------- /start_dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # CRM助手 - 开发环境一键启动脚本 4 | # 支持macOS和Linux系统 5 | 6 | echo "🚀 CRM助手开发环境启动中..." 7 | 8 | # 检查是否存在.env文件 9 | if [ ! -f ".env" ]; then 10 | echo "⚠️ 未找到.env文件,从.env.example复制..." 11 | if [ -f ".env.example" ]; then 12 | cp .env.example .env 13 | echo "✅ 已创建.env文件" 14 | else 15 | echo "❌ 未找到.env.example文件,请手动创建.env文件" 16 | exit 1 17 | fi 18 | fi 19 | 20 | # 检查前端.env文件 21 | if [ ! -f "frontend/.env" ]; then 22 | echo "⚠️ 未找到前端.env文件,从前端.env.example复制..." 23 | if [ -f "frontend/.env.example" ]; then 24 | cp frontend/.env.example frontend/.env 25 | echo "✅ 已创建前端.env文件" 26 | else 27 | echo "❌ 未找到前端.env.example文件,请手动创建前端.env文件" 28 | exit 1 29 | fi 30 | fi 31 | 32 | # 检查Python虚拟环境 33 | if [ ! -d "venv" ]; then 34 | echo "📦 创建Python虚拟环境..." 35 | python3 -m venv venv 36 | fi 37 | 38 | # 激活虚拟环境 39 | echo "🔧 激活Python虚拟环境..." 40 | source venv/bin/activate 41 | 42 | # 安装Python依赖 43 | echo "📋 安装Python依赖..." 44 | pip install -r requirements.txt 45 | 46 | # 检查Node.js依赖 47 | if [ ! -d "frontend/node_modules" ]; then 48 | echo "📦 安装前端依赖..." 49 | cd frontend 50 | npm install 51 | cd .. 52 | fi 53 | 54 | # 创建启动函数 55 | start_backend() { 56 | echo "🖥️ 启动后端服务 (端口5050)..." 57 | source venv/bin/activate 58 | python app.py 59 | } 60 | 61 | start_frontend() { 62 | echo "🌐 启动前端服务 (端口3000)..." 63 | cd frontend 64 | npm start 65 | } 66 | 67 | # 检查端口是否被占用 68 | check_port() { 69 | if lsof -Pi :$1 -sTCP:LISTEN -t >/dev/null ; then 70 | echo "⚠️ 端口 $1 已被占用,请先关闭占用该端口的程序" 71 | return 1 72 | fi 73 | return 0 74 | } 75 | 76 | # 检查必要端口 77 | if ! check_port 5050; then 78 | echo "❌ 后端端口5050被占用" 79 | exit 1 80 | fi 81 | 82 | if ! check_port 3000; then 83 | echo "❌ 前端端口3000被占用" 84 | exit 1 85 | fi 86 | 87 | echo "" 88 | echo "🎯 准备启动服务..." 89 | echo " 后端地址: http://localhost:5050" 90 | echo " 前端地址: http://localhost:3000" 91 | echo " API文档: http://localhost:5050/api/health" 92 | echo "" 93 | echo "💡 提示: 使用 Ctrl+C 停止服务" 94 | echo "" 95 | 96 | # 并行启动前后端 97 | if command -v gnome-terminal >/dev/null; then 98 | # Linux with gnome-terminal 99 | gnome-terminal --tab --title="后端服务" -- bash -c "$(declare -f start_backend); start_backend; exec bash" 100 | gnome-terminal --tab --title="前端服务" -- bash -c "$(declare -f start_frontend); start_frontend; exec bash" 101 | elif command -v osascript >/dev/null; then 102 | # macOS 103 | osascript -e 'tell app "Terminal" to do script "cd '"$(pwd)"' && source venv/bin/activate && python app.py"' 104 | osascript -e 'tell app "Terminal" to do script "cd '"$(pwd)/frontend"' && npm start"' 105 | else 106 | # 无图形界面,顺序启动 107 | echo "🔄 后台启动后端服务..." 108 | start_backend & 109 | BACKEND_PID=$! 110 | 111 | sleep 3 112 | echo "🔄 启动前端服务..." 113 | start_frontend 114 | 115 | # 清理函数 116 | cleanup() { 117 | echo "" 118 | echo "🛑 停止服务..." 119 | kill $BACKEND_PID 2>/dev/null 120 | exit 0 121 | } 122 | 123 | trap cleanup SIGINT SIGTERM 124 | wait 125 | fi 126 | 127 | echo "✅ 启动完成!" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # IntelliQ 3 | ## 介绍 4 | IntelliQ 是一个开源项目,旨在提供一个基于大型语言模型(LLM)的多轮问答系统。该系统结合了先进的意图识别和词槽填充(Slot Filling)技术,致力于提升对话系统的理解深度和响应精确度。本项目为开发者社区提供了一个灵活、高效的解决方案,用于构建和优化各类对话型应用。 5 | 6 | 7 | 8 | ## 工作流程 9 | 10 | 11 | ## 特性 12 | 1. **多轮对话管理**:能够处理复杂的对话场景,支持连续多轮交互。 13 | 2. **意图识别**:准确判定用户输入的意图,支持自定义意图扩展。 14 | 3. **词槽填充**:动态识别并填充关键信息(如时间、地点、对象等)。 15 | 4. **接口槽技术**:直接与外部APIs对接,实现数据的实时获取和处理。 16 | 5. **前后端分离**:React前端 + Flask后端,支持现代化Web开发模式。 17 | 6. **流式AI聊天**:支持Server-Sent Events (SSE),实现实时聊天体验。 18 | 7. **跨域支持**:内置CORS配置,支持本地开发和生产部署。 19 | 8. **环境配置**:灵活的环境变量配置,支持开发/生产环境切换。 20 | 9. **一键启动**:提供跨平台启动脚本,团队成员无需手动配置。 21 | 10. **易于集成**:提供了详细的API文档,支持多种编程语言和平台集成。 22 | 23 | ## 安装和使用 24 | 25 | ### 🌏环境要求 26 | - Python 3.8+ 27 | - Node.js 16+ (包含npm包管理器) 28 | - npm 7+ (随Node.js自动安装) 29 | 30 | > **注意**: npm是Node.js的包管理器,安装Node.js时会自动包含npm。如果您还没有安装Node.js,请访问 [Node.js官网](https://nodejs.org/) 下载安装。 31 | 32 | ### 🔧修改配置 33 | 配置项在 config/__init__.py 34 | * GPT_URL: AI平台的URL 35 | * API_KEY: 修改为自己的API密钥 36 | * API_BASE_URL: 修改为查询/办理的接口 37 | 38 | ### 📋 安装步骤 39 | 40 | 确保您已安装 git、python3、node.js。然后执行以下步骤: 41 | 42 | **1. 克隆代码** 43 | ```bash 44 | git clone https://github.com/answerlink/IntelliQ.git 45 | cd IntelliQ 46 | ``` 47 | 48 | **2. 后端配置** 49 | ```bash 50 | # 创建Python虚拟环境 51 | python3 -m venv venv 52 | source venv/bin/activate # Linux/macOS 53 | # venv\Scripts\activate # Windows 54 | 55 | # 安装Python依赖 56 | pip install -r requirements.txt 57 | ``` 58 | 59 | **3. 前端配置** 60 | ```bash 61 | cd frontend 62 | npm install 63 | cd .. 64 | ``` 65 | 66 | **4. 环境配置** 67 | - 复制 `.env.example` 为 `.env` 并根据需要修改配置 68 | - 复制 `frontend/.env.example` 为 `frontend/.env` 并根据需要修改配置 69 | 70 | **5. 启动服务** 71 | 72 | 启动后端服务: 73 | ```bash 74 | python app.py 75 | ``` 76 | 77 | 启动前端服务(新开一个终端窗口): 78 | ```bash 79 | cd frontend && npm start 80 | ``` 81 | 82 | **访问地址:** 83 | - 前端界面:http://localhost:3000 84 | - 后端API:http://localhost:5050 85 | - API健康检查:http://localhost:5050/api/health 86 | 87 | ### 🔗 API接口 88 | 89 | #### 健康检查 90 | ``` 91 | GET /api/health 92 | ``` 93 | 94 | #### 流式AI聊天 95 | ``` 96 | POST /api/llm_chat 97 | Content-Type: application/json 98 | Accept: text/event-stream 99 | 100 | { 101 | "messages": [], 102 | "user_input": "用户输入", 103 | "session_id": "可选的会话ID" 104 | } 105 | ``` 106 | 107 | #### 获取模拟槽位 108 | ``` 109 | GET /api/mock_slots 110 | ``` 111 | 112 | #### 重置会话 113 | ``` 114 | POST /api/reset_session 115 | { 116 | "session_id": "会话ID" 117 | } 118 | ``` 119 | 120 | 更多详细信息请查看 [前后端联调说明](./前后端联调说明.md)。 121 | 122 | ## 贡献 123 | 124 | 非常欢迎和鼓励社区贡献。如果您想贡献代码,请遵循以下步骤: 125 | 126 | Fork 仓库 127 | 创建新的特性分支 (git checkout -b feature/AmazingFeature) 128 | 提交更改 (git commit -m 'Add some AmazingFeature') 129 | 推送到分支 (git push origin feature/AmazingFeature) 130 | 开启Pull Request 131 | 132 | 查看 [CONTRIBUTING.md](https://github.com/answerlink/IntelliQ/blob/main/CONTRIBUTING.md) 了解更多信息。 133 | 134 | ### All Thanks To Our Contributors: 135 | 136 | 137 | 138 | 139 | ## License 140 | 141 | **Apache License, Version 2.0** 142 | 143 | ## 版本更新 144 | v2.0 2025-7-17 实现真实的API的对接;优化流程;升级前端 145 | 146 | v1.3 2024-1-15 集成通义千问线上模型 147 | 148 | v1.2 2023-12-24 支持Qwen私有化模型 149 | 150 | v1.1 2023-12-21 改造通用场景处理器;完成高度抽象封装;提示词调优 151 | 152 | v1.0 2023-12-17 首次可用更新;框架完成 153 | 154 | v0.1 2023-11-23 首次更新;流程设计 155 | -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Layout, Tabs, Button, Space } from 'antd'; 3 | import { PlusOutlined, RobotOutlined, MessageOutlined } from '@ant-design/icons'; 4 | import ChatBox from './components/ChatBox'; 5 | import AIChatBox from './components/AIChatBox'; 6 | import './App.css'; 7 | 8 | const { Header, Content } = Layout; 9 | 10 | function createNewChat(id, type = 'business') { 11 | return { 12 | id, 13 | title: type === 'business' ? `业务对话${id}` : `AI对话${id}`, 14 | type: type, 15 | messages: [], 16 | participants: { user: '你', ai: type === 'business' ? '业务助手' : 'AI助手' }, 17 | }; 18 | } 19 | 20 | export default function App() { 21 | const [chats, setChats] = useState([ 22 | createNewChat(1, 'business'), 23 | createNewChat(2, 'ai') 24 | ]); 25 | const [activeKey, setActiveKey] = useState('1'); 26 | const [nextId, setNextId] = useState(3); 27 | 28 | const addChat = (type = 'business') => { 29 | const id = String(nextId); 30 | setChats([...chats, createNewChat(id, type)]); 31 | setActiveKey(id); 32 | setNextId(nextId + 1); 33 | }; 34 | 35 | const removeChat = (targetKey) => { 36 | let newActiveKey = activeKey; 37 | let lastIndex = -1; 38 | chats.forEach((chat, i) => { 39 | if (chat.id === targetKey) lastIndex = i - 1; 40 | }); 41 | const newChats = chats.filter(chat => chat.id !== targetKey); 42 | if (newChats.length && newActiveKey === targetKey) { 43 | newActiveKey = newChats[lastIndex >= 0 ? lastIndex : 0].id; 44 | } 45 | setChats(newChats); 46 | setActiveKey(newActiveKey); 47 | }; 48 | 49 | const updateChat = (id, updater) => { 50 | setChats(chats => chats.map(chat => chat.id === id ? updater(chat) : chat)); 51 | }; 52 | 53 | const renderChatComponent = (chat) => { 54 | if (chat.type === 'ai') { 55 | return ( 56 | updateChat(chat.id, updater)} 59 | /> 60 | ); 61 | } else { 62 | return ( 63 | updateChat(chat.id, updater)} 66 | /> 67 | ); 68 | } 69 | }; 70 | 71 | return ( 72 | 73 |
74 |
电信业务AI问答系统
75 | 76 | 83 | 90 | 91 |
92 | 93 | ({ 100 | key: chat.id, 101 | label: ( 102 | 103 | {chat.type === 'business' ? : } 104 | {' '}{chat.title} 105 | 106 | ), 107 | children: renderChatComponent(chat), 108 | }))} 109 | /> 110 | 111 |
112 | ); 113 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .idea 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # poetry 99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 100 | # This is especially recommended for binary packages to ensure reproducibility, and is more 101 | # commonly ignored for libraries. 102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 103 | #poetry.lock 104 | 105 | # pdm 106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 107 | #pdm.lock 108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 109 | # in version control. 110 | # https://pdm.fming.dev/#use-with-ide 111 | .pdm.toml 112 | 113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 114 | __pypackages__/ 115 | 116 | # Celery stuff 117 | celerybeat-schedule 118 | celerybeat.pid 119 | 120 | # SageMath parsed files 121 | *.sage.py 122 | 123 | # Environments 124 | .env 125 | .venv 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | # PyCharm 157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 159 | # and can be added to the global gitignore or merged into this file. For a more nuclear 160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 161 | #.idea/ 162 | 163 | frontend/node_modules/ 164 | 165 | .DS_Store -------------------------------------------------------------------------------- /scene_processor/impl/common_processor.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | import logging 3 | 4 | from scene_config import scene_prompts 5 | from scene_processor.scene_processor import SceneProcessor 6 | from utils.helpers import get_raw_slot, update_slot, format_name_value_for_logging, is_slot_fully_filled, send_message, \ 7 | extract_json_from_string, get_dynamic_example, call_scene_api, process_api_result 8 | from utils.prompt_utils import get_slot_update_message, get_slot_query_user_message 9 | 10 | 11 | class CommonProcessor(SceneProcessor): 12 | def __init__(self, scene_config): 13 | parameters = scene_config["parameters"] 14 | self.scene_config = scene_config 15 | self.scene_name = scene_config["name"] 16 | self.slot_template = get_raw_slot(parameters) 17 | self.slot_dynamic_example = get_dynamic_example(scene_config) 18 | self.slot = get_raw_slot(parameters) 19 | self.scene_prompts = scene_prompts 20 | 21 | def process(self, user_input, context): 22 | # 处理用户输入,更新槽位,检查完整性,以及与用户交互 23 | # 先检查本次用户输入是否有信息补充,保存补充后的结果 编写程序进行字符串value值diff对比,判断是否有更新 24 | message = get_slot_update_message(self.scene_name, self.slot_dynamic_example, self.slot_template, user_input) # 优化封装一下 .format 入参只要填input 25 | new_info_json_raw = send_message(message, user_input, context) # 传递聊天记录 26 | current_values = extract_json_from_string(new_info_json_raw) 27 | # 新增:如果current_values为dict,转为[{name:..., value:...}] 28 | if current_values and isinstance(current_values[0], dict) and not ('name' in current_values[0] and 'value' in current_values[0]): 29 | # 说明是扁平结构 30 | flat = current_values[0] 31 | current_values = [{"name": k, "value": v} for k, v in flat.items()] 32 | logging.debug('current_values: %s', current_values) 33 | logging.debug('slot update before: %s', self.slot) 34 | update_slot(current_values, self.slot) 35 | logging.debug('slot update after: %s', self.slot) 36 | # 判断参数是否已经全部补全 37 | if is_slot_fully_filled(self.slot): 38 | return self.respond_with_complete_data(context) 39 | else: 40 | return self.ask_user_for_missing_data(user_input, context) 41 | 42 | def respond_with_complete_data(self, context): 43 | # 当所有数据都准备好后的响应 44 | logging.debug(f'%s ------ 参数已完整,详细参数如下', self.scene_name) 45 | logging.debug(format_name_value_for_logging(self.slot)) 46 | logging.debug(f'正在请求%sAPI,请稍后……', self.scene_name) 47 | 48 | # 获取场景的真实名称(从场景配置中获取) 49 | scene_key = self._get_scene_key() 50 | if not scene_key: 51 | return f"抱歉,无法找到场景 '{self.scene_name}' 的配置信息。" 52 | 53 | # 准备槽位数据,使用英文键名 54 | slots_data = {} 55 | for slot in self.slot: 56 | if slot['value']: # 只包含有值的槽位 57 | # 从场景配置中获取对应的英文键名 58 | slot_key = self._get_slot_key(slot['name']) 59 | if slot_key: 60 | slots_data[slot_key] = slot['value'] 61 | 62 | # 调用场景API 63 | api_result = call_scene_api(scene_key, slots_data) 64 | 65 | # 处理API结果 66 | if "error" in api_result: 67 | return f"抱歉,调用API时出现错误:{api_result['error']}" 68 | 69 | # 通过AI处理API结果,生成用户友好的回复 70 | user_friendly_response = process_api_result(api_result, context) 71 | 72 | return user_friendly_response 73 | 74 | def ask_user_for_missing_data(self, user_input, context): 75 | message = get_slot_query_user_message(self.scene_name, self.slot, user_input) 76 | # 请求用户填写缺失的数据,传递聊天记录 77 | result = send_message(message, user_input, context) 78 | return result 79 | 80 | def _get_scene_key(self): 81 | """ 82 | 根据场景配置获取场景的英文键名 83 | """ 84 | # 直接从scene_config中获取scene_name字段 85 | return self.scene_config.get('scene_name') 86 | 87 | def _get_slot_key(self, slot_name): 88 | """ 89 | 直接使用参数的name字段 90 | """ 91 | # 查找对应的参数配置 92 | for param in self.scene_config.get("parameters", []): 93 | if param.get("name") == slot_name: 94 | return param.get("name") 95 | 96 | # 如果找不到配置,直接返回原名称 97 | return slot_name 98 | -------------------------------------------------------------------------------- /前后端联调说明.md: -------------------------------------------------------------------------------- 1 | # CRM助手 - 前后端联调说明 2 | 3 | ## 📋 目录 4 | 1. [快速开始](#快速开始) 5 | 2. [环境要求](#环境要求) 6 | 3. [配置说明](#配置说明) 7 | 4. [启动方式](#启动方式) 8 | 5. [API接口说明](#api接口说明) 9 | 6. [常见问题](#常见问题) 10 | 7. [开发调试](#开发调试) 11 | 12 | ## 🚀 快速开始 13 | 14 | ### 一键启动(推荐) 15 | 16 | **macOS/Linux:** 17 | ```bash 18 | ./start_dev.sh 19 | ``` 20 | 21 | **Windows:** 22 | ```cmd 23 | start_dev.cmd 24 | ``` 25 | 26 | ### 分别启动 27 | 28 | **启动后端:** 29 | ```bash 30 | ./start_backend.sh # macOS/Linux 31 | ``` 32 | 33 | **启动前端:** 34 | ```bash 35 | ./start_frontend.sh # macOS/Linux 36 | ``` 37 | 38 | ## 💻 环境要求 39 | 40 | ### 后端要求 41 | - Python 3.8+ 42 | - pip 43 | 44 | ### 前端要求 45 | - Node.js 16+ 46 | - npm 7+ 47 | 48 | ## ⚙️ 配置说明 49 | 50 | ### 后端配置 (.env) 51 | ```env 52 | # 后端配置 53 | FLASK_ENV=development # 运行环境 54 | FLASK_DEBUG=True # 调试模式 55 | BACKEND_HOST=localhost # 后端主机 56 | BACKEND_PORT=5050 # 后端端口 57 | 58 | # 前端配置 59 | FRONTEND_HOST=localhost # 前端主机 60 | FRONTEND_PORT=3000 # 前端端口 61 | 62 | # API配置 63 | API_PREFIX=/api # API路径前缀 64 | 65 | # CORS配置 66 | CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 67 | 68 | # 其他配置 69 | LOG_LEVEL=DEBUG # 日志级别 70 | ``` 71 | 72 | ### 前端配置 (frontend/.env) 73 | ```env 74 | # 前端环境变量配置 75 | REACT_APP_API_BASE_URL=http://localhost:5050 # 后端API地址 76 | REACT_APP_ENV=development # 环境标识 77 | REACT_APP_DEBUG=true # 调试模式 78 | ``` 79 | 80 | ## 🎯 启动方式 81 | 82 | ### 方式1:一键启动(推荐) 83 | 自动检查依赖、创建虚拟环境、安装包,并并行启动前后端服务。 84 | 85 | ### 方式2:手动启动 86 | 87 | **步骤1:准备后端环境** 88 | ```bash 89 | # 创建Python虚拟环境 90 | python3 -m venv venv 91 | 92 | # 激活虚拟环境 93 | source venv/bin/activate # macOS/Linux 94 | # 或 95 | venv\Scripts\activate # Windows 96 | 97 | # 安装Python依赖 98 | pip install -r requirements.txt 99 | ``` 100 | 101 | **步骤2:准备前端环境** 102 | ```bash 103 | cd frontend 104 | npm install 105 | ``` 106 | 107 | **步骤3:启动服务** 108 | ```bash 109 | # 终端1:启动后端 110 | python app.py 111 | 112 | # 终端2:启动前端 113 | cd frontend 114 | npm start 115 | ``` 116 | 117 | ## 🔗 API接口说明 118 | 119 | ### 服务地址 120 | - **后端服务:** http://localhost:5050 121 | - **前端服务:** http://localhost:3000 122 | - **健康检查:** http://localhost:5050/api/health 123 | 124 | ### 主要接口 125 | 126 | #### 1. 健康检查 127 | ``` 128 | GET /api/health 129 | ``` 130 | 131 | #### 2. 流式AI聊天 132 | ``` 133 | POST /api/llm_chat 134 | Content-Type: application/json 135 | Accept: text/event-stream 136 | 137 | { 138 | "messages": [], 139 | "user_input": "用户输入", 140 | "session_id": "可选的会话ID" 141 | } 142 | ``` 143 | 144 | #### 3. 普通AI聊天 145 | ``` 146 | POST /api/llm_chat 147 | Content-Type: application/json 148 | 149 | { 150 | "messages": [], 151 | "user_input": "用户输入", 152 | "session_id": "可选的会话ID" 153 | } 154 | ``` 155 | 156 | #### 4. 获取模拟槽位 157 | ``` 158 | GET /api/mock_slots 159 | ``` 160 | 161 | #### 5. 重置会话 162 | ``` 163 | POST /api/reset_session 164 | 165 | { 166 | "session_id": "会话ID" 167 | } 168 | ``` 169 | 170 | #### 6. 多轮问答(兼容接口) 171 | ``` 172 | POST /multi_question 173 | 174 | { 175 | "question": "用户问题" 176 | } 177 | ``` 178 | 179 | ## 🔧 常见问题 180 | 181 | ### Q1: 端口被占用 182 | **问题:** 启动时提示端口5050或3000被占用 183 | 184 | **解决:** 185 | ```bash 186 | # 查看端口占用 187 | lsof -i :5050 # macOS/Linux 188 | netstat -ano | findstr :5050 # Windows 189 | 190 | # 杀掉占用进程 191 | kill -9 PID # macOS/Linux 192 | taskkill /PID PID /F # Windows 193 | ``` 194 | 195 | ### Q2: 前端无法访问后端API 196 | **问题:** 前端请求显示跨域错误或连接失败 197 | 198 | **解决:** 199 | 1. 确认后端服务已启动 200 | 2. 检查防火墙设置 201 | 3. 验证前端配置的API地址 202 | 4. 查看浏览器网络面板的错误信息 203 | 204 | ### Q3: Python虚拟环境问题 205 | **问题:** 无法创建或激活虚拟环境 206 | 207 | **解决:** 208 | ```bash 209 | # 使用不同的Python版本 210 | python -m venv venv 211 | python3 -m venv venv 212 | python3.8 -m venv venv 213 | 214 | # 删除重建 215 | rm -rf venv 216 | python3 -m venv venv 217 | ``` 218 | 219 | ### Q4: 前端依赖安装失败 220 | **问题:** npm install 失败 221 | 222 | **解决:** 223 | ```bash 224 | # 清除缓存 225 | npm cache clean --force 226 | 227 | # 删除node_modules重新安装 228 | rm -rf node_modules package-lock.json 229 | npm install 230 | 231 | # 或使用yarn 232 | yarn install 233 | ``` 234 | 235 | ## 🐛 开发调试 236 | 237 | ### 后端调试 238 | 1. 查看控制台日志输出 239 | 2. 检查 `LOG_LEVEL=DEBUG` 配置 240 | 3. 使用Postman测试API接口 241 | 242 | ### 前端调试 243 | 1. 打开浏览器开发者工具 244 | 2. 查看Console面板的日志 245 | 3. 查看Network面板的请求状态 246 | 4. 检查 `REACT_APP_DEBUG=true` 配置 247 | 248 | ### 联调调试 249 | 1. 使用健康检查接口验证后端状态 250 | 2. 在前端调用健康检查接口验证连通性 251 | 3. 查看前后端控制台的请求/响应日志 252 | 253 | ## 📝 环境变量说明 254 | 255 | ### 生产环境配置示例 256 | ```env 257 | # .env (后端) 258 | FLASK_ENV=production 259 | FLASK_DEBUG=False 260 | BACKEND_HOST=0.0.0.0 261 | BACKEND_PORT=5050 262 | CORS_ORIGINS=https://yourdomain.com 263 | LOG_LEVEL=INFO 264 | ``` 265 | 266 | ```env 267 | # frontend/.env (前端) 268 | REACT_APP_API_BASE_URL=https://api.yourdomain.com 269 | REACT_APP_ENV=production 270 | REACT_APP_DEBUG=false 271 | ``` 272 | 273 | ## 🎉 完成 274 | 275 | 按照以上步骤操作,你应该能够: 276 | 1. ✅ 前后端服务正常启动 277 | 2. ✅ 前端能够访问后端API 278 | 3. ✅ 跨域问题已解决 279 | 4. ✅ 支持环境配置切换 280 | 5. ✅ 团队成员一键启动开发环境 281 | 282 | 如有其他问题,请查看控制台错误信息或联系开发团队。 -------------------------------------------------------------------------------- /demo/user_input.html.bak: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 智能问答对话框 7 | 89 | 90 | 91 |
92 | 93 |
94 |
95 | 96 | 97 |
98 |
99 | 100 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /frontend/src/utils/intentMap.js: -------------------------------------------------------------------------------- 1 | // frontend/src/utils/intentMap.js 2 | export const intentMap = [ 3 | { 4 | keywords: ['流量', '流量查询', '流量剩余', '查流量'], 5 | api: '/api/traffic_query', 6 | businessKey: 'traffic_query', 7 | slots: ['phone', 'month'] 8 | }, 9 | { 10 | keywords: ['套餐', '套餐变更', '5G套餐', '换套餐'], 11 | api: '/api/package_change', 12 | businessKey: 'package_change', 13 | slots: ['phone', 'package_type', 'effective_time'] 14 | }, 15 | { 16 | keywords: ['副卡', '副卡办理', '办副卡', '加副卡'], 17 | api: '/api/sub_card_apply', 18 | businessKey: 'sub_card_apply', 19 | slots: ['main_phone', 'sub_card_count', 'sub_package_type'] 20 | }, 21 | { 22 | keywords: ['宽带', '宽带报修', '宽带故障', '修宽带'], 23 | api: '/api/broadband_repair', 24 | businessKey: 'broadband_repair', 25 | slots: ['phone', 'broadband_account', 'address', 'fault_type'] 26 | }, 27 | { 28 | keywords: ['账单', '话费账单', '查账单'], 29 | api: '/api/account_bill', 30 | businessKey: 'account_bill', 31 | slots: ['phone', 'bill_period'] 32 | }, 33 | { 34 | keywords: ['实名认证', '实名', '查实名'], 35 | api: '/api/realname_query', 36 | businessKey: 'realname_query', 37 | slots: ['phone', 'id_number'] 38 | }, 39 | { 40 | keywords: ['积分兑换', '兑换积分', '积分换', '积分换话费', '兑换'], 41 | api: '/api/points_exchange', 42 | businessKey: 'points_exchange', 43 | slots: ['phone', 'exchange_type', 'exchange_quantity'] 44 | }, 45 | { 46 | keywords: ['积分查询', '积分'], 47 | api: '/api/points_query', 48 | businessKey: 'points_query', 49 | slots: ['phone'] 50 | }, 51 | { 52 | keywords: ['停机', '停机保号'], 53 | api: '/api/suspend_apply', 54 | businessKey: 'suspend_apply', 55 | slots: ['phone', 'suspend_duration'] 56 | }, 57 | { 58 | keywords: ['发票', '发票申请'], 59 | api: '/api/invoice_apply', 60 | businessKey: 'invoice_apply', 61 | slots: ['phone', 'invoice_type', 'mailing_address'] 62 | }, 63 | { 64 | keywords: ['投诉'], 65 | api: '/api/complaint', 66 | businessKey: 'complaint', 67 | slots: ['phone', 'complaint_type', 'complaint_content'] 68 | }, 69 | { 70 | keywords: ['客服', '在线客服'], 71 | api: '/api/customer_service', 72 | businessKey: 'customer_service', 73 | slots: ['phone', 'ticket_type', 'description'] 74 | }, 75 | { 76 | keywords: ['套餐余量', '余量', '查余量'], 77 | api: '/api/package_balance', 78 | businessKey: 'package_balance', 79 | slots: ['phone'] 80 | }, 81 | { 82 | keywords: ['地址修改', '通信地址', '改地址'], 83 | api: '/api/address_modify', 84 | businessKey: 'address_modify', 85 | slots: ['phone', 'new_address'] 86 | }, 87 | { 88 | keywords: ['实名状态', '实名查询'], 89 | api: '/api/realname_status', 90 | businessKey: 'realname_status', 91 | slots: ['phone'] 92 | }, 93 | { 94 | keywords: ['充值', '缴费'], 95 | api: '/api/recharge', 96 | businessKey: 'recharge', 97 | slots: ['phone', 'amount', 'payment_method'] 98 | }, 99 | { 100 | keywords: ['过户', '号码过户'], 101 | api: '/api/number_transfer', 102 | businessKey: 'number_transfer', 103 | slots: ['original_phone', 'new_owner_id'] 104 | }, 105 | { 106 | keywords: ['国际漫游', '漫游'], 107 | api: '/api/roaming_enable', 108 | businessKey: 'roaming_enable', 109 | slots: ['phone', 'roaming_country', 'start_date', 'end_date'] 110 | }, 111 | { 112 | keywords: ['通知订阅', '订阅'], 113 | api: '/api/notification_subscribe', 114 | businessKey: 'notification_subscribe', 115 | slots: ['phone', 'notification_type'] 116 | }, 117 | { 118 | keywords: ['套餐退订', '退订'], 119 | api: '/api/package_unsubscribe', 120 | businessKey: 'package_unsubscribe', 121 | slots: ['phone', 'package_type'] 122 | }, 123 | { 124 | keywords: ['宽带升级', '升级'], 125 | api: '/api/broadband_upgrade', 126 | businessKey: 'broadband_upgrade', 127 | slots: ['phone', 'broadband_account', 'upgrade_type'] 128 | }, 129 | { 130 | keywords: ['增值业务', '来电显示', '彩铃'], 131 | api: '/api/value_added', 132 | businessKey: 'value_added', 133 | slots: ['phone', 'service_name', 'action'] 134 | }, 135 | { 136 | keywords: ['停机办理', '复机办理', '停复机'], 137 | api: '/api/stop_resume', 138 | businessKey: 'stop_resume', 139 | slots: ['phone', 'action'] 140 | }, 141 | { 142 | keywords: ['密码重置', '重置密码'], 143 | api: '/api/password_reset', 144 | businessKey: 'password_reset', 145 | slots: ['phone', 'id_number'] 146 | }, 147 | { 148 | keywords: ['对话日志', '日志'], 149 | api: '/api/chat_log', 150 | businessKey: 'chat_log', 151 | slots: ['phone', 'action', 'log_content'] 152 | }, 153 | { 154 | keywords: ['评价', '服务评价'], 155 | api: '/api/feedback', 156 | businessKey: 'feedback', 157 | slots: ['phone', 'score', 'comment'] 158 | }, 159 | { 160 | keywords: ['人工', '转人工'], 161 | api: '/api/transfer_agent', 162 | businessKey: 'transfer_agent', 163 | slots: ['phone'] 164 | }, 165 | // 兜底FAQ,必须放最后 166 | { 167 | keywords: ['faq', '常见问题', '帮助', '问题', '咨询', '业务查询', '业务'], 168 | api: '/api/faq', 169 | businessKey: 'faq', 170 | slots: ['question'] 171 | } 172 | ]; 173 | 174 | export function detectIntent(input) { 175 | for (const item of intentMap) { 176 | if (item.keywords.some(kw => input.includes(kw))) { 177 | return item; 178 | } 179 | } 180 | return intentMap[intentMap.length - 1]; 181 | } -------------------------------------------------------------------------------- /frontend/src/api/aiApi.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import config from '../config/config'; 3 | 4 | export const BASE_URL = config.apiBaseUrl; 5 | 6 | export function sendMessage(api, params) { 7 | return axios.post(BASE_URL + api, params); 8 | } 9 | 10 | export function getMockSlots() { 11 | return axios.get(BASE_URL + config.apiPrefix + '/mock_slots'); 12 | } 13 | 14 | // 流式AI聊天接口(支持业务流程) 15 | export function streamLLMChat(messages, userInput, sessionId, onData, onError, onComplete) { 16 | const controller = new AbortController(); 17 | 18 | if (config.debug) { 19 | console.log('开始流式AI聊天', { userInput, sessionId, apiUrl: `${BASE_URL}${config.apiPrefix}/llm_chat` }); 20 | } 21 | 22 | fetch(`${BASE_URL}${config.apiPrefix}/llm_chat`, { 23 | method: 'POST', 24 | headers: { 25 | 'Content-Type': 'application/json; charset=utf-8', 26 | 'Accept': 'text/event-stream; charset=utf-8' 27 | }, 28 | body: JSON.stringify({ 29 | messages: messages, 30 | user_input: userInput, 31 | session_id: sessionId 32 | }), 33 | signal: controller.signal 34 | }) 35 | .then(response => { 36 | if (config.debug) { 37 | console.log('收到响应', { 38 | status: response.status, 39 | contentType: response.headers.get('content-type'), 40 | sessionId: response.headers.get('x-session-id') 41 | }); 42 | } 43 | 44 | if (!response.ok) { 45 | throw new Error(`HTTP error! status: ${response.status}`); 46 | } 47 | 48 | // 获取会话ID 49 | const returnedSessionId = response.headers.get('x-session-id'); 50 | if (returnedSessionId && config.debug) { 51 | console.log('获取到会话ID:', returnedSessionId); 52 | } 53 | 54 | const reader = response.body.getReader(); 55 | // 显式指定UTF-8解码器,使用非严格模式处理不完整字符 56 | const decoder = new TextDecoder('utf-8', { fatal: false, ignoreBOM: true }); 57 | let buffer = ''; 58 | let contentCount = 0; 59 | let totalContent = ''; 60 | 61 | function readStream() { 62 | return reader.read().then(({ done, value }) => { 63 | if (done) { 64 | if (config.debug) { 65 | console.log('流式读取完成', { 66 | contentCount, 67 | totalContent: totalContent.substring(0, 100) + '...', 68 | sessionId: returnedSessionId 69 | }); 70 | } 71 | onComplete && onComplete(returnedSessionId); 72 | return; 73 | } 74 | 75 | try { 76 | // 解码二进制数据为UTF-8字符串 77 | const chunk = decoder.decode(value, { stream: true }); 78 | buffer += chunk; 79 | 80 | // 按行处理,确保完整的SSE消息 81 | const lines = buffer.split('\n'); 82 | buffer = lines.pop() || ''; // 保留不完整的最后一行 83 | 84 | for (const line of lines) { 85 | const trimmedLine = line.trim(); 86 | 87 | if (trimmedLine.startsWith('data: ')) { 88 | const data = trimmedLine.slice(6); 89 | 90 | if (data === '[DONE]') { 91 | if (config.debug) { 92 | console.log('收到[DONE]标记,结束流式处理'); 93 | } 94 | onComplete && onComplete(returnedSessionId); 95 | return; 96 | } else if (data.startsWith('[ERROR]')) { 97 | const errorMsg = data.slice(8); 98 | console.error('收到错误', { errorMsg }); 99 | onError && onError(errorMsg); 100 | return; 101 | } else if (data.trim()) { 102 | contentCount++; 103 | totalContent += data; 104 | 105 | // 记录接收到的内容详情(仅在开发模式下) 106 | if (config.debug) { 107 | console.log(`接收内容 #${contentCount}:`, { 108 | content: data, 109 | length: data.length 110 | }); 111 | } 112 | 113 | // 直接传递解码后的文本增量 114 | onData && onData(data); 115 | } 116 | } 117 | } 118 | } catch (error) { 119 | console.error('UTF-8解码错误', { 120 | error: error.message, 121 | value: value ? Array.from(value.slice(0, 20)) : null // 显示前20个字节 122 | }); 123 | onError && onError(`解码错误: ${error.message}`); 124 | return; 125 | } 126 | 127 | return readStream(); 128 | }); 129 | } 130 | 131 | return readStream(); 132 | }) 133 | .catch(error => { 134 | console.error('fetch错误', { error: error.message, stack: error.stack }); 135 | onError && onError(error.message); 136 | }); 137 | 138 | return { 139 | close: () => controller.abort() 140 | }; 141 | } 142 | 143 | // 普通AI聊天接口(非流式) 144 | export function chatWithLLM(messages, userInput, sessionId) { 145 | return axios.post(BASE_URL + config.apiPrefix + '/llm_chat', { 146 | messages: messages, 147 | user_input: userInput, 148 | session_id: sessionId 149 | }); 150 | } 151 | 152 | // 生成会话ID 153 | export function generateSessionId() { 154 | return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); 155 | } 156 | 157 | // 重置会话 158 | export function resetSession(sessionId) { 159 | return axios.post(BASE_URL + config.apiPrefix + '/reset_session', { 160 | session_id: sessionId 161 | }); 162 | } 163 | 164 | // 健康检查 165 | export function healthCheck() { 166 | return axios.get(BASE_URL + config.apiPrefix + '/health'); 167 | } -------------------------------------------------------------------------------- /frontend/src/utils/businessBot.js: -------------------------------------------------------------------------------- 1 | import { detectIntent, intentMap } from './intentMap'; 2 | import { sendMessage, getMockSlots } from '../api/aiApi'; 3 | 4 | // 业务场景格式化回复(与ChatBox一致,支持所有主流业务) 5 | export function formatBusinessReply(intent, data, slotValues = {}) { 6 | if (!data) return '未查到业务数据,请检查参数或稍后重试。'; 7 | if (intent.businessKey === 'traffic_query') { 8 | return `本月总流量:${data.total_traffic},已用:${data.used_traffic},剩余:${data.remain_traffic},状态:${data.status}`; 9 | } 10 | if (intent.businessKey === 'broadband_repair') { 11 | return `宽带报修工单已创建,联系电话:${data.phone},账号:${data.broadband_account},地址:${data.address},故障类型:${data.fault_type},工单状态:${data.status || '已受理'}。${data.remark || ''}`; 12 | } 13 | if (intent.businessKey === 'package_change') { 14 | return `套餐变更成功!原套餐:${data.old_package || ''},新套餐:${data.new_package || data.package_type || ''},生效时间:${data.effective_time || ''},月费:${data.monthly_fee || ''},状态:${data.status || ''}。${data.remark || ''}`; 15 | } 16 | if (intent.businessKey === 'sub_card_apply') { 17 | return `副卡办理成功!主卡:${data.main_phone},副卡张数:${data.sub_card_count},套餐:${data.sub_package_type},副卡号码:${Array.isArray(data.sub_cards) ? data.sub_cards.join('、') : ''},月费:${data.monthly_fee},总月费:${data.total_monthly_fee},办理时间:${data.apply_time}。${data.remark || ''}`; 18 | } 19 | if (intent.businessKey === 'realname_query') { 20 | if (data.status === '已实名' || data.realname_status === '已实名') { 21 | return `实名认证结果:已实名,姓名:${data.name || ''},证件号:${data.id_number || ''}`; 22 | } else if (data.status === '未实名' || data.realname_status === '未实名') { 23 | return '实名认证结果:未实名。'; 24 | } else { 25 | return data.remark || data.msg || '实名认证结果:' + (data.status || data.realname_status || '未知'); 26 | } 27 | } 28 | if (intent.businessKey === 'account_bill') { 29 | return `账单周期:${data.bill_period || ''},应缴:${data.amount_due || ''}元,已缴:${data.amount_paid || ''}元,状态:${data.status || ''}`; 30 | } 31 | if (intent.businessKey === 'points_query') { 32 | return `当前积分:${data.points || ''}分,有效期至:${data.expire_date || ''}`; 33 | } 34 | if (intent.businessKey === 'suspend_apply') { 35 | return `停机保号办理成功,停机时长:${data.suspend_duration || ''}月,生效时间:${data.effective_time || ''},状态:${data.status || ''}`; 36 | } 37 | if (intent.businessKey === 'invoice_apply') { 38 | return `发票申请成功,类型:${data.invoice_type || ''},邮寄地址:${data.mailing_address || ''},状态:${data.status || ''}`; 39 | } 40 | if (intent.businessKey === 'package_balance') { 41 | return `套餐余量:语音${data.voice_balance || ''},流量${data.data_balance || ''},短信${data.sms_balance || ''},套餐:${data.package_name || ''},到期:${data.expire_date || ''}`; 42 | } 43 | if (intent.businessKey === 'recharge') { 44 | const amount = data.amount || slotValues.amount || ''; 45 | const payment_method = data.payment_method || slotValues.payment_method || ''; 46 | const status = data.status || '充值成功'; 47 | return `充值成功,金额:${amount}元,方式:${payment_method},状态:${status}`; 48 | } 49 | if (intent.businessKey === 'number_transfer') { 50 | return `号码过户成功,原号码:${data.original_phone || ''},新机主证件号:${data.new_owner_id || ''},状态:${data.status || ''}`; 51 | } 52 | if (intent.businessKey === 'roaming_enable') { 53 | return `国际漫游已开通,国家:${data.roaming_country || ''},起止:${data.start_date || ''}~${data.end_date || ''},状态:${data.status || ''}`; 54 | } 55 | if (intent.businessKey === 'points_exchange') { 56 | const exchange_type = data.exchange_type || slotValues.exchange_type || ''; 57 | const exchange_quantity = data.exchange_quantity || slotValues.exchange_quantity || ''; 58 | const status = data.status || '兑换成功'; 59 | return `积分兑换成功,类型:${exchange_type},数量:${exchange_quantity},状态:${status}`; 60 | } 61 | if (intent.businessKey === 'notification_subscribe') { 62 | return `通知订阅成功,类型:${data.notification_type || ''},状态:${data.status || ''}`; 63 | } 64 | if (intent.businessKey === 'package_unsubscribe') { 65 | return `套餐退订成功,类型:${data.package_type || ''},状态:${data.status || ''}`; 66 | } 67 | if (intent.businessKey === 'broadband_upgrade') { 68 | return `宽带升级成功,账号:${data.broadband_account || ''},类型:${data.upgrade_type || ''},状态:${data.status || ''}`; 69 | } 70 | if (intent.businessKey === 'stop_resume') { 71 | return `业务办理成功,操作:${data.action || ''},状态:${data.status || ''}`; 72 | } 73 | if (intent.businessKey === 'password_reset') { 74 | return `密码重置成功,状态:${data.status || ''}`; 75 | } 76 | if (intent.businessKey === 'chat_log') { 77 | return data.log || data.remark || data.msg || '对话日志操作完成'; 78 | } 79 | if (intent.businessKey === 'feedback') { 80 | return `服务评价提交成功,评分:${data.score || ''},内容:${data.comment || ''}`; 81 | } 82 | if (intent.businessKey === 'complaint') { 83 | return `投诉已受理,工单号:${data.complaint_id || ''},类型:${data.complaint_type || ''},优先级:${data.priority || ''},预计处理时长:${data.estimated_time || ''},状态:${data.status || ''}。${data.remark || ''}`; 84 | } 85 | if (intent.businessKey === 'value_added') { 86 | return `增值业务${data.service_name || ''}${data.action || ''},状态:${data.status || ''}`; 87 | } 88 | if (intent.businessKey === 'faq') { 89 | return data.answer || data.msg || '这是一个模拟FAQ答案。'; 90 | } 91 | if (intent.businessKey === 'address_modify') { 92 | const new_address = data.new_address || slotValues.new_address || ''; 93 | const status = data.status || '修改成功'; 94 | return `通信地址修改成功,新地址:${new_address},状态:${status}`; 95 | } 96 | // 兜底:输出原始data内容,便于debug 97 | return `【原始业务数据】${JSON.stringify(data)}`; 98 | } 99 | 100 | // 判断是否为业务意图(非FAQ) 101 | export function isBusinessIntent(input) { 102 | const intent = detectIntent(input); 103 | return intent && intent.businessKey && intent.businessKey !== 'faq'; 104 | } 105 | 106 | // 支持传入intent参数,补全流程时优先用传入的intent 107 | export async function handleBusinessInput({ input, slotValues = {}, mockSlots, intent: currentIntent }) { 108 | const intent = currentIntent || detectIntent(input); 109 | if (!intent || !intent.api) return { type: 'faq', text: '未识别到业务意图' }; 110 | if (intent.businessKey === 'faq') { 111 | return { type: 'faq', text: slotValues.question || input }; 112 | } 113 | let slotDefs = (mockSlots && intent.businessKey && mockSlots[intent.businessKey]?.slots) || {}; 114 | let requiredKeys = Object.entries(slotDefs).filter(([k, v]) => v.required).map(([k]) => k); 115 | let missing = requiredKeys.filter(key => !slotValues[key]); 116 | if (missing.length > 0) { 117 | return { type: 'slot', missing, intent, slotDefs }; 118 | } 119 | // 调用后端API 120 | const params = {}; 121 | requiredKeys.forEach(key => { params[key] = slotValues[key]; }); 122 | const res = await sendMessage(intent.api, params); 123 | const plainText = formatBusinessReply(intent, res.data.data, slotValues); 124 | return { 125 | type: 'business', 126 | plainText, 127 | data: res.data.data, 128 | intent, 129 | slotValues 130 | }; 131 | } -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | import json 3 | import logging 4 | import uuid 5 | from threading import Lock 6 | from flask import Flask, request, jsonify, send_file, Response 7 | from flask_cors import CORS 8 | 9 | from models.chatbot_model import ChatbotModel 10 | from utils.app_init import before_init 11 | from utils.helpers import load_all_scene_configs 12 | from app_config import config 13 | 14 | app = Flask(__name__) 15 | 16 | # 使用配置文件中的CORS设置 17 | CORS(app, origins="*", supports_credentials=True) 18 | 19 | # 实例化ChatbotModel 20 | chatbot_model = ChatbotModel(load_all_scene_configs()) 21 | 22 | # 会话存储 - 生产环境应使用Redis或数据库 23 | sessions = {} 24 | sessions_lock = Lock() 25 | 26 | def get_or_create_session(session_id=None): 27 | """获取或创建会话""" 28 | with sessions_lock: 29 | if not session_id: 30 | session_id = f"session_{uuid.uuid4().hex[:8]}" 31 | 32 | if session_id not in sessions: 33 | sessions[session_id] = { 34 | 'messages': [], 35 | 'context': {}, 36 | 'created_at': None 37 | } 38 | 39 | return session_id, sessions[session_id] 40 | 41 | @app.route('/multi_question', methods=['POST']) 42 | def api_multi_question(): 43 | """多轮问答接口(原有接口保持兼容)""" 44 | data = request.json 45 | question = data.get('question') 46 | if not question: 47 | return jsonify({"error": "No question provided"}), 400 48 | 49 | response = chatbot_model.process_multi_question(question) 50 | return jsonify({"answer": response}) 51 | 52 | @app.route(f'{config.API_PREFIX}/llm_chat', methods=['POST']) 53 | def api_llm_chat(): 54 | """流式AI聊天接口""" 55 | data = request.json 56 | messages = data.get('messages', []) 57 | user_input = data.get('user_input', '') 58 | session_id = data.get('session_id') 59 | 60 | if not user_input: 61 | return jsonify({"error": "No user_input provided"}), 400 62 | 63 | # 获取或创建会话 64 | session_id, session_data = get_or_create_session(session_id) 65 | 66 | # 检查是否是流式请求 67 | accept_header = request.headers.get('Accept', '') 68 | is_stream = 'text/event-stream' in accept_header 69 | 70 | try: 71 | if is_stream: 72 | # 流式响应 73 | def generate(): 74 | try: 75 | import time 76 | 77 | # 处理消息 78 | response = chatbot_model.process_multi_question(user_input) 79 | 80 | # 流式输出:逐字符发送 81 | buffer = "" 82 | for i, char in enumerate(response): 83 | buffer += char 84 | 85 | # 每隔几个字符或遇到标点符号时发送一次 86 | if len(buffer) >= 3 or char in '。!?,、;:': 87 | # 发送SSE格式数据 88 | yield f"data: {buffer}\n\n" 89 | buffer = "" 90 | # 添加小延迟模拟真实流式体验 91 | time.sleep(0.05) 92 | 93 | # 发送剩余内容 94 | if buffer.strip(): 95 | yield f"data: {buffer}\n\n" 96 | 97 | # 发送完成标记 98 | yield "data: [DONE]\n\n" 99 | 100 | except Exception as e: 101 | logging.error(f"流式处理错误: {str(e)}") 102 | yield f"data: [ERROR] {str(e)}\n\n" 103 | 104 | response = Response( 105 | generate(), 106 | mimetype='text/event-stream', 107 | headers={ 108 | 'Cache-Control': 'no-cache', 109 | 'Connection': 'keep-alive', 110 | 'Access-Control-Allow-Origin': '*', 111 | 'Access-Control-Allow-Headers': 'Content-Type', 112 | 'X-Session-ID': session_id 113 | } 114 | ) 115 | return response 116 | else: 117 | # 非流式响应 118 | response = chatbot_model.process_multi_question(user_input) 119 | return jsonify({ 120 | "response": response, 121 | "session_id": session_id 122 | }) 123 | 124 | except Exception as e: 125 | logging.error(f"LLM聊天错误: {str(e)}") 126 | return jsonify({"error": str(e)}), 500 127 | 128 | @app.route(f'{config.API_PREFIX}/mock_slots', methods=['GET']) 129 | def api_mock_slots(): 130 | """获取模拟槽位数据""" 131 | mock_data = { 132 | "slots": { 133 | "phone_number": "13812345678", 134 | "user_name": "张三", 135 | "service_type": "流量套餐", 136 | "package_type": "月套餐" 137 | }, 138 | "available_services": [ 139 | {"id": 1, "name": "流量套餐", "description": "包月流量服务"}, 140 | {"id": 2, "name": "通话套餐", "description": "包月通话服务"}, 141 | {"id": 3, "name": "短信套餐", "description": "包月短信服务"} 142 | ] 143 | } 144 | return jsonify(mock_data) 145 | 146 | @app.route(f'{config.API_PREFIX}/reset_session', methods=['POST']) 147 | def api_reset_session(): 148 | """重置会话""" 149 | data = request.json 150 | session_id = data.get('session_id') 151 | 152 | if not session_id: 153 | return jsonify({"error": "No session_id provided"}), 400 154 | 155 | with sessions_lock: 156 | if session_id in sessions: 157 | del sessions[session_id] 158 | 159 | return jsonify({"message": "Session reset successfully", "session_id": session_id}) 160 | 161 | @app.route(f'{config.API_PREFIX}/health', methods=['GET']) 162 | def api_health(): 163 | """健康检查接口""" 164 | return jsonify({ 165 | "status": "healthy", 166 | "backend_url": config.backend_url, 167 | "environment": config.FLASK_ENV 168 | }) 169 | 170 | @app.route('/', methods=['GET']) 171 | def index(): 172 | """主页""" 173 | return send_file('./demo/user_input.html') 174 | 175 | @app.errorhandler(404) 176 | def not_found(error): 177 | return jsonify({"error": "API endpoint not found"}), 404 178 | 179 | @app.errorhandler(500) 180 | def internal_error(error): 181 | return jsonify({"error": "Internal server error"}), 500 182 | 183 | if __name__ == '__main__': 184 | before_init() 185 | 186 | # 配置日志 187 | logging.basicConfig( 188 | level=getattr(logging, config.LOG_LEVEL), 189 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 190 | ) 191 | 192 | print(f"🚀 后端服务启动中...") 193 | print(f"📍 地址: {config.backend_url}") 194 | print(f"🌍 环境: {config.FLASK_ENV}") 195 | print(f"🔗 允许跨域: {', '.join(config.CORS_ORIGINS)}") 196 | 197 | app.run( 198 | host=config.BACKEND_HOST, 199 | port=config.BACKEND_PORT, 200 | debug=config.FLASK_DEBUG 201 | ) 202 | -------------------------------------------------------------------------------- /demo/user_input.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 智能问答对话框 7 | 96 | 97 | 98 |
99 | 100 |
101 |
102 | 103 | 104 | 105 |
106 |
107 | 108 | 224 | 225 | 226 | -------------------------------------------------------------------------------- /scene_config/scene_templates.json: -------------------------------------------------------------------------------- 1 | { 2 | "common_fields": [ 3 | {"name": "phone", "desc": "手机号,必须是11位数字", "type": "string", "required": true} 4 | ], 5 | "scene_list": [{ 6 | "scene_name": "traffic_query", 7 | "name": "流量查询", 8 | "description": "手机流量使用情况查询服务,可查询指定手机号某月的流量使用情况。", 9 | "example": "JSON:[{'name': 'phone', 'desc': '需要查询的手机号', 'value': ''}, {'name': 'month', 'desc': '查询的月份,格式为yyyy-MM', 'value': ''} ]\n输入:帮我查一下18724011022在2024年7月的流量\n答:{ 'phone': '18724011022', 'month': '2024-07' }", 10 | "parameters": [ 11 | { 12 | "name": "month", 13 | "desc": "查询的月份,格式为yyyy-MM", 14 | "type": "string", 15 | "required": true 16 | } 17 | ], 18 | "enabled": true 19 | },{ 20 | "scene_name": "broadband_repair", 21 | "name": "宽带报修", 22 | "description": "宽带故障报修服务,提交手机号、宽带账号、地址和故障类型。", 23 | "parameters": [ 24 | {"name": "broadband_account", "desc": "宽带账号", "type": "string", "required": true}, 25 | {"name": "address", "desc": "报修地址", "type": "string", "required": true}, 26 | {"name": "fault_type", "desc": "故障类型", "type": "string", "required": true} 27 | ], 28 | "enabled": true 29 | },{ 30 | "scene_name": "package_change", 31 | "name": "5G套餐变更", 32 | "description": "办理5G套餐变更,提交手机号、套餐类型和生效时间。", 33 | "parameters": [ 34 | {"name": "package_type", "desc": "套餐类型", "type": "string", "required": true}, 35 | {"name": "effective_time", "desc": "生效时间,格式yyyy-MM-dd", "type": "string", "required": true} 36 | ], 37 | "enabled": true 38 | }, 39 | { 40 | "scene_name": "sub_card_apply", 41 | "name": "副卡办理", 42 | "description": "副卡办理服务,提交主卡手机号、副卡数量和套餐类型。", 43 | "parameters": [ 44 | {"name": "sub_card_count", "desc": "副卡数量", "type": "string", "required": true}, 45 | {"name": "sub_package_type", "desc": "副卡套餐类型", "type": "string", "required": true} 46 | ], 47 | "enabled": true 48 | }, 49 | { 50 | "scene_name": "account_bill", 51 | "name": "话费账单", 52 | "description": "查询话费账单,提交手机号和账单周期。", 53 | "parameters": [ 54 | {"name": "bill_period", "desc": "账单周期,格式yyyy-MM", "type": "string", "required": true} 55 | ], 56 | "enabled": true 57 | }, 58 | { 59 | "scene_name": "realname_query", 60 | "name": "实名认证", 61 | "description": "实名认证查询,提交手机号和身份证号。", 62 | "parameters": [ 63 | {"name": "id_number", "desc": "身份证号", "type": "string", "required": true} 64 | ], 65 | "enabled": true 66 | }, 67 | { 68 | "scene_name": "points_query", 69 | "name": "积分查询", 70 | "description": "查询积分信息,提交手机号。", 71 | "parameters": [ 72 | {} 73 | ], 74 | "enabled": true 75 | }, 76 | { 77 | "scene_name": "suspend_apply", 78 | "name": "停机保号办理", 79 | "description": "办理停机保号,提交手机号和停机时长。", 80 | "parameters": [ 81 | {"name": "suspend_duration", "desc": "停机时长(天)", "type": "string", "required": true} 82 | ], 83 | "enabled": true 84 | }, 85 | { 86 | "scene_name": "invoice_apply", 87 | "name": "发票申请", 88 | "description": "发票申请,提交手机号、发票类型和邮寄地址。", 89 | "parameters": [ 90 | {"name": "invoice_type", "desc": "发票类型", "type": "string", "required": true}, 91 | {"name": "mailing_address", "desc": "邮寄地址", "type": "string", "required": true} 92 | ], 93 | "enabled": true 94 | }, 95 | { 96 | "scene_name": "address_modify", 97 | "name": "修改通信地址", 98 | "description": "修改通信地址,提交手机号和新地址。", 99 | "parameters": [ 100 | {"name": "new_address", "desc": "新地址", "type": "string", "required": true} 101 | ], 102 | "enabled": true 103 | }, 104 | { 105 | "scene_name": "recharge", 106 | "name": "充值缴费", 107 | "description": "充值缴费,提交手机号、金额和支付方式。", 108 | "parameters": [ 109 | {"name": "amount", "desc": "充值金额", "type": "string", "required": true}, 110 | {"name": "payment_method", "desc": "支付方式", "type": "string", "required": true} 111 | ], 112 | "enabled": true 113 | }, 114 | { 115 | "scene_name": "number_transfer", 116 | "name": "号码过户", 117 | "description": "号码过户,提交原手机号和新用户身份证号。", 118 | "parameters": [ 119 | {"name": "new_owner_id", "desc": "新用户身份证号", "type": "string", "required": true} 120 | ], 121 | "enabled": true 122 | }, 123 | { 124 | "scene_name": "roaming_enable", 125 | "name": "国际漫游开通", 126 | "description": "国际漫游开通,提交手机号、漫游国家、起止日期。", 127 | "parameters": [ 128 | {"name": "roaming_country", "desc": "漫游国家", "type": "string", "required": true}, 129 | {"name": "start_date", "desc": "开始日期", "type": "string", "required": true}, 130 | {"name": "end_date", "desc": "结束日期", "type": "string", "required": true} 131 | ], 132 | "enabled": true 133 | }, 134 | { 135 | "scene_name": "package_unsubscribe", 136 | "name": "套餐退订", 137 | "description": "套餐退订,提交手机号和套餐类型。", 138 | "parameters": [ 139 | {"name": "package_type", "desc": "套餐类型", "type": "string", "required": true} 140 | ], 141 | "enabled": true 142 | }, 143 | { 144 | "scene_name": "points_exchange", 145 | "name": "积分兑换", 146 | "description": "积分兑换,提交手机号、兑换类型和数量。", 147 | "parameters": [ 148 | {"name": "exchange_type", "desc": "兑换类型", "type": "string", "required": true}, 149 | {"name": "exchange_quantity", "desc": "兑换数量", "type": "string", "required": true} 150 | ], 151 | "enabled": true 152 | }, 153 | { 154 | "scene_name": "realname_status", 155 | "name": "查询实名状态", 156 | "description": "查询实名状态,提交手机号。", 157 | "parameters": [ 158 | {} 159 | ], 160 | "enabled": true 161 | }, 162 | { 163 | "scene_name": "notification_subscribe", 164 | "name": "通知订阅", 165 | "description": "通知订阅,提交手机号和通知类型。", 166 | "parameters": [ 167 | {"name": "notification_type", "desc": "通知类型", "type": "string", "required": true} 168 | ], 169 | "enabled": true 170 | }, 171 | { 172 | "scene_name": "complaint", 173 | "name": "投诉工单", 174 | "description": "投诉工单,提交手机号、投诉类型和内容。", 175 | "parameters": [ 176 | {"name": "complaint_type", "desc": "投诉类型", "type": "string", "required": true}, 177 | {"name": "complaint_content", "desc": "投诉内容", "type": "string", "required": true} 178 | ], 179 | "enabled": true 180 | }, 181 | { 182 | "scene_name": "transfer_agent", 183 | "name": "转人工客服", 184 | "description": "转人工客服,提交手机号。", 185 | "parameters": [ 186 | {} 187 | ], 188 | "enabled": true 189 | }, 190 | { 191 | "scene_name": "faq", 192 | "name": "FAQ查询", 193 | "description": "FAQ查询,提交问题内容。", 194 | "parameters": [ 195 | {"name": "question", "desc": "问题内容", "type": "string", "required": true} 196 | ], 197 | "enabled": true 198 | }, 199 | { 200 | "scene_name": "broadband_upgrade", 201 | "name": "宽带升级", 202 | "description": "宽带升级,提交手机号、宽带账号和升级类型。", 203 | "parameters": [ 204 | {"name": "broadband_account", "desc": "宽带账号", "type": "string", "required": true}, 205 | {"name": "upgrade_type", "desc": "升级类型", "type": "string", "required": true} 206 | ], 207 | "enabled": true 208 | }, 209 | { 210 | "scene_name": "feedback", 211 | "name": "服务评价提交", 212 | "description": "服务评价提交,提交手机号、评分和评价内容。", 213 | "parameters": [ 214 | {"name": "score", "desc": "评分", "type": "string", "required": true}, 215 | {"name": "comment", "desc": "评价内容", "type": "string", "required": true} 216 | ], 217 | "enabled": true 218 | }, 219 | { 220 | "scene_name": "password_reset", 221 | "name": "密码重置", 222 | "description": "密码重置,提交手机号和身份证号。", 223 | "parameters": [ 224 | {"name": "id_number", "desc": "身份证号", "type": "string", "required": true} 225 | ], 226 | "enabled": true 227 | }, 228 | { 229 | "scene_name": "stop_resume", 230 | "name": "停/复机业务办理", 231 | "description": "停/复机业务办理,提交手机号和操作类型。", 232 | "parameters": [ 233 | {"name": "action", "desc": "操作类型(停机/复机)", "type": "string", "required": true} 234 | ], 235 | "enabled": true 236 | }, 237 | { 238 | "scene_name": "value_added", 239 | "name": "增值业务办理/退订", 240 | "description": "增值业务办理/退订,提交手机号、业务名称和操作类型。", 241 | "parameters": [ 242 | {"name": "service_name", "desc": "业务名称", "type": "string", "required": true}, 243 | {"name": "action", "desc": "操作类型(办理/退订)", "type": "string", "required": true} 244 | ], 245 | "enabled": true 246 | }, 247 | { 248 | "scene_name": "chat_log", 249 | "name": "对话日志记录", 250 | "description": "对话日志记录,提交手机号、操作和日志内容。", 251 | "parameters": [ 252 | {"name": "action", "desc": "操作", "type": "string", "required": true}, 253 | {"name": "log_content", "desc": "日志内容", "type": "string", "required": true} 254 | ], 255 | "enabled": true 256 | }] 257 | } 258 | -------------------------------------------------------------------------------- /models/chatbot_model.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | import logging 3 | 4 | from config import RELATED_INTENT_THRESHOLD, NO_SCENE_RESPONSE 5 | from scene_processor.impl.common_processor import CommonProcessor 6 | from utils.data_format_utils import extract_continuous_digits, extract_float 7 | from utils.helpers import send_message 8 | from scene_config import scene_prompts 9 | 10 | 11 | class ChatbotModel: 12 | def __init__(self, scene_templates: dict): 13 | self.scene_templates: dict = scene_templates 14 | self.current_purpose: str = '' 15 | self.last_recognized_scene: str = '' # 记录上次识别到的场景 16 | self.processors = {} 17 | self.scene_slots = {} # 新增:每个场景的槽位数据 18 | self.chat_history = [] # 添加聊天记录存储 19 | self.is_slot_filling = False # 标记是否正在补槽阶段 20 | 21 | @staticmethod 22 | def load_scene_processor(self, scene_config): 23 | try: 24 | return CommonProcessor(scene_config) 25 | except (ImportError, AttributeError, KeyError): 26 | raise ImportError(f"未找到场景处理器 scene_config: {scene_config}") 27 | 28 | def recognize_intent(self, user_input): 29 | # 根据场景模板生成选项 30 | purpose_options = {} 31 | purpose_description = {} 32 | index = 1 33 | for template_key, template_info in self.scene_templates.items(): 34 | purpose_options[str(index)] = template_key 35 | purpose_description[str(index)] = template_info["description"] 36 | index += 1 37 | options_prompt = "\n".join([f"{key}. {value} - 请回复{key}" for key, value in purpose_description.items()]) 38 | options_prompt += "\n0. 无场景/无法判断/没有符合的选项 - 请回复0" 39 | 40 | # 发送选项给AI,带上聊天记录 41 | last_scene_info = f"上次识别到的场景:{self.last_recognized_scene}" if self.last_recognized_scene else "上次识别到的场景:无" 42 | user_choice = send_message( 43 | f"有下面多种场景,需要你根据用户输入进行判断,以最新的聊天记录为准,只答选项\n{last_scene_info}\n{options_prompt}\n用户输入:{user_input}\n请回复序号:", 44 | user_input, 45 | self.chat_history 46 | ) 47 | 48 | logging.debug(f'purpose_options: %s', purpose_options) 49 | logging.debug(f'user_choice: %s', user_choice) 50 | 51 | user_choices = extract_continuous_digits(user_choice) 52 | 53 | # 根据用户选择获取对应场景 54 | if user_choices and user_choices[0] != '0': 55 | # 可以判断场景,更新当前场景 56 | new_purpose = purpose_options[user_choices[0]] 57 | if new_purpose != self.current_purpose: 58 | # 场景发生变化,重置补槽状态 59 | self.current_purpose = new_purpose 60 | self.last_recognized_scene = new_purpose # 更新上次识别到的场景 61 | self.is_slot_filling = False 62 | # 清除之前的处理器 63 | if new_purpose in self.processors: 64 | del self.processors[new_purpose] 65 | print(f"用户选择了场景:{self.scene_templates[self.current_purpose]['name']}") 66 | else: 67 | # 用户选择了"无场景/无法判断" 68 | if self.current_purpose and self.is_slot_filling: 69 | # 有当前场景且正在补槽阶段,保留当前场景 70 | print(f"无法判断意图,保留当前场景:{self.scene_templates[self.current_purpose]['name']}") 71 | else: 72 | # 没有当前场景或不在补槽阶段,清空场景 73 | self.current_purpose = '' 74 | self.is_slot_filling = False 75 | print("无法识别用户意图") 76 | 77 | def get_processor_for_scene(self, scene_name): 78 | if scene_name in self.processors: 79 | return self.processors[scene_name] 80 | 81 | scene_config = self.scene_templates.get(scene_name) 82 | if not scene_config: 83 | raise ValueError(f"未找到名为{scene_name}的场景配置") 84 | 85 | # 新增:为该场景初始化槽位数据 86 | from utils.helpers import get_raw_slot 87 | if scene_name not in self.scene_slots: 88 | self.scene_slots[scene_name] = get_raw_slot(scene_config["parameters"]) 89 | 90 | # 修改:将槽位数据传递给CommonProcessor 91 | processor_class = self.load_scene_processor(self, scene_config) 92 | processor_class.slot = self.scene_slots[scene_name] # 让处理器直接操作全局槽位 93 | self.processors[scene_name] = processor_class 94 | return self.processors[scene_name] 95 | 96 | def clear_current_scene(self): 97 | """清除当前场景,用于场景处理完成后""" 98 | self.current_purpose = '' 99 | self.last_recognized_scene = '' # 清除上次识别到的场景记录 100 | self.is_slot_filling = False 101 | # 清除所有处理器 102 | self.processors.clear() 103 | logging.info("场景处理完成,已清除当前场景") 104 | 105 | def generate_no_scene_response(self, user_input): 106 | """生成无场景识别时的AI回复""" 107 | # 生成场景选项字符串 108 | purpose_description = {} 109 | index = 1 110 | for template_key, template_info in self.scene_templates.items(): 111 | purpose_description[str(index)] = template_info["description"] 112 | index += 1 113 | options_prompt = "\n".join([f"{key}. {value}" for key, value in purpose_description.items()]) 114 | options_prompt += "\n0. 无场景/无法判断" 115 | 116 | prompt = scene_prompts.no_scene_response.format(user_input, options_prompt) 117 | response = send_message(prompt, user_input, self.chat_history) 118 | return response if response else NO_SCENE_RESPONSE 119 | 120 | def detect_scene_switch(self, user_input): 121 | """检测用户是否有切换场景的意图""" 122 | if not self.current_purpose: 123 | return False 124 | 125 | current_scene_name = self.scene_templates[self.current_purpose]['name'] 126 | last_scene_name = self.scene_templates[self.last_recognized_scene]['name'] if self.last_recognized_scene else "无" 127 | 128 | prompt = scene_prompts.scene_switch_detection.format( 129 | current_scene_name, 130 | #last_scene_name, 131 | user_input 132 | ) 133 | 134 | response = send_message(prompt, user_input, self.chat_history) 135 | 136 | # 提取数字回复 137 | digits = extract_continuous_digits(response) 138 | if digits and digits[0] == '1': 139 | logging.info(f"检测到用户意图切换场景,当前场景:{current_scene_name}") 140 | return True 141 | 142 | return False 143 | 144 | def process_multi_question(self, user_input): 145 | """ 146 | 处理多轮问答 147 | :param user_input: 148 | :return: 149 | """ 150 | # 添加用户输入到聊天记录 151 | self.chat_history.append({"role": "user", "content": user_input}) 152 | 153 | # 如果没有当前场景,尝试识别意图 154 | if not self.current_purpose: 155 | self.recognize_intent(user_input) 156 | 157 | # 如果仍然没有场景,使用AI生成回复 158 | if not self.current_purpose: 159 | response = self.generate_no_scene_response(user_input) 160 | self.chat_history.append({"role": "assistant", "content": response}) 161 | return response 162 | 163 | # 有场景,标记为补槽阶段 164 | self.is_slot_filling = True 165 | logging.info('current_purpose: %s', self.current_purpose) 166 | 167 | # 在提取用户信息前,先检测用户是否有切换场景的意图 168 | if self.detect_scene_switch(user_input): 169 | # 用户想要切换场景,清除当前场景并重新开始意图识别 170 | self.clear_current_scene() 171 | self.recognize_intent(user_input) 172 | 173 | # 如果重新识别到场景,继续处理;否则生成无场景回复 174 | if self.current_purpose: 175 | processor = self.get_processor_for_scene(self.current_purpose) 176 | response = processor.process(user_input, self.chat_history) 177 | 178 | if not response.startswith("请问") and not response.startswith("抱歉,无法找到场景"): 179 | self.clear_current_scene() 180 | 181 | self.chat_history.append({"role": "assistant", "content": response}) 182 | return response 183 | else: 184 | response = self.generate_no_scene_response(user_input) 185 | self.chat_history.append({"role": "assistant", "content": response}) 186 | return response 187 | 188 | if self.current_purpose in self.scene_templates: 189 | # 根据场景模板调用相应场景的处理逻辑 190 | processor = self.get_processor_for_scene(self.current_purpose) 191 | # 调用抽象类process方法,传递聊天记录 192 | response = processor.process(user_input, self.chat_history) 193 | 194 | # 检查是否完成场景处理(通过检查响应内容是否包含错误信息或成功处理结果) 195 | if not response.startswith("请问") and not response.startswith("抱歉,无法找到场景"): 196 | # 场景处理完成,清除当前场景 197 | self.clear_current_scene() 198 | 199 | # 添加助手回复到聊天记录 200 | self.chat_history.append({"role": "assistant", "content": response}) 201 | return response 202 | else: 203 | # 场景不存在的情况 204 | response = self.generate_no_scene_response(user_input) 205 | self.chat_history.append({"role": "assistant", "content": response}) 206 | return response 207 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /frontend/src/components/AIChatBox.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import { Input, Button, Spin, message as antdMsg, Space, Typography, Badge } from 'antd'; 3 | import MessageBubble from './MessageBubble'; 4 | import { streamLLMChat, generateSessionId, resetSession } from '../api/aiApi'; 5 | 6 | const { Text } = Typography; 7 | const { TextArea } = Input; 8 | 9 | const WELCOME_MSG = '您好!我是中国电信客服助手小天,很高兴为您服务。您可以咨询流量查询、宽带报修、套餐变更、副卡申请等业务,也可以与我闲聊。'; 10 | 11 | export default function AIChatBox({ chat, updateChat }) { 12 | const [input, setInput] = useState(''); 13 | const [loading, setLoading] = useState(false); 14 | const [streaming, setStreaming] = useState(false); 15 | const [currentStream, setCurrentStream] = useState(null); 16 | const [sessionId, setSessionId] = useState(null); 17 | const [businessMode, setBusinessMode] = useState(false); 18 | const inputRef = useRef(); 19 | const [welcomed, setWelcomed] = useState(false); 20 | 21 | // 初始化会话ID 22 | useEffect(() => { 23 | if (!sessionId) { 24 | const newSessionId = generateSessionId(); 25 | setSessionId(newSessionId); 26 | console.log('生成新的会话ID:', newSessionId); 27 | } 28 | }, [sessionId]); 29 | 30 | // 只在Tab初始渲染时输出欢迎语 31 | useEffect(() => { 32 | if (!welcomed && chat.messages.length === 0) { 33 | updateChat(chat => ({ 34 | ...chat, 35 | messages: [ 36 | { 37 | id: Date.now(), 38 | role: 'ai', 39 | text: WELCOME_MSG, 40 | status: 'welcome' 41 | } 42 | ] 43 | })); 44 | setWelcomed(true); 45 | } 46 | // eslint-disable-next-line 47 | }, [chat.messages, welcomed]); 48 | 49 | // 重置会话 50 | const handleResetSession = async () => { 51 | try { 52 | if (sessionId) { 53 | await resetSession(sessionId); 54 | } 55 | const newSessionId = generateSessionId(); 56 | setSessionId(newSessionId); 57 | setBusinessMode(false); 58 | console.log('会话已重置,新会话ID:', newSessionId); 59 | antdMsg.success('会话已重置'); 60 | } catch (error) { 61 | console.error('重置会话失败:', error); 62 | antdMsg.error('重置会话失败'); 63 | } 64 | }; 65 | 66 | // 主输入处理:集成业务流程管理 67 | const handleSend = async () => { 68 | if (!input.trim() || !sessionId) return; 69 | 70 | const userMsg = { 71 | id: Date.now(), 72 | role: 'user', 73 | text: input.trim() 74 | }; 75 | updateChat(chat => ({ ...chat, messages: [...chat.messages, userMsg] })); 76 | setStreaming(true); 77 | 78 | const aiMsg = { 79 | id: Date.now() + 1, 80 | role: 'ai', 81 | text: '', 82 | status: 'thinking' 83 | }; 84 | updateChat(chat => ({ ...chat, messages: [...chat.messages, aiMsg] })); 85 | 86 | try { 87 | const eventSource = streamLLMChat( 88 | chat.messages.map(msg => ({ role: msg.role, content: msg.text })), 89 | input.trim(), 90 | sessionId, 91 | (token) => { 92 | // 流式渲染纯文本增量 93 | updateChat(chat => { 94 | const newMessages = [...chat.messages]; 95 | const lastMsg = newMessages[newMessages.length - 1]; 96 | if (lastMsg && lastMsg.role === 'ai') { 97 | lastMsg.text += token; // 直接拼接纯文本增量 98 | lastMsg.status = 'streaming'; 99 | } 100 | return { ...chat, messages: newMessages }; 101 | }); 102 | }, 103 | (error) => { 104 | console.error('AI回复错误:', error); 105 | antdMsg.error(`AI回复失败: ${error}`); 106 | updateChat(chat => { 107 | const newMessages = [...chat.messages]; 108 | const lastMsg = newMessages[newMessages.length - 1]; 109 | if (lastMsg && lastMsg.role === 'ai') { 110 | lastMsg.status = 'error'; 111 | lastMsg.text = `抱歉,AI回复出现错误: ${error}`; 112 | } 113 | return { ...chat, messages: newMessages }; 114 | }); 115 | setStreaming(false); 116 | }, 117 | (returnedSessionId) => { 118 | console.log('AI流式响应完成,会话ID:', returnedSessionId); 119 | setStreaming(false); 120 | 121 | // 更新会话ID(如果服务器返回了新的) 122 | if (returnedSessionId && returnedSessionId !== sessionId) { 123 | setSessionId(returnedSessionId); 124 | } 125 | 126 | updateChat(chat => { 127 | const newMessages = [...chat.messages]; 128 | const lastMsg = newMessages[newMessages.length - 1]; 129 | if (lastMsg && lastMsg.role === 'ai') { 130 | lastMsg.status = 'completed'; 131 | 132 | // 检查是否包含业务相关内容,启用业务模式指示 133 | const businessKeywords = ['流量', '宽带', '套餐', '副卡', '查询', '报修', '变更', '申请']; 134 | const hasBusinessContent = businessKeywords.some(keyword => 135 | lastMsg.text.includes(keyword) 136 | ); 137 | if (hasBusinessContent) { 138 | setBusinessMode(true); 139 | } 140 | } 141 | return { ...chat, messages: newMessages }; 142 | }); 143 | } 144 | ); 145 | setCurrentStream(eventSource); 146 | } catch (error) { 147 | console.error('streamLLMChat调用异常:', error); 148 | antdMsg.error('AI接口调用失败'); 149 | updateChat(chat => { 150 | const newMessages = [...chat.messages]; 151 | const lastMsg = newMessages[newMessages.length - 1]; 152 | if (lastMsg && lastMsg.role === 'ai') { 153 | lastMsg.status = 'error'; 154 | lastMsg.text = '抱歉,AI接口调用失败'; 155 | } 156 | return { ...chat, messages: newMessages }; 157 | }); 158 | setStreaming(false); 159 | } finally { 160 | setInput(''); 161 | } 162 | }; 163 | 164 | const handleKeyPress = (e) => { 165 | if (e.key === 'Enter' && !e.shiftKey) { 166 | e.preventDefault(); 167 | handleSend(); 168 | } 169 | }; 170 | 171 | useEffect(() => { 172 | return () => { 173 | if (currentStream) { 174 | currentStream.close(); 175 | } 176 | }; 177 | }, [currentStream]); 178 | 179 | return ( 180 |
181 | {/* 状态栏 */} 182 |
190 | 191 | 195 | 196 | 会话ID: {sessionId ? sessionId.slice(-8) : 'loading...'} 197 | 198 | 199 | 207 |
208 | 209 | {/* 消息列表 */} 210 |
211 | {chat.messages.map((message) => ( 212 | 217 | ))} 218 | {loading && ( 219 |
220 | 221 |
222 | AI正在思考中... 223 |
224 |
225 | )} 226 |
227 | 228 | {/* 输入区域 */} 229 |
230 | 231 |