From 93f3ab1ed9e5cef5b4c4734c7b5bc1b7b27f1990 Mon Sep 17 00:00:00 2001 From: HuangHai <10402852@qq.com> Date: Tue, 24 Jun 2025 18:55:48 +0800 Subject: [PATCH] 'commit' --- dsRag/Config/Config.py | 8 + .../Config/__pycache__/Config.cpython-310.pyc | Bin 515 -> 664 bytes dsRag/Sql/t_ai_kb.sql | 61 ++++ dsRag/Start.py | 137 +++++++++ dsRag/Util/MySQLUtil.py | 281 ++++++++++++++++++ .../__pycache__/MySQLUtil.cpython-310.pyc | Bin 0 -> 9499 bytes 6 files changed, 487 insertions(+) create mode 100644 dsRag/Sql/t_ai_kb.sql create mode 100644 dsRag/Start.py create mode 100644 dsRag/Util/MySQLUtil.py create mode 100644 dsRag/Util/__pycache__/MySQLUtil.cpython-310.pyc diff --git a/dsRag/Config/Config.py b/dsRag/Config/Config.py index 520bee08..db0bf35c 100644 --- a/dsRag/Config/Config.py +++ b/dsRag/Config/Config.py @@ -13,3 +13,11 @@ WORD2VEC_MODEL_PATH = r"D:\Tencent_AILab_ChineseEmbedding\Tencent_AILab_ChineseE # DeepSeek DEEPSEEK_API_KEY = 'sk-44ae895eeb614aa1a9c6460579e322f1' DEEPSEEK_URL = 'https://api.deepseek.com' + + +# MYSQL配置信息 +MYSQL_HOST = "10.10.14.210" +MYSQL_PORT = 22066 +MYSQL_USER = "root" +MYSQL_PASSWORD = "DsideaL147258369" +MYSQL_DB_NAME = "base_db" \ No newline at end of file diff --git a/dsRag/Config/__pycache__/Config.cpython-310.pyc b/dsRag/Config/__pycache__/Config.cpython-310.pyc index 6451932ce1563fd0c3f9d17ce898b48bc1c6e41c..a05d9cfa3d276603ab05f6371b37d9f924e68390 100644 GIT binary patch delta 308 zcmZo>nZe4J&&$ij00e)VqB6>uCi2NJnoQJIl!#(U;f!KU;fi8Q;f`WY;fdl%;SFZc zUmjncSdc$xo9r zYH~WGd=5~Jt7|~8tE+dsV}NJ8w`*h+4_qWP$mbT9Z)9+wPrQeJa0rYY;2#8MhX%U_ z-Qt6&32+P!4)+goxy1|NyEw)BIr_S;WGLbQ`oD-{@_I%)1sNcdiHV630@;4EaI!G7 NFtV@!F%WVv0syiLQ_lbZ delta 159 zcmbQi+RVb2&&$ij00jPSkr_7`C-TWKDooT?WC>=_)|c3`LwkON%%sTQS**2?3c*OiYXr$o89slZBas5ddkYEd~Gp diff --git a/dsRag/Sql/t_ai_kb.sql b/dsRag/Sql/t_ai_kb.sql new file mode 100644 index 00000000..d83cbfbe --- /dev/null +++ b/dsRag/Sql/t_ai_kb.sql @@ -0,0 +1,61 @@ +/* + Navicat Premium Dump SQL + + Source Server : 10.10.14.210 + Source Server Type : MySQL + Source Server Version : 50742 (5.7.42-log) + Source Host : 10.10.14.210:22066 + Source Schema : base_db + + Target Server Type : MySQL + Target Server Version : 50742 (5.7.42-log) + File Encoding : 65001 + + Date: 24/06/2025 18:45:23 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for t_ai_kb +-- ---------------------------- +DROP TABLE IF EXISTS `t_ai_kb`; +CREATE TABLE `t_ai_kb` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', + `kb_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '知识库名称', + `short_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '英文简称', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `is_delete` int(11) NOT NULL DEFAULT 0 COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `short_name`(`short_name`) USING BTREE, + INDEX `is_delete`(`is_delete`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI知识库' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of t_ai_kb +-- ---------------------------- + +-- ---------------------------- +-- Table structure for t_ai_kb_files +-- ---------------------------- +DROP TABLE IF EXISTS `t_ai_kb_files`; +CREATE TABLE `t_ai_kb_files` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', + `file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '文件名称', + `ext_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '文件扩展名', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `kb_id` int(11) NOT NULL COMMENT '隶属知识库ID', + `is_delete` int(11) NOT NULL DEFAULT 0 COMMENT '是否删除', + `state` int(11) NOT NULL DEFAULT 0 COMMENT '0:上传后未处理,1:上传后已处理,2:处理失败', + PRIMARY KEY (`id`) USING BTREE, + INDEX `kb_id`(`kb_id`) USING BTREE, + INDEX `is_delete`(`is_delete`, `state`) USING BTREE, + CONSTRAINT `t_ai_kb_files_ibfk_1` FOREIGN KEY (`kb_id`) REFERENCES `t_ai_kb` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI知识库上传的文件' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of t_ai_kb_files +-- ---------------------------- + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/dsRag/Start.py b/dsRag/Start.py new file mode 100644 index 00000000..1c452d94 --- /dev/null +++ b/dsRag/Start.py @@ -0,0 +1,137 @@ +""" +pip install fastapi uvicorn aiomysql +""" +import uvicorn +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel + +from Util.MySQLUtil import * + +""" +API文档访问: http://localhost:8000/docs +该实现包含以下功能: + +- 知识库(t_ai_kb)的增删改查接口 +- 知识库文件(t_ai_kb_files)的增删改查接口 +- 使用MySQLUtil.py中的连接池管理 +- 自动生成的Swagger文档 +""" + +app = FastAPI() + +# 知识库模型 +class KbModel(BaseModel): + kb_name: str + short_name: str + is_delete: Optional[int] = 0 + +# 知识库文件模型 +class KbFileModel(BaseModel): + file_name: str + ext_name: str + kb_id: int + is_delete: Optional[int] = 0 + state: Optional[int] = 0 + +@app.on_event("startup") +async def startup_event(): + app.state.mysql_pool = await init_mysql_pool() + +@app.on_event("shutdown") +async def shutdown_event(): + app.state.mysql_pool.close() + await app.state.mysql_pool.wait_closed() + +# 知识库CRUD接口 +@app.post("/kb") +async def create_kb(kb: KbModel): + async with app.state.mysql_pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute( + """INSERT INTO t_ai_kb (kb_name, short_name, is_delete) + VALUES (%s, %s, %s)""", + (kb.kb_name, kb.short_name, kb.is_delete) + ) + await conn.commit() + return {"id": cur.lastrowid} + +@app.get("/kb/{kb_id}") +async def read_kb(kb_id: int): + async with app.state.mysql_pool.acquire() as conn: + async with conn.cursor(DictCursor) as cur: + await cur.execute("SELECT * FROM t_ai_kb WHERE id = %s", (kb_id,)) + result = await cur.fetchone() + if not result: + raise HTTPException(status_code=404, detail="Knowledge base not found") + return result + +@app.put("/kb/{kb_id}") +async def update_kb(kb_id: int, kb: KbModel): + async with app.state.mysql_pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute( + """UPDATE t_ai_kb + SET kb_name = %s, short_name = %s, is_delete = %s + WHERE id = %s""", + (kb.kb_name, kb.short_name, kb.is_delete, kb_id) + ) + await conn.commit() + return {"message": "Knowledge base updated"} + +@app.delete("/kb/{kb_id}") +async def delete_kb(kb_id: int): + async with app.state.mysql_pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute("DELETE FROM t_ai_kb WHERE id = %s", (kb_id,)) + await conn.commit() + return {"message": "Knowledge base deleted"} + +# 知识库文件CRUD接口 +@app.post("/kb_files") +async def create_kb_file(file: KbFileModel): + async with app.state.mysql_pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute( + """INSERT INTO t_ai_kb_files + (file_name, ext_name, kb_id, is_delete, state) + VALUES (%s, %s, %s, %s, %s)""", + (file.file_name, file.ext_name, file.kb_id, file.is_delete, file.state) + ) + await conn.commit() + return {"id": cur.lastrowid} + +@app.get("/kb_files/{file_id}") +async def read_kb_file(file_id: int): + async with app.state.mysql_pool.acquire() as conn: + async with conn.cursor(DictCursor) as cur: + await cur.execute("SELECT * FROM t_ai_kb_files WHERE id = %s", (file_id,)) + result = await cur.fetchone() + if not result: + raise HTTPException(status_code=404, detail="File not found") + return result + +@app.put("/kb_files/{file_id}") +async def update_kb_file(file_id: int, file: KbFileModel): + async with app.state.mysql_pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute( + """UPDATE t_ai_kb_files + SET file_name = %s, ext_name = %s, kb_id = %s, + is_delete = %s, state = %s + WHERE id = %s""", + (file.file_name, file.ext_name, file.kb_id, + file.is_delete, file.state, file_id) + ) + await conn.commit() + return {"message": "File updated"} + +@app.delete("/kb_files/{file_id}") +async def delete_kb_file(file_id: int): + async with app.state.mysql_pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute("DELETE FROM t_ai_kb_files WHERE id = %s", (file_id,)) + await conn.commit() + return {"message": "File deleted"} + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/dsRag/Util/MySQLUtil.py b/dsRag/Util/MySQLUtil.py new file mode 100644 index 00000000..0302e558 --- /dev/null +++ b/dsRag/Util/MySQLUtil.py @@ -0,0 +1,281 @@ +""" +pip install aiomysql +""" +import logging +from typing import Optional, Dict, List + +from aiomysql import create_pool +from Config.Config import * +# 配置日志 +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + +# MySQL 配置 +MYSQL_CONFIG = { + "host": MYSQL_HOST, + "port": MYSQL_PORT, + "user": MYSQL_USER, + "password": MYSQL_PASSWORD, + "db": MYSQL_DB_NAME, + "minsize": 1, + "maxsize": 20, +} + + +# 初始化 MySQL 连接池 +async def init_mysql_pool(): + return await create_pool(**MYSQL_CONFIG) + + +# 保存聊天记录到 MySQL +async def save_chat_to_mysql(mysql_pool, person_id, prompt, result, audio_url, duration, input_type=1, output_type=1, + input_image_type=0, image_width=0, image_height=0): + async with mysql_pool.acquire() as conn: + await conn.ping() # 重置连接 + async with conn.cursor() as cur: + await cur.execute( + "INSERT INTO t_chat_log (person_id, user_input, model_response,audio_url,duration,input_type,output_type,input_image_type,image_width,image_height,create_time) VALUES (%s, %s, %s, %s, %s, %s, %s,%s,%s,%s,NOW())", + (person_id, prompt, result, audio_url, duration, input_type, output_type, input_image_type, image_width, + image_height) + ) + await conn.commit() + + +# 清空表 +async def truncate_chat_log(mysql_pool): + async with mysql_pool.acquire() as conn: + await conn.ping() # 重置连接 + async with conn.cursor() as cur: + await cur.execute("TRUNCATE TABLE t_chat_log") + await conn.commit() + logger.info("表 t_chat_log 已清空。") + + +from aiomysql import DictCursor + + +# 分页查询聊天记录 +async def get_chat_log_by_session(mysql_pool, person_id, page=1, page_size=10): + """ + 根据 person_id 查询聊天记录,并按 id 降序分页 + :param mysql_pool: MySQL 连接池 + :param person_id: 用户会话 ID + :param page: 当前页码(默认值为 1,但会动态计算为最后一页) + :param page_size: 每页记录数 + :return: 分页数据 + """ + if not mysql_pool: + raise ValueError("MySQL 连接池未初始化") + + async with mysql_pool.acquire() as conn: + await conn.ping() # 重置连接 + async with conn.cursor(DictCursor) as cur: # 使用 DictCursor + # 查询总记录数 + await cur.execute( + "SELECT COUNT(*) FROM t_chat_log WHERE person_id = %s", + (person_id,) + ) + total = (await cur.fetchone())['COUNT(*)'] + + # 计算总页数 + total_pages = (total + page_size - 1) // page_size + + # 计算偏移量 + offset = (page - 1) * page_size + + # 查询分页数据,按 id 降序排列 + await cur.execute( + "SELECT id, person_id, user_input, model_response, audio_url, duration,input_type,output_type,input_image_type,image_width,image_height, create_time " + "FROM t_chat_log WHERE person_id = %s ORDER BY id DESC LIMIT %s OFFSET %s", + (person_id, page_size, offset) + ) + records = await cur.fetchall() + + # 将查询结果反转,确保最新消息显示在最后 + if page==1 and records: + records.reverse() + + # 将查询结果转换为字典列表 + result = [ + { + "id": record['id'], + "person_id": record['person_id'], + "user_input": record['user_input'], + "model_response": record['model_response'], + "audio_url": record['audio_url'], + "duration": record['duration'], + "input_type": record['input_type'], + "output_type": record['output_type'], + "image_width": record['image_width'], + "image_height": record['image_height'], + "input_image_type": record['input_image_type'], + "create_time": record['create_time'].strftime("%Y-%m-%d %H:%M:%S") + } + for record in records + ] + + return { + "data": result, # 按 id 升序排列的数据 + "total": total, + "page": page, + "page_size": page_size, + "total_pages": total_pages + } + + +# 获取指定会话的最后一条记录的 id +async def get_last_chat_log_id(mysql_pool, person_id): + """ + 获取指定会话的最后一条记录的 id + :param mysql_pool: MySQL 连接池 + :param session_id: 用户会话 ID + :return: 最后一条记录的 id,如果未找到则返回 None + """ + async with mysql_pool.acquire() as conn: + await conn.ping() # 重置连接 + async with conn.cursor() as cur: + await cur.execute( + "SELECT id FROM t_chat_log WHERE person_id = %s ORDER BY id DESC LIMIT 1", + (person_id,) + ) + result = await cur.fetchone() + return result[0] if result else None + + +# 更新为危险的记录 +async def update_risk(mysql_pool, person_id, risk_memo): + async with mysql_pool.acquire() as conn: + await conn.ping() # 重置连接 + async with conn.cursor() as cur: + # 1. 获取此人员的最后一条记录 id + last_id = await get_last_chat_log_id(mysql_pool, person_id) + + if last_id: + # 2. 更新 risk_flag 和 risk_memo + await cur.execute( + "UPDATE t_chat_log SET risk_flag = 1, risk_memo = %s WHERE id = %s", + (risk_memo.replace('\n', '').replace("NO", ""), last_id) + ) + await conn.commit() + logger.info(f"已更新 person_id={person_id} 的最后一条记录 (id={last_id}) 的 risk_flag 和 risk_memo。") + else: + logger.warning(f"未找到 person_id={person_id} 的记录。") + + +# 查询用户信息 +async def get_user_by_login_name(mysql_pool, login_name: str) -> Optional[Dict]: + """ + 根据用户名查询用户信息 + :param pool: MySQL 连接池 + :param login_name: 用户名 + :return: 用户信息(字典形式) + """ + async with mysql_pool.acquire() as conn: + await conn.ping() # 重置连接 + async with conn.cursor() as cursor: + sql = "SELECT * FROM t_base_person WHERE login_name = %s" + await cursor.execute(sql, (login_name,)) + row = await cursor.fetchone() + if not row: + return None + + # 将元组转换为字典 + columns = [column[0] for column in cursor.description] + return dict(zip(columns, row)) + + +# 显示统计分析页面 +async def get_chat_logs_summary(mysql_pool, risk_flag: int, offset: int, page_size: int) -> (List[Dict], int): + """ + 获取聊天记录的统计分析结果 + :param mysql_pool: MySQL 连接池 + :param risk_flag: 风险标志 + :param offset: 偏移量 + :param page_size: 每页记录数 + :return: 日志列表和总记录数 + """ + async with mysql_pool.acquire() as conn: + await conn.ping() # 重置连接 + async with conn.cursor() as cursor: + # 查询符合条件的记录 + sql = """ + SELECT tbp.*, COUNT(*) AS cnt + FROM t_chat_log AS tcl + INNER JOIN t_base_person AS tbp ON tcl.person_id = tbp.person_id + WHERE tcl.risk_flag = %s + GROUP BY tcl.person_id + ORDER BY COUNT(*) DESC + LIMIT %s OFFSET %s + """ + await cursor.execute(sql, (risk_flag, page_size, offset)) + rows = await cursor.fetchall() + + # 获取列名 + columns = [column[0] for column in cursor.description] + + # 查询总记录数 + count_sql = """ + SELECT COUNT(DISTINCT tcl.person_id) + FROM t_chat_log AS tcl + INNER JOIN t_base_person AS tbp ON tcl.person_id = tbp.person_id + WHERE tcl.risk_flag = %s + """ + await cursor.execute(count_sql, (risk_flag,)) + total = (await cursor.fetchone())[0] + + # 将元组转换为字典 + logs = [dict(zip(columns, row)) for row in rows] if rows else [] + + return logs, total + + +async def get_chat_logs_by_risk_flag(mysql_pool, risk_flag: int, person_id: str, offset: int, page_size: int) -> ( + List[Dict], int): + """ + 根据风险标志查询聊天记录 + :param mysql_pool: MySQL 连接池 + :param risk_flag: 风险标志 + :param offset: 分页偏移量 + :param page_size: 每页记录数 + :return: 聊天记录列表和总记录数 + """ + async with mysql_pool.acquire() as conn: + await conn.ping() # 重置连接 + async with conn.cursor() as cursor: + # 查询符合条件的记录 + sql = """ + SELECT tcl.id, tcl.user_input, tcl.model_response, tcl.audio_url, tcl.duration, + tcl.create_time, tcl.risk_flag, tcl.risk_memo, tcl.risk_result, + tbp.person_id, tbp.login_name, tbp.person_name + FROM t_chat_log AS tcl + INNER JOIN t_base_person AS tbp ON tcl.person_id = tbp.person_id + WHERE tcl.risk_flag = %s and tcl.person_id=%s ORDER BY TCL.ID DESC + LIMIT %s OFFSET %s + """ + await cursor.execute(sql, (risk_flag, person_id, page_size, offset)) + rows = await cursor.fetchall() + + # 在 count_sql 执行前获取列名 + columns = [column[0] for column in cursor.description] + + # 查询总记录数 + count_sql = """ + SELECT COUNT(*) + FROM t_chat_log AS tcl + INNER JOIN t_base_person AS tbp ON tcl.person_id = tbp.person_id + WHERE tcl.risk_flag = %s and tcl.person_id=%s + """ + await cursor.execute(count_sql, (risk_flag, person_id)) + total = (await cursor.fetchone())[0] + + # 将元组转换为字典,并格式化 create_time + if rows: + logs = [] + for row in rows: + log = dict(zip(columns, row)) + # 格式化 create_time + if log["create_time"]: + log["create_time"] = log["create_time"].strftime("%Y-%m-%d %H:%M:%S") + logs.append(log) + return logs, total + return [], 0 diff --git a/dsRag/Util/__pycache__/MySQLUtil.cpython-310.pyc b/dsRag/Util/__pycache__/MySQLUtil.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c310e899a1723242da50d667b42f473ee8a3d388 GIT binary patch literal 9499 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