123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565 |
- import sys,os
- current_path = os.getcwd()
- sys.path.append(current_path)
- from config.site import SiteConfig
- from fastapi import APIRouter, Depends, Query
- from db.database import get_db
- from sqlalchemy.orm import Session
- from sqlalchemy import text
- from agent.models.web.response import StandardResponse,FAILED,SUCCESS
- from agent.models.web.request import BasicRequest
- from agent.libs.graph import GraphBusiness
- from agent.libs.auth import verify_session_id, SessionValues
- import logging
- from typing import Optional, List
- from agent.models.db.graph import DbKgNode
- from agent.models.db.tree_structure import TreeStructure,KgGraphCategory
- import string
- import json
- from agent.tree_utils import get_tree_dto
- router = APIRouter(prefix="/kb", tags=["knowledge build interface"])
- logger = logging.getLogger(__name__)
- config = SiteConfig()
- LOG_DIR = config.get_config("TASK_LOG_DIR", current_path)
- # job_category = Column(String(64), nullable=False)
- # job_name = Column(String(64))
- # job_details = Column(Text, nullable=False)
- # job_creator = Column(String(64), nullable=False)
- # job_logs = Column(Text, nullable=True)
- # job_files = Column(String(300), nullable=True)
- @router.post('/summary', response_model=StandardResponse)
- def summary_func(request:BasicRequest, db: Session = Depends(get_db), sess:SessionValues = Depends(verify_session_id))->StandardResponse:
- if request.action != "get_summary":
- return StandardResponse(code=FAILED, message="invalid action")
- graph_id = request.get_param("graph_id",0)
- biz = GraphBusiness(db)
- summary = biz.get_graph_summary(graph_id=graph_id)
- if summary:
- logger.info(summary)
- return StandardResponse(code=SUCCESS, message="summary found", records=[summary])
- else:
- return StandardResponse(code=FAILED, message="summary not found",records=[])
- @router.post('/schemas', response_model=StandardResponse)
- def schemas_func(request:BasicRequest, db: Session = Depends(get_db), sess:SessionValues = Depends(verify_session_id))->StandardResponse:
- if request.action== "get_nodes_schemas":
- graph_id = request.get_param("graph_id",0)
- biz = GraphBusiness(db)
- schemas = biz.get_nodes_categories(graph_id=graph_id)
- if schemas:
- return StandardResponse(code=SUCCESS, message="schemas found", records=schemas)
- if request.action== "get_edges_schemas":
- graph_id = request.get_param("graph_id",0)
- biz = GraphBusiness(db)
- schemas = biz.get_edges_categories(graph_id=graph_id)
- if schemas:
- return StandardResponse(code=SUCCESS, message="schemas found", records=schemas)
- return StandardResponse(code=FAILED, message="invalid action")
- @router.post('/nodes', response_model=StandardResponse)
- def nodes_func(request:BasicRequest, db: Session = Depends(get_db), sess:SessionValues = Depends(verify_session_id))->StandardResponse:
- if (request.action == "search_nodes"):
- node_name = request.get_param("name","")
- category = request.get_param("category","")
- graph_id = request.get_param("graph_id",0)
- biz = GraphBusiness(db)
- if node_name == "":
- return StandardResponse(code=FAILED, message="node name is empty", records=[])
- if category == "":
- return StandardResponse(code=FAILED, message="category is empty", records=[])
- if graph_id == 0:
- return StandardResponse(code=FAILED, message="graph id is empty", records=[])
-
- nodes = biz.search_like_node_by_name(graph_id=graph_id, category=category, name=node_name)
- if nodes:
- return StandardResponse(code=SUCCESS, message="nodes found", records=nodes)
-
- else:
- return StandardResponse(code=FAILED, message="search job failed")
- elif (request.action == "get_nodes"):
- graph_id = request.get_param("graph_id",0)
- page = request.get_param("page",1)
- page_size = request.get_param("page_size",1)
- biz = GraphBusiness(db)
- nodes = biz.get_nodes_by_page(graph_id=graph_id, page=page, page_size=page_size)
- if nodes:
- return StandardResponse(code=SUCCESS, message="nodes found", records=nodes)
- elif (request.action == "neighbors"):
- graph_id = request.get_param("graph_id",0)
- node_id = request.get_param("node_id",0)
- if node_id>0:
- biz = GraphBusiness(db)
- node = biz.get_node_by_id(graph_id=graph_id, node_id=node_id)
- if node is None:
- return StandardResponse(code=FAILED, message="node not found", records=[])
- nodes_in = biz.get_neighbors(graph_id=graph_id, node_id=node_id, direction="in")
- nodes_out = biz.get_neighbors(graph_id=graph_id, node_id=node_id, direction="out")
- nodes_all = []
- nodes_all.append({"id": node.id, "name":node.name, "category":node.category, "direction":"self"})
- for node in nodes_in:
- nodes_all.append({"id": node.id, "name":node.name, "category":node.category, "direction":"in"})
- for node in nodes_out:
- nodes_all.append({"id": node.id, "name":node.name, "category":node.category, "direction":"out"})
-
- return StandardResponse(code=SUCCESS, message="nodes found", records=nodes_all)
- return StandardResponse(code=FAILED, message="invalid action")
- async def get_node_properties(node_id: int, db: Session) -> dict:
- """
- 查询节点的属性
- :param node_id: 节点ID
- :param db: 数据库会话
- :return: 属性字典
- """
- prop_sql = text("SELECT prop_title, prop_value FROM kg_props WHERE ref_id = :node_id")
- result = db.execute(prop_sql, {'node_id': node_id}).fetchall()
- properties = {}
- for row in result:
- properties[row._mapping['prop_title']] = row._mapping['prop_value']
- return properties
- @router.get("/graph_data", response_model=StandardResponse)
- async def get_graph_data(
- label_name: str,
- user_id: int,
- graph_id: int,
- db: Session = Depends(get_db),
- input_str: Optional[str] = None
- ):
- """
- 获取用户关联的图谱数据
- - 从session_id获取user_id
- - 查询DbUserDataRelation获取用户关联的数据
- - 返回与Java端一致的数据结构
- """
- try:
- # 1. 从session获取user_id
- # user_id = sess.user_id
- # if not user_id:
- # return StandardResponse(code=FAILED, message="user not found", records=[])
-
- # 处理input_str为空的情况
- if not input_str:
- # 根据user_id、graph_id和label_name从kg_nodes表中获取一个name
- get_name_sql = text("""
- SELECT n.name
- FROM user_data_relations udr
- JOIN kg_nodes n ON udr.data_id = n.id AND udr.data_category = 'DbKgNode'
- WHERE udr.user_id = :user_id
- AND n.category = :label_name
- AND n.status = '0'
- AND n.graph_id = :graph_id
- LIMIT 1
- """)
-
- name_result = db.execute(get_name_sql, {
- 'user_id': user_id,
- 'label_name': label_name,
- 'graph_id': graph_id
- }).fetchone()
-
- if not name_result:
- return StandardResponse(code=FAILED, message="No node found for given parameters", records=[])
-
- input_str = name_result._mapping['name']
-
- # 2. 使用JOIN查询用户关联的图谱数据
- sql = text("""
- WITH RankedRelations AS (
- SELECT
- e.name as rType,
- m.id as target_id,
- m.name as target_name,
- m.category as target_label,
- (SELECT COUNT(*) FROM kg_edges WHERE src_id = m.id) as pCount,
- ROW_NUMBER() OVER(PARTITION BY e.name ORDER BY m.id) as rn
- FROM user_data_relations udr
- JOIN kg_nodes n ON udr.data_id = n.id AND udr.data_category = 'DbKgNode'
- JOIN kg_edges e ON n.id = e.src_id
- JOIN kg_nodes m ON e.dest_id = m.id
- WHERE udr.user_id = :user_id
- AND n.category = :label_name
- AND n.name = :input_str
- AND n.status = '0'
- AND n.graph_id = :graph_id
- )
- SELECT rType, target_id, target_name, target_label, pCount
- FROM RankedRelations
- WHERE rn <= 50
- ORDER BY rType
- """)
-
- # 3. 组装返回数据结构
- categories = [{"name": "中心词"}, {"name": "关系"}]
- nodes = []
- links = []
-
- # 查询中心节点
- center_sql = text("""
- SELECT n.id, n.name, n.category
- FROM user_data_relations udr
- JOIN kg_nodes n ON udr.data_id = n.id AND udr.data_category = 'DbKgNode'
- WHERE udr.user_id = :user_id
- AND n.category = :label_name
- AND n.name = :input_str
- AND n.status = '0' AND n.graph_id= :graph_id limit 1
- """)
-
- # 执行查询并处理结果
- center_node = None
- c_map = {"中心词": 0, "关系": 1}
- node_id = 0
-
- # 构建graph_dto数据结构
- graph_dto = {
- "label": "",
- "name": "",
- "id": 0,
- "properties": {},
- "ENodeRSDTOS": []
- }
-
- # 1. 查询中心节点
- center_result = db.execute(center_sql, {
- 'user_id': user_id,
- 'label_name': label_name,
- 'input_str': input_str,
- 'graph_id': graph_id
- }).fetchall()
-
- if center_result:
- for row in center_result:
- graph_dto["label"] = row._mapping['category']
- graph_dto["name"] = row._mapping['name']
- graph_dto["id"] = row._mapping['id']
- graph_dto["properties"] = await get_node_properties(row._mapping['id'], db)
- break
-
- # 2. 查询关联的边和目标节点
- relation_result = db.execute(sql, {
- 'user_id': user_id,
- 'label_name': label_name,
- 'input_str': input_str,
- 'graph_id': graph_id
- }).fetchall()
-
- if relation_result:
- rs_id = 2
- for row in relation_result:
- r_type = row._mapping['rtype']
-
- # 添加到graph_dto
- e_node_rs = {
- "RType": r_type,
- "ENodeDTOS": [{
- "Label": row._mapping['target_label'],
- "Name": row._mapping['target_name'],
- "Id": row._mapping['target_id'],
- "PCount": row._mapping['pcount'],
- "properties": await get_node_properties(row._mapping['target_id'], db)
- }]
- }
-
- # 检查是否已有该关系类型
- existing_rs = next((rs for rs in graph_dto["ENodeRSDTOS"] if rs["RType"] == r_type), None)
- if existing_rs:
- existing_rs["ENodeDTOS"].extend(e_node_rs["ENodeDTOS"])
- else:
- graph_dto["ENodeRSDTOS"].append(e_node_rs)
-
- if r_type not in c_map:
- c_map[r_type] = rs_id
- categories.append({"name": r_type})
- rs_id += 1
-
- print("graph_dto:", graph_dto) # 打印graph_dto
-
- # 构建中心节点
- center_node = {
- "label": graph_dto["name"],
- 'type': graph_dto["label"],
- "category": 0,
- "name": "0",
- # "id": graph_dto["id"],
- "symbol": "circle",
- "symbolSize": 50,
- "properties": graph_dto["properties"],
- "nodeId": graph_dto["id"],
- "itemStyle": {"display": True}
- }
- nodes.append(center_node)
- # 处理关系类型
- rs_id = 2
-
- for rs in graph_dto["ENodeRSDTOS"]:
- r_type = rs["RType"]
-
- if r_type not in c_map:
- c_map[r_type] = rs_id
- categories.append({"name": r_type})
- rs_id += 1
-
- # 关系节点
- relation_node = {
- "label": "",
- 'type': graph_dto["label"],
- "category": 1,
- "name": str(len(nodes)),
- # "id": len(nodes),
- "symbol": "diamond",
- "symbolSize": 10,
- "properties": graph_dto["properties"],
- "nodeId": len(nodes),
- "itemStyle": {"display": True}
- }
- nodes.append(relation_node)
-
- # 添加链接
- links.append({
- "source": "0",
- "target": str(nodes.index(relation_node)),
- "value": r_type,
- "relationType": r_type
- })
- # 处理子节点
- for e_node in rs["ENodeDTOS"]:
- item_style = {"display": e_node["PCount"] > 0}
- child_node = {
- "label": e_node["Name"],
- "type": e_node["Label"],
- "category": c_map[r_type],
- "name": str(len(nodes)),
- # "id": e_node["Id"],
- "symbol": "circle",
- "symbolSize": 28,
- "properties": e_node["properties"],
- "nodeId": e_node["Id"],
- "itemStyle": item_style
- }
- nodes.append(child_node)
-
- links.append({
- "source": str(nodes.index(relation_node)),
- "target": str(nodes.index(child_node)),
- "value": "",
- "relationType": r_type
- })
-
- final_data = {
- "categories": categories,
- "node": nodes,
- "links": links
- }
- return StandardResponse(
- records=[{"records": final_data}],
- message="Graph data retrieved"
- )
- except Exception as e:
- return StandardResponse(
- code=500,
- message=str(e)
- )
-
-
- @router.get("/user_sub_graphs", response_model=StandardResponse)
- async def get_user_sub_graphs(
- user_id: int,
- pageNo: int = 1,
- pageSize: int = 10,
- db: Session = Depends(get_db)
- ):
- """
- 获取用户关联的子图列表
- - 根据user_id和data_category='sub_graph'查询user_data_relations表
- - 关联jobs表获取job_name
- - 返回data_id和job_name列表
- - 支持分页查询,参数pageNo(默认1)和pageSize(默认10)
- """
- try:
- # 查询用户关联的子图
- offset = (pageNo - 1) * pageSize
- sql = text("""
- SELECT udr.data_id, j.job_name
- FROM user_data_relations udr
- LEFT JOIN jobs j ON udr.data_id = j.id
- WHERE udr.user_id = :user_id
- AND udr.data_category = 'sub_graph' order by udr.data_id desc
- LIMIT :pageSize OFFSET :offset
- """)
-
- result = db.execute(sql, {'user_id': user_id, 'pageSize': pageSize, 'offset': offset}).fetchall()
-
- records = []
- for row in result:
- records.append({
- "graph_id": row._mapping['data_id'],
- "graph_name": row._mapping['job_name']
- })
-
- return StandardResponse(
- records=records,
- message="User sub graphs retrieved"
- )
- except Exception as e:
- return StandardResponse(
- code=500,
- message=str(e)
- )
- def build_disease_tree(disease_nodes: list, root_name: str = "疾病") -> dict:
- """
- 构建疾病树状结构的公共方法
- :param disease_nodes: 疾病节点列表,每个节点需包含name属性
- :param root_name: 根节点名称,默认为"疾病"
- :return: 树状结构字典
- """
- if not disease_nodes:
- return {"name": root_name, "sNode": []}
-
- # 按拼音首字母分类
- letter_groups = {letter: [] for letter in string.ascii_uppercase}
- letter_groups['其他'] = []
-
- for node in disease_nodes:
- name = node.name if hasattr(node, 'name') else str(node)
- first_letter = get_first_letter(name)
- letter_groups[first_letter].append(name)
-
- # 构建JSON结构
- tree_structure = {
- "name": root_name,
- "sNode": []
- }
-
- # 先添加A-Z的分类
- for letter in string.ascii_uppercase:
- if letter_groups[letter]:
- letter_node = {
- "name": letter,
- "sNode": [{"name": disease, "sNode": []} for disease in sorted(letter_groups[letter])]
- }
- tree_structure["sNode"].append(letter_node)
-
- # 最后添加"其他"分类(如果有的话)
- if letter_groups['其他']:
- other_node = {
- "name": "其他",
- "sNode": [{"name": disease, "sNode": []} for disease in sorted(letter_groups['其他'])]
- }
- tree_structure["sNode"].append(other_node)
- # content=json.dumps(tree_structure, ensure_ascii=False)
- # print(content)
- # tree_dto=get_tree_dto(content)
- # print(tree_dto)
- return tree_structure
- def get_first_letter(word):
- """获取中文词语的拼音首字母"""
- if not word:
- return '其他'
-
- # 获取第一个汉字的拼音首字母
- first_char = word[0]
- try:
- import pypinyin
- first_letter = pypinyin.pinyin(first_char, style=pypinyin.FIRST_LETTER)[0][0].upper()
- return first_letter if first_letter in string.ascii_uppercase else '其他'
- except Exception as e:
- print(str(e))
- return '其他'
- @router.get('/disease_tree')
- async def get_disease_tree(graph_id: int, db: Session = Depends(get_db)):
- """
- 根据graph_id查询kg_nodes表中category是疾病的数据并构建树状结构
- 严格按照字母A-Z顺序进行归类,中文首字母归类到对应拼音首字母
- """
- # 查询疾病节点
- disease_nodes = db.query(DbKgNode).filter(
- DbKgNode.graph_id == graph_id,
- DbKgNode.category == '疾病'
- ).all()
-
- tree_structure = build_disease_tree(disease_nodes)
- return StandardResponse(records=[{"records": tree_structure}])
-
- @router.get('/graph_categories')
- async def get_graph_categories(
- user_id: int,
- graph_id: int,
- db: Session = Depends(get_db)
- ):
- """
- 根据user_id和graph_id查询kg_graph_category表中的category列表
- 返回category的字符串列表(按照id正序排列)
- """
- try:
- # 查询category列表(按照id正序)
- categories = db.query(KgGraphCategory.category).filter(
- KgGraphCategory.user_id == user_id,
- KgGraphCategory.graph_id == graph_id
- ).order_by(KgGraphCategory.id).all()
-
- if not categories:
- return StandardResponse(code=FAILED, message="No categories found")
-
- # 转换为字符串列表
- category_list = [category[0] for category in categories]
-
- return StandardResponse(
- records=[{"records": category_list}],
- message="Graph categories retrieved"
- )
- except Exception as e:
- return StandardResponse(
- code=500,
- message=str(e)
- )
-
- @router.get('/tree_structure')
- async def get_tree_structure(
- user_id: int,
- graph_id: int,
- db: Session = Depends(get_db)
- ):
- """
- 根据user_id和graph_id获取树状结构数据
- 1. 查询kg_tree_structures表获取content
- 2. 调用get_tree_dto方法转换数据格式
- 3. 返回转换后的数据
- """
- try:
- # 查询树状结构数据
- tree_structure = db.query(TreeStructure).filter(
- TreeStructure.user_id == user_id,
- TreeStructure.graph_id == graph_id
- ).first()
-
- if not tree_structure:
- return StandardResponse(code=FAILED, message="Tree structure not found")
-
- # 转换数据格式
- tree_dto = get_tree_dto(tree_structure.content)
-
- return StandardResponse(
- records=[{"records": tree_dto}],
- message="Tree structure retrieved"
- )
- except Exception as e:
- return StandardResponse(
- code=500,
- message=str(e)
- )
- kb_router = router
|