From a26781be57dc228599306a5f63ef10648862f02e Mon Sep 17 00:00:00 2001 From: HuangHai <10402852@qq.com> Date: Wed, 2 Jul 2025 10:48:44 +0800 Subject: [PATCH] 'commit' --- dsRag/Neo4j/Backup/N3_InputShiTi.py | 6 +- dsRag/Neo4j/Backup/N4_PrintShiTi.py | 8 +- dsRag/Neo4j/N3_InsertIntoMysql.py | 63 +++++ dsRag/Neo4j/knowledge_points.sql | 10 + dsRag/Util/MySQLUtil.py | 239 +----------------- .../__pycache__/MySQLUtil.cpython-310.pyc | Bin 9499 -> 1477 bytes 6 files changed, 82 insertions(+), 244 deletions(-) create mode 100644 dsRag/Neo4j/N3_InsertIntoMysql.py create mode 100644 dsRag/Neo4j/knowledge_points.sql diff --git a/dsRag/Neo4j/Backup/N3_InputShiTi.py b/dsRag/Neo4j/Backup/N3_InputShiTi.py index 4944cc1c..7a64b78a 100644 --- a/dsRag/Neo4j/Backup/N3_InputShiTi.py +++ b/dsRag/Neo4j/Backup/N3_InputShiTi.py @@ -128,7 +128,7 @@ class StreamLLMClient: 3. 返回严格JSON格式: {{ "problem_types": ["题型"], - "knowledge_points": ["匹配的知识点"], + "knowledge_points.sql": ["匹配的知识点"], "literacy_points": ["匹配的素养点"] }} @@ -205,13 +205,13 @@ class ProblemAnalyzer: print("\n📊 分析结果:") print(f" 题型: {analysis.get('problem_types', [])}") - print(f" 知识点: {analysis.get('knowledge_points', [])}") + print(f" 知识点: {analysis.get('knowledge_points.sql', [])}") print(f" 素养点: {analysis.get('literacy_points', [])}") self.kg.store_analysis( question_id=self.question_id, content=self.content, - knowledge=analysis.get('knowledge_points', []), + knowledge=analysis.get('knowledge_points.sql', []), literacy=analysis.get('literacy_points', []) ) diff --git a/dsRag/Neo4j/Backup/N4_PrintShiTi.py b/dsRag/Neo4j/Backup/N4_PrintShiTi.py index ee535d41..8628951c 100644 --- a/dsRag/Neo4j/Backup/N4_PrintShiTi.py +++ b/dsRag/Neo4j/Backup/N4_PrintShiTi.py @@ -31,7 +31,7 @@ def query_all_questions(): OPTIONAL MATCH (q)-[:DEVELOPS_LITERACY]->(lp:LiteracyNode) RETURN q.content AS content, - collect(DISTINCT {id: kp.id, name: kp.name}) AS knowledge_points, + collect(DISTINCT {id: kp.id, name: kp.name}) AS knowledge_points.sql, collect(DISTINCT {id: lp.value, title: lp.title}) AS literacy_points """, qid=qid).data() @@ -39,7 +39,7 @@ def query_all_questions(): result = { "question_id": qid, "content": data[0]['content'], - "knowledge_points": data[0]['knowledge_points'], + "knowledge_points.sql": data[0]['knowledge_points.sql'], "literacy_points": data[0]['literacy_points'] } results.append(result) @@ -49,8 +49,8 @@ def query_all_questions(): print(f"📚 试题ID: {qid}") print(f"📝 内容全文: {result['content']}") # 新增完整内容输出 print(f"🔍 内容摘要: {result['content'][:50]}...") # 保留摘要显示 - print(f"🧠 知识点: {[kp['name'] for kp in result['knowledge_points']]}") - print(f"🆔 知识点ID: {[kp['id'] for kp in result['knowledge_points']]}") + print(f"🧠 知识点: {[kp['name'] for kp in result['knowledge_points.sql']]}") + print(f"🆔 知识点ID: {[kp['id'] for kp in result['knowledge_points.sql']]}") print(f"🌟 素养点: {[lp['title'] for lp in result['literacy_points']]}") print(f"🔢 素养点ID: {[lp['id'] for lp in result['literacy_points']]}") print('=' * 90) diff --git a/dsRag/Neo4j/N3_InsertIntoMysql.py b/dsRag/Neo4j/N3_InsertIntoMysql.py new file mode 100644 index 00000000..dfcd1476 --- /dev/null +++ b/dsRag/Neo4j/N3_InsertIntoMysql.py @@ -0,0 +1,63 @@ +import json +import logging +from typing import Dict, List +from Util.MySQLUtil import init_mysql_pool + +# 配置日志 +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + +async def process_node(node: Dict, parent_id: str = None) -> List[Dict]: + """处理单个知识点节点""" + knowledge_point = { + "id": node["value"], + "title": node["title"], + "parent_id": parent_id, + "is_leaf": node["isLeaf"], + "prerequisite": json.dumps(node.get("PREREQUISITE", [])), + "related": json.dumps(node.get("RELATED_TO", [])) + } + + children = [] + if not node["isLeaf"] and "children" in node: + for child in node["children"]: + children.extend(await process_node(child, node["value"])) + + return [knowledge_point] + children + +async def insert_knowledge_points(mysql_pool, knowledge_points: List[Dict]): + """批量插入知识点数据""" + async with mysql_pool.acquire() as conn: + await conn.ping() + async with conn.cursor() as cur: + for point in knowledge_points: + await cur.execute( + """INSERT INTO knowledge_points + (id, title, parent_id, is_leaf, prerequisite, related) + VALUES (%s, %s, %s, %s, %s, %s)""", + (point["id"], point["title"], point["parent_id"], + point["is_leaf"], point["prerequisite"], point["related"]) + ) + await conn.commit() + +async def main(): + """主函数""" + # 初始化MySQL连接池 + mysql_pool = await init_mysql_pool() + + # 读取JSON文件 + with open("d:\\dsWork\\dsProject\\dsRag\\Neo4j\\小学数学知识点体系.json", "r", encoding="utf-8") as f: + data = json.load(f) + + # 处理知识点数据 + knowledge_points = [] + for node in data["data"]["tree"]: + knowledge_points.extend(await process_node(node)) + + # 插入数据库 + await insert_knowledge_points(mysql_pool, knowledge_points) + logger.info(f"成功插入 {len(knowledge_points)} 条知识点数据") + +if __name__ == "__main__": + import asyncio + asyncio.run(main()) \ No newline at end of file diff --git a/dsRag/Neo4j/knowledge_points.sql b/dsRag/Neo4j/knowledge_points.sql new file mode 100644 index 00000000..9c7f9185 --- /dev/null +++ b/dsRag/Neo4j/knowledge_points.sql @@ -0,0 +1,10 @@ +CREATE TABLE knowledge_points ( + id VARCHAR(32) PRIMARY KEY COMMENT '知识点唯一标识', + title VARCHAR(100) NOT NULL COMMENT '知识点标题', + parent_id VARCHAR(32) COMMENT '父节点ID', + is_leaf BOOLEAN NOT NULL COMMENT '是否为叶子节点', + sort INT COMMENT '排序字段', + prerequisite JSON COMMENT '先修知识点(仅一级节点)', + related JSON COMMENT '相关知识点(仅一级节点)', + KEY idx_parent_id (parent_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='小学数学知识点体系'; \ No newline at end of file diff --git a/dsRag/Util/MySQLUtil.py b/dsRag/Util/MySQLUtil.py index 0302e558..a6269e93 100644 --- a/dsRag/Util/MySQLUtil.py +++ b/dsRag/Util/MySQLUtil.py @@ -2,10 +2,11 @@ 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__) @@ -41,241 +42,5 @@ async def save_chat_to_mysql(mysql_pool, person_id, prompt, result, audio_url, d 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 index c310e899a1723242da50d667b42f473ee8a3d388..45ab5be6a9517bb2b1b8d2bbbf822fa07dba746f 100644 GIT binary patch delta 502 zcmYjN&r2LJ7)|2NOlD_)iU*5eh26`7XAxPnq9{tMh*B_@F!+Vt$n0j!%(Qsx!Kr3-eNjU` znNIrXa(?;%BfwCKN(V^bW5=`$4!Gw88-C{rdXJee$|4XIQ57}tfP#PHu{x9?I76)A zdp?F5RG@l>Srf=U>eTZ^{4rPf_bf4ciprP6{k_k8ZFgs5Yg56jXRly-vmGEhsI9v!D(_5&E;LEwK8ke zj-y?3i~-x}6*`lJ$2q7Y&I>P6y`CKQ7!9QwS!j|!wU%h%T1Du5p}oFRx9^)4to8e! hv2+xMb_WWwNG#hmN!vgP(TNJ7q%o2IZBLWh^9yfBe9Hg; literal 9499 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