Browse Source

代码提交

SGTY 1 month ago
parent
commit
26c80794e0
44 changed files with 1391 additions and 1 deletions
  1. 0 0
      build/lib/knowledge/__init__.py
  2. 0 0
      build/lib/knowledge/db/__init__.py
  3. 10 0
      build/lib/knowledge/db/base_class.py
  4. 38 0
      build/lib/knowledge/db/session.py
  5. 17 0
      build/lib/knowledge/main.py
  6. 1 0
      build/lib/knowledge/middlewares/__init__.py
  7. 37 0
      build/lib/knowledge/middlewares/api_route.py
  8. 206 0
      build/lib/knowledge/middlewares/base.py
  9. 0 0
      build/lib/knowledge/model/__init__.py
  10. 16 0
      build/lib/knowledge/model/kg_edges.py
  11. 16 0
      build/lib/knowledge/model/kg_node.py
  12. 18 0
      build/lib/knowledge/model/kg_prop.py
  13. 10 0
      build/lib/knowledge/model/response.py
  14. 0 0
      build/lib/knowledge/router/__init__.py
  15. 17 0
      build/lib/knowledge/router/base.py
  16. 120 0
      build/lib/knowledge/router/knowledge_nodes_api.py
  17. 55 0
      build/lib/knowledge/server.py
  18. 0 0
      build/lib/knowledge/service/__init__.py
  19. 106 0
      build/lib/knowledge/service/kg_edge_service.py
  20. 220 0
      build/lib/knowledge/service/kg_node_service.py
  21. 66 0
      build/lib/knowledge/service/kg_prop_service.py
  22. 0 0
      build/lib/knowledge/settings/__init__.py
  23. 19 0
      build/lib/knowledge/settings/auth_setting.py
  24. 8 0
      build/lib/knowledge/settings/base_setting.py
  25. 24 0
      build/lib/knowledge/settings/log_setting.py
  26. 64 0
      build/lib/knowledge/utils/ObjectToJsonArrayConverter.py
  27. 0 0
      build/lib/knowledge/utils/__init__.py
  28. 5 0
      build/lib/knowledge/utils/context_util.py
  29. 11 0
      build/lib/knowledge/utils/embed_helper.py
  30. 39 0
      build/lib/knowledge/utils/license.py
  31. 27 0
      build/lib/knowledge/utils/log_util.py
  32. 26 0
      build/lib/knowledge/utils/trace_util.py
  33. 41 0
      build/lib/knowledge/utils/vector_distance.py
  34. 34 0
      build/lib/knowledge/utils/vectorizer.py
  35. 16 0
      src/knowledge.egg-info/PKG-INFO
  36. 43 0
      src/knowledge.egg-info/SOURCES.txt
  37. 1 0
      src/knowledge.egg-info/dependency_links.txt
  38. 2 0
      src/knowledge.egg-info/entry_points.txt
  39. 12 0
      src/knowledge.egg-info/requires.txt
  40. 1 0
      src/knowledge.egg-info/top_level.txt
  41. 1 1
      src/knowledge/.env
  42. 5 0
      src/knowledge/router/knowledge_nodes_api.py
  43. 4 0
      src/logs/error.log
  44. 55 0
      src/logs/server.log

+ 0 - 0
build/lib/knowledge/__init__.py


+ 0 - 0
build/lib/knowledge/db/__init__.py


+ 10 - 0
build/lib/knowledge/db/base_class.py

@@ -0,0 +1,10 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy import event
+from sqlalchemy import DDL
+
+Base = declarative_base()
+
+# 启用pgvector扩展
+event.listens_for(Base.metadata, 'before_create')
+def enable_vector_extension(target, connection, **kw):
+    connection.execute(DDL('CREATE EXTENSION IF NOT EXISTS vector'))

+ 38 - 0
build/lib/knowledge/db/session.py

@@ -0,0 +1,38 @@
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker, scoped_session
+import os
+
+# 数据库配置
+# 远程PostgreSQL数据库连接配置
+# 从环境变量获取数据库连接信息,如果未设置则使用默认值
+DB_HOST = os.getenv("DB_HOST", "173.18.12.203")
+DB_PORT = os.getenv("DB_PORT", "5432")
+DB_USER = os.getenv("DB_USER", "knowledge")
+DB_PASS = os.getenv("DB_PASSWORD", "qwer1234.")
+DB_NAME = os.getenv("DB_NAME", "postgres")
+
+DATABASE_URL = f"postgresql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
+
+engine = create_engine(
+    DATABASE_URL,
+    pool_size=20,
+    max_overflow=10,
+    pool_pre_ping=True,
+    connect_args={'options': '-c search_path=public'},
+    echo=True  # 开启SQL日志
+)
+
+SessionLocal = sessionmaker(
+    autocommit=False,
+    autoflush=False,
+    bind=engine
+)
+
+session = scoped_session(SessionLocal)
+
+def get_db():
+    db = SessionLocal()
+    try:
+        yield db
+    finally:
+        db.close()

+ 17 - 0
build/lib/knowledge/main.py

@@ -0,0 +1,17 @@
+# 导入FastAPI及相关模块
+import uvicorn
+from py_tools.logging import logger
+
+from .settings import base_setting
+from .server import app
+
+def main():
+    logger.info(f"project run {base_setting.server_host}:{base_setting.server_port}")
+    uvicorn.run(
+        app=app, host=base_setting.server_host, port=base_setting.server_port, log_level=base_setting.server_log_level,
+        access_log=False
+    )
+
+if __name__ == "__main__":
+    main()
+

+ 1 - 0
build/lib/knowledge/middlewares/__init__.py

@@ -0,0 +1 @@
+

+ 37 - 0
build/lib/knowledge/middlewares/api_route.py

@@ -0,0 +1,37 @@
+# @Desc: { 路由中间件 }
+import time
+from typing import Callable
+
+from fastapi.requests import Request
+from fastapi.responses import Response
+from fastapi.routing import APIRoute
+from py_tools.logging import logger
+
+
+class LoggingAPIRoute(APIRoute):
+    def get_route_handler(self) -> Callable:
+        original_route_handler = super().get_route_handler()
+
+        async def log_route_handler(request: Request) -> Response:
+            """日志记录请求信息与处理耗时"""
+            req_log_info = f"--> {request.method} {request.url.path} {request.client.host}:{request.client.port}"
+            if request.query_params:
+                req_log_info += f"\n--> Query Params: {request.query_params}"
+
+            if "application/json" in request.headers.get("Content-Type", ""):
+                try:
+                    json_body = await request.json()
+                    req_log_info += f"\n--> json_body: {json_body}"
+                except Exception:
+                    logger.exception("Failed to parse JSON body")
+
+            logger.info(req_log_info)
+            start_time = time.perf_counter()
+            response: Response = await original_route_handler(request)
+            process_time = time.perf_counter() - start_time
+            response.headers["X-Response-Time"] = str(process_time)
+            resp_log_info = f"<-- {response.status_code} {request.url.path} (took: {process_time:.5f}s)"
+            logger.info(resp_log_info)  # 处理大量并发请求时,记录请求日志信息会影响服务性能,可以用nginx代替
+            return response
+
+        return log_route_handler

+ 206 - 0
build/lib/knowledge/middlewares/base.py

@@ -0,0 +1,206 @@
+# @Desc: { 模块描述 }
+import time
+
+from fastapi import Request, status
+from fastapi.middleware import Middleware
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import Response
+from py_tools.logging import logger
+from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
+
+from ..settings import auth_setting
+from ..utils.trace_util import TraceUtil
+from typing import Optional
+
+
+class LoggingMiddleware(BaseHTTPMiddleware):
+    """
+    日志中间件
+    记录请求参数信息、计算响应时间
+    """
+
+    async def set_body(self, request: Request):
+        receive_ = await request._receive()
+
+        async def receive():
+            return receive_
+
+        request._receive = receive
+
+    async def dispatch(self, request: Request, call_next) -> Response:
+        start_time = time.perf_counter()
+
+        # 打印请求信息
+        request_id = TraceUtil.get_req_id()
+        logger.info(f"--> {request.method} {request.url.path} {request.client.host}")
+        if request.query_params:
+            logger.info(f"--> Query Params: {request.query_params}")
+
+        if "application/json" in request.headers.get("Content-Type", ""):
+            await self.set_body(request)
+            try:
+                # starlette 中间件中不能读取请求数据,否则会进入循环等待 需要特殊处理或者换APIRoute实现
+                body = await request.json()
+                logger.info(f"--> Request Body: {body}")
+            except Exception as e:
+                logger.warning(f"Failed to parse JSON body: {e}")
+
+        # 执行请求获取响应
+        response = await call_next(request)
+
+        # 计算响应时间
+        process_time = time.perf_counter() - start_time
+        response.headers["X-Response-Time"] = f"{process_time:.2f}s"
+
+        # 记录响应基本信息
+        logger.info(f"<-- Response: [Status: {response.status_code}, Type: {response.__class__.__name__}, Headers: {dict(response.headers)}]")
+
+        # 尝试记录响应体
+        try:
+            from starlette.responses import StreamingResponse, JSONResponse, Response
+            import json
+
+            # 针对不同类型的响应采用不同的解析方式
+            if isinstance(response, StreamingResponse):
+                logger.info("<-- Streaming response body (skipped logging)")
+                return response
+
+            # 获取响应体内容
+            body = None
+            if isinstance(response, JSONResponse):
+                body = response.body
+            elif hasattr(response, 'body'):
+                try:
+                    body = await response.body()
+                except Exception as e:
+                    logger.warning(f"<-- Failed to get response body: {e.__class__.__name__}: {str(e)}")
+                    return response
+
+            # 解析并记录响应体
+            if body:
+                try:
+                    if isinstance(body, bytes):
+                        body = body.decode('utf-8')
+                    if isinstance(body, str):
+                        # 尝试解析为JSON
+                        try:
+                            body_json = json.loads(body)
+                            logger.info(f"<-- Response Body (JSON): {body_json}")
+                        except json.JSONDecodeError:
+                            # 如果不是JSON格式,直接记录字符串内容
+                            logger.info(f"<-- Response Body (Text): {body}")
+                except UnicodeDecodeError as e:
+                    logger.warning(f"<-- Failed to decode response body: {str(e)}")
+                except Exception as e:
+                    logger.warning(f"<-- Failed to process response body: {e.__class__.__name__}: {str(e)}")
+        except Exception as e:
+            logger.warning(f"<-- Failed to handle response body: {e.__class__.__name__}: {str(e)}")
+
+
+        logger.info(f"<-- {response.status_code} {request.url.path} (took: {process_time:.2f}s)")
+
+        return response
+
+
+class TraceReqMiddleware(BaseHTTPMiddleware):
+    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
+        # 设置请求id
+        request_id = TraceUtil.set_req_id(request.headers.get("X-Request-ID"))
+        response = await call_next(request)
+        response.headers["X-Request-ID"] = f"{request_id}"  # 记录同一个请求的唯一id
+        return response
+
+
+class AuthMiddleware(BaseHTTPMiddleware):
+    """鉴权中间件"""
+
+    @staticmethod
+    def set_auth_err_resp(err_msg: str):
+        return Response(
+            content=err_msg,
+            status_code=status.HTTP_401_UNAUTHORIZED
+        )
+
+    async def verify_token(self, authorization: str) -> Optional[dict]:
+        """
+        验证 token 有效性
+        返回:验证成功返回用户信息字典,失败返回 None
+        """
+        if not authorization or not authorization.startswith("Bearer "):
+            return None
+
+        token = authorization[7:]
+        # 从环境变量获取预设的token值进行比对
+        admin_token = auth_setting.admin_token
+        user_token = auth_setting.user_token
+
+        if token == admin_token:
+            return {"id": 1, "username": "admin", "role": "admin"}
+        elif token == user_token:
+            return {"id": 2, "username": "user", "role": "user"}
+        return None
+
+    def should_intercept(self, path: str) -> bool:
+        """
+        判断是否需要拦截当前路径
+        """
+        if path in auth_setting.auth_whitelist_urls:
+            return False
+
+        for pattern in auth_setting.auth_blacklist_urls:
+            # 处理通配符匹配
+            if pattern.endswith("/*"):
+                if path.startswith(pattern[:-1]):
+                    return True
+            # 精确匹配
+            elif path == pattern:
+                return True
+        return False
+
+    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
+        # 初始化请求上下文
+        request.state.context = {
+            "request_id": TraceUtil.get_req_id(),
+            "client_ip": request.client.host
+        }
+
+        path = request.url.path
+
+        if not self.should_intercept(path):
+            return await call_next(request)
+
+        # 权限校验
+        auth_header = request.headers.get("Authorization")
+        if not auth_header:
+            return self.set_auth_err_resp("Missing Authorization header")
+
+        user_info = await self.verify_token(auth_header)
+        if not user_info:
+            return self.set_auth_err_resp("Invalid token")
+
+        # 初始化操作:将用户信息添加到请求状态中
+        request.state.user = user_info
+
+        # 继续处理请求
+        response = await call_next(request)
+        # 可以在返回前添加统一响应处理(如添加头信息)
+        # response.headers["request-id"] = request.state.context["request_id"]
+
+        return response
+
+
+
+def register_middlewares():
+    """注册中间件(逆序执行)"""
+    return [
+        Middleware(TraceReqMiddleware),  # 最先设置request_id
+        Middleware(LoggingMiddleware),
+        Middleware(
+            CORSMiddleware,
+            allow_origins=["*"],
+            allow_credentials=True,
+            allow_methods=["*"],
+            allow_headers=["*"],
+        ),
+        Middleware(AuthMiddleware),
+    ]

+ 0 - 0
build/lib/knowledge/model/__init__.py


+ 16 - 0
build/lib/knowledge/model/kg_edges.py

@@ -0,0 +1,16 @@
+from sqlalchemy import Column, Integer, String
+from ..db.base_class import Base
+
+class KGEdge(Base):
+    __tablename__ = 'kg_edges'
+
+    id = Column(Integer, primary_key=True, index=True)
+    category = Column(String(64), nullable=False)
+    src_id = Column(Integer, nullable=False)
+    dest_id = Column(Integer, nullable=False)
+    name = Column(String(64), nullable=False)
+    version = Column(String(16))
+    status = Column(Integer, nullable=False, default=0)
+
+    def __repr__(self):
+        return f"<KGEdge(id={self.id}, name={self.name}, category={self.category})>"

+ 16 - 0
build/lib/knowledge/model/kg_node.py

@@ -0,0 +1,16 @@
+from sqlalchemy import Column, Integer, String
+from ..db.base_class import Base
+from pgvector.sqlalchemy import Vector
+
+class KGNode(Base):
+    __tablename__ = 'kg_nodes'
+
+    id = Column(Integer, primary_key=True, index=True)
+    name = Column(String(255), nullable=False)
+    category = Column(String(255), nullable=False)
+    embedding = Column(Vector(1024))
+    version = Column(String(50))
+    status = Column(Integer)
+    
+    def __repr__(self):
+        return f"<KGNode(id={self.id}, name={self.name}, category={self.category})>"

+ 18 - 0
build/lib/knowledge/model/kg_prop.py

@@ -0,0 +1,18 @@
+from sqlalchemy import Column, Integer, String, Text
+from ..db.base_class import Base
+
+class KGProp(Base):
+    __tablename__ = 'kg_props'
+
+    id = Column(Integer, primary_key=True, index=True)
+    category = Column(Integer, nullable=False, default=0)
+    ref_id = Column(Integer, nullable=False)
+    prop_name = Column(String(64), nullable=False)
+    prop_value = Column(Text)
+    prop_title = Column(String(64))
+    type = Column(Integer)
+
+    def __repr__(self):
+        return f"<KGProp(id={self.id}, prop_name={self.prop_name}, ref_id={self.ref_id})>"
+
+# 类型:空或1-实体属性,2-关系属性

+ 10 - 0
build/lib/knowledge/model/response.py

@@ -0,0 +1,10 @@
+from pydantic import BaseModel
+from typing import Any, Optional
+
+class StandardResponse(BaseModel):
+    success: bool
+    requestId: Optional[str] = None
+    errorCode: Optional[int] = None
+    errorMsg: Optional[str] = None
+    records: Optional[Any] = None
+    data: Optional[Any] = None

+ 0 - 0
build/lib/knowledge/router/__init__.py


+ 17 - 0
build/lib/knowledge/router/base.py

@@ -0,0 +1,17 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+# @Author: Hui
+# @Desc: { 模块描述 }
+# @Date: 2023/11/16 14:10
+# import fastapi
+#
+# from ..settings import log_setting
+# from ..middlewares.api_route import LoggingAPIRoute
+
+
+# class BaseAPIRouter(fastapi.APIRouter):
+#     def __init__(self, *args, api_log=log_setting.server_access_log, **kwargs):
+#         super().__init__(*args, **kwargs)
+#         if api_log:
+#             # 开启api请求日志信息
+#             self.route_class = LoggingAPIRoute

+ 120 - 0
build/lib/knowledge/router/knowledge_nodes_api.py

@@ -0,0 +1,120 @@
+from fastapi import APIRouter, Depends, HTTPException, Request, Security
+from fastapi.security import APIKeyHeader
+from typing import Optional
+from pydantic import BaseModel
+from ..model.response import StandardResponse
+from ..db.session import get_db
+from sqlalchemy.orm import Session
+from ..service.kg_node_service import KGNodeService
+from ..service.kg_edge_service import KGEdgeService
+from ..service.kg_prop_service import KGPropService
+import logging
+from ..utils.ObjectToJsonArrayConverter import ObjectToJsonArrayConverter
+
+router = APIRouter(prefix="/v1/knowledge", tags=["SaaS Knowledge Base"])
+
+logger = logging.getLogger(__name__)
+
+class PaginatedSearchRequest(BaseModel):
+    name: str
+    distance: float = 1.45
+    category: Optional[str] = None
+    pageNo: int = 1
+    limit: int = 10
+
+async def get_request_id(request: Request):
+    return request.state.context["request_id"]
+
+api_key_header = APIKeyHeader(name="Authorization", auto_error=False)
+
+@router.post("/nodes/paginated_search", response_model=StandardResponse)
+async def paginated_search(
+    payload: PaginatedSearchRequest,
+    db: Session = Depends(get_db),
+    request_id: str = Depends(get_request_id),
+    api_key: str = Security(api_key_header)
+):
+    try:
+        service = KGNodeService(db)
+        search_params = {
+            'keyword': payload.name,
+            'category': payload.category,
+            'pageNo': payload.pageNo,
+            'limit': payload.limit,
+            'load_props': True,
+            'distance': 2.0-payload.distance,
+        }
+        result = service.paginated_search(search_params)
+        #result[data]的distance属性去掉
+        for item in result['records']:
+            item.pop('distance', None)
+        #result[pagination]去掉
+        result.pop('pagination', None)
+        #result[records]改为result[nodes]
+        result['nodes'] = result['records']
+        result.pop('records', None)   
+        return StandardResponse(
+            success=True,
+            requestId=request_id,
+            data=ObjectToJsonArrayConverter.convert(result)
+        )
+    except Exception as e:
+        logger.error(f"分页查询失败: {str(e)}")
+        raise HTTPException(
+            status_code=500,
+            detail=StandardResponse(
+                success=False,
+                error_code=500,
+                error_msg=str(e)
+            )
+        )
+
+@router.get("/nodes/{src_id}/relationships", response_model=StandardResponse)
+async def get_node_relationships(
+    src_id: int,
+    db: Session = Depends(get_db),
+    request_id: str = Depends(get_request_id),
+    api_key: str = Security(api_key_header)
+):
+    try:
+        edge_service = KGEdgeService(db)
+        prop_service = KGPropService(db)
+        
+        edges = edge_service.get_edges_by_nodes(src_id=src_id, dest_id=None)
+        relationships = []
+               
+        #count = 0
+        for edge in edges:
+            #if count >= 2:
+                #break
+            dest_node = edge['dest_node']
+            dest_props = []
+            edge_props = []
+            #count += 1
+            #dest_props = [{'prop_title': p['prop_title'], 'prop_value': p['prop_value']}
+            #              for p in prop_service.get_props_by_ref_id(dest_node['id'])]
+            
+            #edge_props = [{'prop_title': p['prop_title'], 'prop_value': p['prop_value']}
+            #             for p in prop_service.get_props_by_ref_id(edge['id'])]
+
+            relationships.append({
+                "name": edge['name'],
+                "props": edge_props,
+                "destNode": {
+                    "category": dest_node['category'],
+                    "id": str(dest_node['id']),
+                    "name": dest_node['name'],
+                    "props": dest_props
+                }
+            })
+        
+        return StandardResponse(
+            success=True,
+            requestId=request_id,
+            data=ObjectToJsonArrayConverter.convert({"relationships": relationships})
+        )
+    except Exception as e:
+        logger.error(f"获取节点关系失败: {str(e)}")
+        raise HTTPException(500, detail=StandardResponse.error(str(e)))
+
+knowledge_nodes_api_router = router

+ 55 - 0
build/lib/knowledge/server.py

@@ -0,0 +1,55 @@
+from contextlib import asynccontextmanager
+from datetime import datetime
+
+from fastapi import FastAPI
+from py_tools.connections.http import AsyncHttpClient
+from py_tools.logging import logger
+
+from .middlewares.base import register_middlewares
+from .router.knowledge_nodes_api import knowledge_nodes_api_router
+from .utils import log_util
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+    await startup()
+    yield
+    await shutdown()
+
+
+app = FastAPI(
+    description="知识图谱开放平台",
+    lifespan=lifespan,
+    middleware=register_middlewares(),  # 注册web中间件
+)
+
+@app.get("/health")
+async def health_check():
+    """健康检查接口"""
+    return {
+        "status": "ok",
+        "timestamp": datetime.utcnow().isoformat(),
+        "service": "knowledge-graph"
+    }
+
+
+async def init_setup():
+    """初始化项目配置"""
+
+    log_util.setup_logger()
+
+
+async def startup():
+    """项目启动时准备环境"""
+
+    await init_setup()
+
+    # 加载路由
+    app.include_router(knowledge_nodes_api_router)
+
+    logger.info("fastapi startup success")
+
+
+async def shutdown():
+    await AsyncHttpClient.close()
+    logger.error("app shutdown")

+ 0 - 0
build/lib/knowledge/service/__init__.py


+ 106 - 0
build/lib/knowledge/service/kg_edge_service.py

@@ -0,0 +1,106 @@
+from sqlalchemy.orm import Session
+from sqlalchemy import or_
+from typing import Optional
+from ..model.kg_edges import KGEdge
+import logging
+from sqlalchemy.exc import IntegrityError
+
+logger = logging.getLogger(__name__)
+
+class KGEdgeService:
+    def __init__(self, db: Session):
+        self.db = db
+
+    def get_edge(self, edge_id: int):
+        edge = self.db.query(KGEdge).get(edge_id)
+        if not edge:
+            raise ValueError("Edge not found")
+        return edge
+
+    def create_edge(self, edge_data: dict):
+        try:
+            existing = self.db.query(KGEdge).filter(
+                KGEdge.src_id == edge_data['src_id'],
+                KGEdge.dest_id == edge_data['dest_id'],
+                KGEdge.name == edge_data['name'],
+                KGEdge.version == edge_data.get('version')
+            ).first()
+
+            if existing:
+                raise ValueError("Edge already exists")
+
+            new_edge = KGEdge(**edge_data)
+            self.db.add(new_edge)
+            self.db.commit()
+            return new_edge
+
+        except IntegrityError as e:
+            self.db.rollback()
+            logger.error(f"创建边失败: {str(e)}")
+            raise ValueError("Database integrity error")
+
+    def update_edge(self, edge_id: int, update_data: dict):
+        edge = self.db.query(KGEdge).get(edge_id)
+        if not edge:
+            raise ValueError("Edge not found")
+
+        try:
+            for key, value in update_data.items():
+                setattr(edge, key, value)
+            self.db.commit()
+            return edge
+        except Exception as e:
+            self.db.rollback()
+            logger.error(f"更新边失败: {str(e)}")
+            raise ValueError("Update failed")
+
+    def delete_edge(self, edge_id: int):
+        edge = self.db.query(KGEdge).get(edge_id)
+        if not edge:
+            raise ValueError("Edge not found")
+
+        try:
+            self.db.delete(edge)
+            self.db.commit()
+            return None
+        except Exception as e:
+            self.db.rollback()
+            logger.error(f"删除边失败: {str(e)}")
+            raise ValueError("Delete failed")
+
+    def get_edges_by_nodes(self, src_id: Optional[int], dest_id: Optional[int], and_logic: bool = True):
+        if src_id is None and dest_id is None:
+            raise ValueError("至少需要提供一个有效的查询条件")
+        try:
+            filters = []
+            if src_id is not None:
+                filters.append(KGEdge.src_id == src_id)
+            if dest_id is not None:
+                filters.append(KGEdge.dest_id == dest_id)
+
+            if and_logic:
+                edges = self.db.query(KGEdge).filter(*filters).all()
+            else:
+                edges = self.db.query(KGEdge).filter(or_(*filters)).all()
+            from ..service.kg_node_service import KGNodeService
+            node_service = KGNodeService(self.db)
+            result = []
+            for edge in edges:
+                try:
+                    edge_info = {
+                        'id': edge.id,
+                        'src_id': edge.src_id,
+                        'dest_id': edge.dest_id,
+                        'name': edge.name,
+                        'version': edge.version,
+                        'src_node': node_service.get_node(edge.src_id),
+                        'dest_node': node_service.get_node(edge.dest_id)
+                    }
+                    result.append(edge_info)
+                except ValueError as e:
+                    logger.warning(f"跳过边关系 {edge.id}: {str(e)}")
+                    continue
+            return result
+        except Exception as e:
+            logger.error(f"查询边失败: {str(e)}")
+            raise e

+ 220 - 0
build/lib/knowledge/service/kg_node_service.py

@@ -0,0 +1,220 @@
+from sqlalchemy.orm import Session
+from ..model.kg_node import KGNode
+from ..db.session import get_db
+import logging
+from sqlalchemy.exc import IntegrityError
+
+from ..utils.vectorizer import Vectorizer
+from sqlalchemy import func
+from ..service.kg_prop_service import KGPropService
+from ..service.kg_edge_service import KGEdgeService
+
+logger = logging.getLogger(__name__)
+DISTANCE_THRESHOLD = 0.65
+DISTANCE_THRESHOLD2 = 0.3
+class KGNodeService:
+    def __init__(self, db: Session):
+        self.db = db
+
+    _cache = {}
+
+    def search_title_index(self, index: str, title: str, top_k: int = 3):
+        cache_key = f"{index}:{title}:{top_k}"
+        if cache_key in self._cache:
+            return self._cache[cache_key]
+
+        query_embedding = Vectorizer.get_instance().get_embedding(title)
+        db = next(get_db())
+        # 执行向量搜索
+        results = (
+            db.query(
+                KGNode.id,
+                KGNode.name,
+                KGNode.category,
+                KGNode.embedding.l2_distance(query_embedding).label('distance')
+            )
+            .filter(KGNode.status == 0)
+            #过滤掉version不等于'er'的节点
+            .filter(KGNode.version != 'ER')
+            .filter(KGNode.embedding.l2_distance(query_embedding) <= DISTANCE_THRESHOLD2)
+            .order_by('distance').limit(top_k).all()
+        )
+        results = [
+            {
+                "id": node.id,
+               "title": node.name,
+               "text": node.category,
+               "score": 2.0-node.distance
+            }
+                for node in results
+            ]
+
+        self._cache[cache_key] = results
+        return results
+
+    def paginated_search(self, search_params: dict) -> dict:
+        load_props = search_params.get('load_props', False)
+        prop_service = KGPropService(self.db)
+        edge_service = KGEdgeService(self.db)
+        keyword = search_params.get('keyword', '')
+        category = search_params.get('category', '')
+        page_no = search_params.get('pageNo', 1)
+        distance = search_params.get('distance',DISTANCE_THRESHOLD)
+        limit = search_params.get('limit', 10)
+
+        if page_no < 1:
+            page_no = 1
+        if limit < 1:
+            limit = 10
+
+        embedding = Vectorizer.get_instance().get_embedding(keyword)
+        offset = (page_no - 1) * limit
+
+        try:
+            # 构建基础查询条件
+            base_query = self.db.query(func.count(KGNode.id)).filter(
+                KGNode.status == 0,
+                KGNode.embedding.l2_distance(embedding) < distance
+            )
+            # 如果有category,则添加额外过滤条件
+            if category:
+                base_query = base_query.filter(KGNode.category == category)
+            # 如果有knowledge_ids,则添加额外过滤条件
+            if search_params.get('knowledge_ids'):
+                total_count = base_query.filter(
+                    KGNode.version.in_(search_params['knowledge_ids'])
+                ).scalar()
+            else:
+                total_count = base_query.scalar()
+
+            query = self.db.query(
+                KGNode.id,
+                KGNode.name,
+                KGNode.category,
+                KGNode.embedding.l2_distance(embedding).label('distance')
+            )            
+            query = query.filter(KGNode.status == 0)
+            #category有值时,过滤掉category不等于category的节点
+            if category:
+                query = query.filter(KGNode.category == category)
+            if search_params.get('knowledge_ids'):
+                query = query.filter(KGNode.version.in_(search_params['knowledge_ids']))
+            query = query.filter(KGNode.embedding.l2_distance(embedding) < distance)
+            results = query.order_by('distance').offset(offset).limit(limit).all()
+            #将results相同distance的category=疾病的放在前面
+            results = sorted(results, key=lambda x: (x.distance, not x.category == '疾病'))
+            return {
+                'records': [{
+                    'id': r.id,
+                    'name': r.name,
+                    'category': r.category,
+                    'props': prop_service.get_props_by_ref_id(r.id) if load_props else [],
+                    #'edges':edge_service.get_edges_by_nodes(r.id, r.id,False) if load_props else [],
+                    'distance': round(r.distance, 3)
+                } for r in results],
+                'pagination': {
+                    'total': total_count,
+                    'pageNo': page_no,
+                    'limit': limit,
+                    'totalPages': (total_count + limit - 1) // limit
+                }
+            
+            }
+        except Exception as e:
+            logger.error(f"分页查询失败: {str(e)}")
+            raise e
+
+    def create_node(self, node_data: dict):
+        try:
+            existing = self.db.query(KGNode).filter(
+                KGNode.name == node_data['name'],
+                KGNode.category == node_data['category'],
+                KGNode.version == node_data.get('version')
+            ).first()
+            
+            if existing:
+                raise ValueError("Node already exists")
+
+            new_node = KGNode(**node_data)
+            self.db.add(new_node)
+            self.db.commit()
+            return new_node
+
+        except IntegrityError as e:
+            self.db.rollback()
+            logger.error(f"创建节点失败: {str(e)}")
+            raise ValueError("Database integrity error")
+
+    def get_node(self, node_id: int):
+   
+        if node_id is None:
+            raise ValueError("Node ID is required")
+     
+        node = self.db.query(KGNode).filter(KGNode.id == node_id, KGNode.status == 0).first()     
+
+        if not node:
+            raise ValueError("Node not found")
+        return {
+            'id': node.id,
+            'name': node.name,
+            'category': node.category,
+            'version': node.version
+        }
+
+    def update_node(self, node_id: int, update_data: dict):
+        node = self.db.query(KGNode).get(node_id)
+        if not node:
+            raise ValueError("Node not found")
+
+        try:
+            for key, value in update_data.items():
+                setattr(node, key, value)
+            self.db.commit()
+            return node
+        except Exception as e:
+            self.db.rollback()
+            logger.error(f"更新节点失败: {str(e)}")
+            raise ValueError("Update failed")
+
+    def delete_node(self, node_id: int):
+        node = self.db.query(KGNode).get(node_id)
+        if not node:
+            raise ValueError("Node not found")
+
+        try:
+            self.db.delete(node)
+            self.db.commit()
+            return None
+        except Exception as e:
+            self.db.rollback()
+            logger.error(f"删除节点失败: {str(e)}")
+            raise ValueError("Delete failed")
+
+    def batch_process_er_nodes(self):
+        batch_size = 200
+        offset = 0
+
+        while True:
+            try:
+                nodes = self.db.query(KGNode).filter(
+                    #KGNode.version == 'ER',
+                    KGNode.embedding == None
+                ).offset(offset).limit(batch_size).all()
+
+                if not nodes:
+                    break
+
+                updated_nodes = []
+                for node in nodes:
+                    if not node.embedding:
+                        embedding = Vectorizer.get_instance().get_embedding(node.name)
+                        node.embedding = embedding
+                        updated_nodes.append(node)
+                if updated_nodes:
+                    self.db.commit()
+
+                offset += batch_size
+            except Exception as e:
+                self.db.rollback()
+                print(f"批量处理ER节点失败: {str(e)}")
+                raise ValueError("Batch process failed")

+ 66 - 0
build/lib/knowledge/service/kg_prop_service.py

@@ -0,0 +1,66 @@
+from sqlalchemy.orm import Session
+from typing import List
+from ..model.kg_prop import KGProp
+import logging
+from sqlalchemy.exc import IntegrityError
+
+logger = logging.getLogger(__name__)
+
+class KGPropService:
+    def __init__(self, db: Session):
+        self.db = db
+
+    def get_props_by_ref_id(self, ref_id: int) -> List[dict]:
+        try:
+            props = self.db.query(KGProp).filter(KGProp.ref_id == ref_id).all()
+            return [{
+                'id': p.id,
+                'category': p.category,
+                'prop_name': p.prop_name,
+                'prop_value': p.prop_value,
+                'prop_title': p.prop_title,
+                'type': p.type
+            } for p in props]
+        except Exception as e:
+            logger.error(f"根据ref_id查询属性失败: {str(e)}")
+            raise ValueError("查询失败")
+
+    def create_prop(self, prop_data: dict) -> KGProp:
+        try:
+            new_prop = KGProp(**prop_data)
+            self.db.add(new_prop)
+            self.db.commit()
+            return new_prop
+        except IntegrityError as e:
+            self.db.rollback()
+            logger.error(f"创建属性失败: {str(e)}")
+            raise ValueError("数据库完整性错误")
+
+    def update_prop(self, prop_id: int, update_data: dict) -> KGProp:
+        prop = self.db.query(KGProp).get(prop_id)
+        if not prop:
+            raise ValueError("属性未找到")
+
+        try:
+            for key, value in update_data.items():
+                setattr(prop, key, value)
+            self.db.commit()
+            return prop
+        except Exception as e:
+            self.db.rollback()
+            logger.error(f"更新属性失败: {str(e)}")
+            raise ValueError("更新失败")
+
+    def delete_prop(self, prop_id: int) -> None:
+        prop = self.db.query(KGProp).get(prop_id)
+        if not prop:
+            raise ValueError("属性未找到")
+
+        try:
+            self.db.delete(prop)
+            self.db.commit()
+            return None
+        except Exception as e:
+            self.db.rollback()
+            logger.error(f"删除属性失败: {str(e)}")
+            raise ValueError("删除失败")

+ 0 - 0
build/lib/knowledge/settings/__init__.py


+ 19 - 0
build/lib/knowledge/settings/auth_setting.py

@@ -0,0 +1,19 @@
+# @Desc: { 模块描述 }
+import datetime
+
+# 鉴权白名单路由
+auth_whitelist_urls = (
+    "/docs",
+    "/redoc",
+    "/openapi",
+    "/health",
+)
+
+# 鉴权名单路由
+auth_blacklist_urls = (
+    "/v1/knowledge/*",
+)
+
+
+admin_token = "hQGbYnaHoDtAc0yf4pm37X5l6ZCU9weMgIsLWJTOj1EdVNRKx2Frvq8uBSkPizcI"
+user_token = "yhF0VGWSJREvgm17PjHI2KpOe4BNusArYczowdbQiUCxfX85t3lZ9aqLkDM6TnPo"

+ 8 - 0
build/lib/knowledge/settings/base_setting.py

@@ -0,0 +1,8 @@
+# @Desc: { 项目服务配置模块 }
+import logging
+
+server_host = "0.0.0.0"
+server_port = 8081
+server_log_level = logging.INFO
+server_access_log = True
+

+ 24 - 0
build/lib/knowledge/settings/log_setting.py

@@ -0,0 +1,24 @@
+# @Desc: { 日志配置模块 }
+import logging
+import os
+
+server_access_log = True
+
+# 项目基准路径
+base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+# 项目日志目录
+logging_dir = "/app/logs/"
+
+# 项目日志配置
+console_log_level = logging.DEBUG
+log_format = "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level:<8} | {trace_msg} | {name}:{function}:{line} - {message}"
+
+# 项目服务综合日志滚动配置(每天 0 点新创建一个 log 文件)
+# 错误日志 超过10 MB就自动新建文件扩充
+server_logging_rotation = "00:00"
+error_logging_rotation = "10 MB"
+
+# 服务综合日志文件最长保留 7 天,错误日志 30 天
+server_logging_retention = "7 days"
+error_logging_retention = "30 days"

+ 64 - 0
build/lib/knowledge/utils/ObjectToJsonArrayConverter.py

@@ -0,0 +1,64 @@
+import json
+
+
+class ObjectToJsonArrayConverter:
+    """
+    将Python对象转换为特定结构的JSON数组。
+    """
+
+    @staticmethod
+    def convert(obj):
+        """
+        递归转换对象为JSON数组结构。
+
+        Args:
+            obj: 待转换的Python对象(dict/list/其他)
+
+        Returns:
+            list: 转换后的结构,每个元素为 {"key": key, "value": processed_value}
+        """
+        def _process(value, in_list_context):
+            """处理值,根据是否在列表环境中决定包装逻辑"""
+            if isinstance(value, dict):
+                # 字典递归转换为列表结构
+                return ObjectToJsonArrayConverter.convert(value)
+            if isinstance(value, list):
+                # 列表递归处理,子元素默认在列表环境中
+                return [_process(item, in_list_context=True) for item in value]
+            # 普通值:列表环境中不包装,否则包装为单元素列表
+            return value if in_list_context else [value]
+
+        # 处理外层对象
+        if isinstance(obj, dict):
+            return [{"key": k, "value": _process(v, False)} for k, v in obj.items()]
+        if isinstance(obj, list):
+            return [{"key": None, "value": _process(item, True)} for item in obj]
+        return []  # 非dict/list返回空(根据原逻辑)
+
+    @staticmethod
+    def to_json(obj, indent=None):
+        """
+        转换为JSON字符串。
+
+        Args:
+            obj: 待转换对象
+            indent: 缩进量(默认无缩进)
+
+        Returns:
+            str: JSON字符串
+        """
+        return json.dumps(
+            ObjectToJsonArrayConverter.convert(obj),
+            indent=indent,
+            ensure_ascii=False
+        )
+
+
+# 测试代码
+if __name__ == "__main__":
+    test_obj = [
+            {"name": "Alice"},
+            [{"age": 30}, "直接值"]
+        ]
+
+    print(ObjectToJsonArrayConverter.to_json(test_obj, indent=4))

+ 0 - 0
build/lib/knowledge/utils/__init__.py


+ 5 - 0
build/lib/knowledge/utils/context_util.py

@@ -0,0 +1,5 @@
+# @Desc: { 上下文模块描述 }
+import contextvars
+
+# 请求唯一id
+REQUEST_ID: contextvars.ContextVar[str] = contextvars.ContextVar("request_id", default="")

+ 11 - 0
build/lib/knowledge/utils/embed_helper.py

@@ -0,0 +1,11 @@
+#load enviroment variable
+from ..config.site import SiteConfig
+from sentence_transformers import SentenceTransformer
+config = SiteConfig()
+
+class EmbedHelper:
+    def __init__(self):
+        self.embedding_model_name = config.get_config("EMBEDDING_MODEL")
+        self.embedding_model = SentenceTransformer(model_name_or_path=self.embedding_model_name) 
+    def embed_text(self, text):
+        return self.embedding_model.encode(text).tolist()

+ 39 - 0
build/lib/knowledge/utils/license.py

@@ -0,0 +1,39 @@
+from cryptography.hazmat.primitives.asymmetric import padding
+from cryptography.hazmat.primitives import hashes,serialization
+import json
+import time
+import traceback
+
+def validate_license(public_key_pem, license_json, signature):
+    public_key = serialization.load_pem_public_key(public_key_pem)
+
+    try:
+        public_key.verify(
+        signature,
+        license_json,
+        padding.PKCS1v15(),
+        hashes.SHA256()
+        )
+    except:
+        #打印异常信息
+        traceback.print_exc()
+        return False
+
+    license_data=json.loads(license_json.decode())
+    # 检查是否过期
+    if time.time()>license_data["expiration_time"]:
+        return False
+    return True
+
+if __name__ == '__main__':
+    with open("license_issued/public.key","rb") as f:
+        public_key_pem = f.read()
+    with open("license_issued/license_issued.lic","rb") as f:
+        data = json.loads(f.read())
+        license_json = json.dumps(data, sort_keys=True).encode()
+    with open("license_issued/license_issued.key","rb") as f:
+        signature = f.read()
+    if validate_license(public_key_pem,license_json, signature):
+        print("许可证有效!")
+    else:
+        print("许可证无效或已过期!")

+ 27 - 0
build/lib/knowledge/utils/log_util.py

@@ -0,0 +1,27 @@
+# @Desc: { 日志工具模块 }
+
+from py_tools.logging import setup_logging
+
+from ..settings import log_setting
+from ..utils.trace_util import TraceUtil
+
+
+def _logger_filter(record):
+    """日志过滤器补充request_id"""
+    req_id = TraceUtil.get_req_id()
+
+    trace_msg = f"{req_id}"
+    record["trace_msg"] = trace_msg
+    return record
+
+
+def setup_logger():
+    """配置项目日志信息"""
+    setup_logging(
+        log_dir=log_setting.logging_dir,
+        log_filter=_logger_filter,
+        log_format=log_setting.log_format,
+        console_log_level=log_setting.console_log_level,
+        log_retention=log_setting.server_logging_retention,
+        log_rotation=log_setting.server_logging_rotation,
+    )

+ 26 - 0
build/lib/knowledge/utils/trace_util.py

@@ -0,0 +1,26 @@
+# @Desc: { 日志链路追踪工具模块 }
+import uuid
+
+from ..utils import context_util
+
+class TraceUtil(object):
+    @staticmethod
+    def set_req_id(req_id: str = None, title="req-id") -> str:
+        """
+        设置请求唯一ID
+        Args:
+            req_id: 请求ID 默认None取uuid
+            title: 标题 默认req-id
+
+        Returns:
+            title:req_id
+        """
+        req_id = req_id or uuid.uuid4().hex
+        # req_id = f"{title}:{req_id}"
+
+        context_util.REQUEST_ID.set(req_id)
+        return req_id
+
+    @staticmethod
+    def get_req_id() -> str:
+        return context_util.REQUEST_ID.get()

File diff suppressed because it is too large
+ 41 - 0
build/lib/knowledge/utils/vector_distance.py


+ 34 - 0
build/lib/knowledge/utils/vectorizer.py

@@ -0,0 +1,34 @@
+import logging
+from typing import List
+from ..utils.embed_helper import EmbedHelper
+
+
+logger = logging.getLogger(__name__)
+
+class Vectorizer:
+    _instance = None
+    
+    def __init__(self):
+        self.embedHelper = EmbedHelper()
+
+    def get_embedding(self, text: str) -> List[float]:
+       return self.embedHelper.embed_text(text)
+
+    @classmethod
+    def get_instance(cls):
+        if cls._instance is None:
+            cls._instance = cls()
+        return cls._instance
+
+    def chunk_text(self, text: str, chunk_size: int = 500) -> List[str]:
+        tokens = self.tokenizer.tokenize(text)
+        return [self.tokenizer.convert_tokens_to_string(tokens[i:i+chunk_size]) 
+               for i in range(0, len(tokens), chunk_size)]
+
+
+if __name__ == '__main__':
+
+    text = '你好'
+    print(text)
+
+    embedding2 = Vectorizer.get_instance().get_embedding(text)

+ 16 - 0
src/knowledge.egg-info/PKG-INFO

@@ -0,0 +1,16 @@
+Metadata-Version: 2.4
+Name: knowledge
+Version: 1.0
+Requires-Dist: fastapi==0.115.12
+Requires-Dist: sentence-transformers==4.0.1
+Requires-Dist: numpy==1.26.4
+Requires-Dist: pgvector==0.1.8
+Requires-Dist: pydantic==2.11.1
+Requires-Dist: Requests==2.31.0
+Requires-Dist: SQLAlchemy==2.0.20
+Requires-Dist: urllib3==2.3.0
+Requires-Dist: uvicorn==0.34.0
+Requires-Dist: psycopg2-binary==2.9.10
+Requires-Dist: python-dotenv==1.0.0
+Requires-Dist: hui-tools[all]==0.5.8
+Dynamic: requires-dist

+ 43 - 0
src/knowledge.egg-info/SOURCES.txt

@@ -0,0 +1,43 @@
+setup.py
+src/knowledge/__init__.py
+src/knowledge/main.py
+src/knowledge/server.py
+src/knowledge.egg-info/PKG-INFO
+src/knowledge.egg-info/SOURCES.txt
+src/knowledge.egg-info/dependency_links.txt
+src/knowledge.egg-info/entry_points.txt
+src/knowledge.egg-info/requires.txt
+src/knowledge.egg-info/top_level.txt
+src/knowledge/config/__init__.py
+src/knowledge/config/site.py
+src/knowledge/db/__init__.py
+src/knowledge/db/base_class.py
+src/knowledge/db/session.py
+src/knowledge/middlewares/__init__.py
+src/knowledge/middlewares/api_route.py
+src/knowledge/middlewares/base.py
+src/knowledge/model/__init__.py
+src/knowledge/model/kg_edges.py
+src/knowledge/model/kg_node.py
+src/knowledge/model/kg_prop.py
+src/knowledge/model/response.py
+src/knowledge/router/__init__.py
+src/knowledge/router/base.py
+src/knowledge/router/knowledge_nodes_api.py
+src/knowledge/service/__init__.py
+src/knowledge/service/kg_edge_service.py
+src/knowledge/service/kg_node_service.py
+src/knowledge/service/kg_prop_service.py
+src/knowledge/settings/__init__.py
+src/knowledge/settings/auth_setting.py
+src/knowledge/settings/base_setting.py
+src/knowledge/settings/log_setting.py
+src/knowledge/utils/ObjectToJsonArrayConverter.py
+src/knowledge/utils/__init__.py
+src/knowledge/utils/context_util.py
+src/knowledge/utils/embed_helper.py
+src/knowledge/utils/license.py
+src/knowledge/utils/log_util.py
+src/knowledge/utils/trace_util.py
+src/knowledge/utils/vector_distance.py
+src/knowledge/utils/vectorizer.py

+ 1 - 0
src/knowledge.egg-info/dependency_links.txt

@@ -0,0 +1 @@
+

+ 2 - 0
src/knowledge.egg-info/entry_points.txt

@@ -0,0 +1,2 @@
+[console_scripts]
+knowledge = knowledge.main:main

+ 12 - 0
src/knowledge.egg-info/requires.txt

@@ -0,0 +1,12 @@
+fastapi==0.115.12
+sentence-transformers==4.0.1
+numpy==1.26.4
+pgvector==0.1.8
+pydantic==2.11.1
+Requests==2.31.0
+SQLAlchemy==2.0.20
+urllib3==2.3.0
+uvicorn==0.34.0
+psycopg2-binary==2.9.10
+python-dotenv==1.0.0
+hui-tools[all]==0.5.8

+ 1 - 0
src/knowledge.egg-info/top_level.txt

@@ -0,0 +1 @@
+knowledge

+ 1 - 1
src/knowledge/.env

@@ -5,4 +5,4 @@ DB_USER=knowledge
 DB_PASSWORD=qwer1234.
 DB_PASSWORD=qwer1234.
 
 
 license=E:\project\knowledge\license_issued
 license=E:\project\knowledge\license_issued
-EMBEDDING_MODEL=E:\project\knowledge2\bge-m3
+EMBEDDING_MODEL=E:\project\bge-m3

+ 5 - 0
src/knowledge/router/knowledge_nodes_api.py

@@ -22,6 +22,10 @@ class PaginatedSearchRequest(BaseModel):
     pageNo: int = 1
     pageNo: int = 1
     limit: int = 10
     limit: int = 10
 
 
+class GetNodeRelationshipsRequest(BaseModel):
+    relation_name: str
+
+
 async def get_request_id(request: Request):
 async def get_request_id(request: Request):
     return request.state.context["request_id"]
     return request.state.context["request_id"]
 
 
@@ -72,6 +76,7 @@ async def paginated_search(
 @router.get("/nodes/{src_id}/relationships", response_model=StandardResponse)
 @router.get("/nodes/{src_id}/relationships", response_model=StandardResponse)
 async def get_node_relationships(
 async def get_node_relationships(
     src_id: int,
     src_id: int,
+    payload: GetNodeRelationshipsRequest,
     db: Session = Depends(get_db),
     db: Session = Depends(get_db),
     request_id: str = Depends(get_request_id),
     request_id: str = Depends(get_request_id),
     api_key: str = Security(api_key_header)
     api_key: str = Security(api_key_header)

+ 4 - 0
src/logs/error.log

@@ -0,0 +1,4 @@
+2025-06-17 21:07:07.823 | ERROR    | src.knowledge.server:shutdown:55 - app shutdown
+2025-06-17 21:15:55.489 | ERROR    | src.knowledge.server:shutdown:55 - app shutdown
+2025-06-17 21:16:10.181 | ERROR    | src.knowledge.server:shutdown:55 - app shutdown
+2025-06-17 21:19:40.608 | ERROR    | src.knowledge.server:shutdown:55 - app shutdown

+ 55 - 0
src/logs/server.log

@@ -0,0 +1,55 @@
+2025-06-17 20:54:39.525 | INFO     |  | py_tools.logging.base:setup_logging:87 - setup logging success
+2025-06-17 20:54:39.530 | INFO     |  | src.knowledge.server:startup:50 - fastapi startup success
+2025-06-17 20:55:29.934 | INFO     | 194a20991b654bd88f75d7bd75fbdc86 | src.knowledge.middlewares.base:dispatch:35 - --> GET /docs 127.0.0.1
+2025-06-17 20:55:29.935 | INFO     | 194a20991b654bd88f75d7bd75fbdc86 | src.knowledge.middlewares.base:dispatch:56 - <-- Response: [Status: 200, Type: _StreamingResponse, Headers: {'content-length': '931', 'content-type': 'text/html; charset=utf-8', 'x-response-time': '0.00s'}]
+2025-06-17 20:55:29.935 | INFO     | 194a20991b654bd88f75d7bd75fbdc86 | src.knowledge.middlewares.base:dispatch:100 - <-- 200 /docs (took: 0.00s)
+2025-06-17 20:55:31.032 | INFO     | 744bea7d9d704f2e9bc894a485d1db1c | src.knowledge.middlewares.base:dispatch:35 - --> GET /openapi.json 127.0.0.1
+2025-06-17 20:55:31.476 | INFO     | 744bea7d9d704f2e9bc894a485d1db1c | src.knowledge.middlewares.base:dispatch:56 - <-- Response: [Status: 200, Type: _StreamingResponse, Headers: {'content-length': '3091', 'content-type': 'application/json', 'x-response-time': '0.44s'}]
+2025-06-17 20:55:31.476 | INFO     | 744bea7d9d704f2e9bc894a485d1db1c | src.knowledge.middlewares.base:dispatch:100 - <-- 200 /openapi.json (took: 0.44s)
+2025-06-17 20:56:48.707 | INFO     | f337ced325aa41178abbb36052c78a24 | src.knowledge.middlewares.base:dispatch:35 - --> POST /v1/knowledge/nodes/paginated_search 127.0.0.1
+2025-06-17 20:56:48.708 | INFO     | f337ced325aa41178abbb36052c78a24 | src.knowledge.middlewares.base:dispatch:44 - --> Request Body: {'name': '咳嗽', 'distance': 1.45, 'category': '', 'pageNo': 1, 'limit': 10}
+2025-06-17 20:56:48.709 | INFO     | f337ced325aa41178abbb36052c78a24 | src.knowledge.middlewares.base:dispatch:56 - <-- Response: [Status: 401, Type: _StreamingResponse, Headers: {'content-length': '13', 'access-control-allow-origin': '*', 'access-control-allow-credentials': 'true', 'x-response-time': '0.00s'}]
+2025-06-17 20:56:48.709 | INFO     | f337ced325aa41178abbb36052c78a24 | src.knowledge.middlewares.base:dispatch:100 - <-- 401 /v1/knowledge/nodes/paginated_search (took: 0.00s)
+2025-06-17 20:57:17.955 | INFO     | 78a40591f159494488456764c954e8c8 | src.knowledge.middlewares.base:dispatch:35 - --> POST /v1/knowledge/nodes/paginated_search 127.0.0.1
+2025-06-17 20:57:17.956 | INFO     | 78a40591f159494488456764c954e8c8 | src.knowledge.middlewares.base:dispatch:44 - --> Request Body: {'name': '咳嗽', 'distance': 1.45, 'category': '', 'pageNo': 1, 'limit': 10}
+2025-06-17 20:57:17.957 | INFO     | 78a40591f159494488456764c954e8c8 | src.knowledge.middlewares.base:dispatch:56 - <-- Response: [Status: 401, Type: _StreamingResponse, Headers: {'content-length': '13', 'access-control-allow-origin': '*', 'access-control-allow-credentials': 'true', 'x-response-time': '0.00s'}]
+2025-06-17 20:57:17.957 | INFO     | 78a40591f159494488456764c954e8c8 | src.knowledge.middlewares.base:dispatch:100 - <-- 401 /v1/knowledge/nodes/paginated_search (took: 0.00s)
+2025-06-17 20:57:57.424 | INFO     | ebcb3cd14dbf4dc582cd12c7812bde5c | src.knowledge.middlewares.base:dispatch:35 - --> POST /v1/knowledge/nodes/paginated_search 127.0.0.1
+2025-06-17 20:57:57.426 | INFO     | ebcb3cd14dbf4dc582cd12c7812bde5c | src.knowledge.middlewares.base:dispatch:44 - --> Request Body: {'name': '咳嗽', 'distance': 1.45, 'category': '', 'pageNo': 1, 'limit': 10}
+2025-06-17 21:07:07.823 | ERROR    |  | src.knowledge.server:shutdown:55 - app shutdown
+2025-06-17 21:07:22.365 | INFO     |  | py_tools.logging.base:setup_logging:87 - setup logging success
+2025-06-17 21:07:22.368 | INFO     |  | src.knowledge.server:startup:50 - fastapi startup success
+2025-06-17 21:07:56.994 | INFO     | 6bd544e62b624b9bab8104a088e20018 | src.knowledge.middlewares.base:dispatch:35 - --> POST /v1/knowledge/nodes/paginated_search 127.0.0.1
+2025-06-17 21:07:56.995 | INFO     | 6bd544e62b624b9bab8104a088e20018 | src.knowledge.middlewares.base:dispatch:44 - --> Request Body: {'name': '咳嗽', 'distance': 1.45, 'category': '', 'pageNo': 1, 'limit': 10}
+2025-06-17 21:12:50.339 | INFO     | 4379033a7eb24bc692722da36b57ffa5 | src.knowledge.middlewares.base:dispatch:35 - --> POST /v1/knowledge/nodes/paginated_search 127.0.0.1
+2025-06-17 21:12:50.340 | INFO     | 4379033a7eb24bc692722da36b57ffa5 | src.knowledge.middlewares.base:dispatch:44 - --> Request Body: {'name': '咳嗽', 'distance': 1.45, 'pageNo': 1, 'limit': 10}
+2025-06-17 21:12:52.841 | INFO     | 58a7e9fa2e3c4bf19318fb22074317e1 | src.knowledge.middlewares.base:dispatch:35 - --> POST /v1/knowledge/nodes/paginated_search 127.0.0.1
+2025-06-17 21:12:52.843 | INFO     | 58a7e9fa2e3c4bf19318fb22074317e1 | src.knowledge.middlewares.base:dispatch:44 - --> Request Body: {'name': '咳嗽', 'distance': 1.45, 'pageNo': 1, 'limit': 10}
+2025-06-17 21:14:03.121 | INFO     | 4d58420fd5f640a4999fbfe6b2e12cc5 | src.knowledge.middlewares.base:dispatch:35 - --> POST /v1/knowledge/nodes/paginated_search 127.0.0.1
+2025-06-17 21:14:03.122 | INFO     | 4d58420fd5f640a4999fbfe6b2e12cc5 | src.knowledge.middlewares.base:dispatch:44 - --> Request Body: {'name': '咳嗽', 'distance': 1.45, 'category': '症状', 'pageNo': 1, 'limit': 10}
+2025-06-17 21:15:55.467 | INFO     |  | py_tools.logging.base:setup_logging:87 - setup logging success
+2025-06-17 21:15:55.487 | INFO     |  | src.knowledge.server:startup:50 - fastapi startup success
+2025-06-17 21:15:55.489 | ERROR    |  | src.knowledge.server:shutdown:55 - app shutdown
+2025-06-17 21:16:10.181 | ERROR    |  | src.knowledge.server:shutdown:55 - app shutdown
+2025-06-17 21:16:28.088 | INFO     |  | py_tools.logging.base:setup_logging:87 - setup logging success
+2025-06-17 21:16:28.094 | INFO     |  | src.knowledge.server:startup:50 - fastapi startup success
+2025-06-17 21:16:34.515 | INFO     | f30eb82824a7436db5bd66bd362c1601 | src.knowledge.middlewares.base:dispatch:35 - --> POST /v1/knowledge/nodes/paginated_search 127.0.0.1
+2025-06-17 21:16:34.516 | INFO     | f30eb82824a7436db5bd66bd362c1601 | src.knowledge.middlewares.base:dispatch:44 - --> Request Body: {'name': '咳嗽', 'distance': 1.45, 'category': '症状', 'pageNo': 1, 'limit': 10}
+2025-06-17 21:17:19.770 | INFO     | fb3015745d20490983cd3fe0ded707f9 | src.knowledge.middlewares.base:dispatch:35 - --> POST /v1/knowledge/nodes/paginated_search 127.0.0.1
+2025-06-17 21:17:19.772 | INFO     | fb3015745d20490983cd3fe0ded707f9 | src.knowledge.middlewares.base:dispatch:44 - --> Request Body: {'name': '咳嗽', 'distance': 1.45, 'category': '症状', 'pageNo': 1, 'limit': 10}
+2025-06-17 21:18:04.572 | INFO     | 0e188c2085d345578d6016ecb32eca2d | src.knowledge.middlewares.base:dispatch:35 - --> POST /v1/knowledge/nodes/paginated_search 127.0.0.1
+2025-06-17 21:18:04.573 | INFO     | 0e188c2085d345578d6016ecb32eca2d | src.knowledge.middlewares.base:dispatch:44 - --> Request Body: {'name': '咳嗽', 'distance': 1.45, 'category': '症状', 'pageNo': 1, 'limit': 10}
+2025-06-17 21:19:40.608 | ERROR    |  | src.knowledge.server:shutdown:55 - app shutdown
+2025-06-17 21:20:00.109 | INFO     |  | py_tools.logging.base:setup_logging:87 - setup logging success
+2025-06-17 21:20:00.114 | INFO     |  | src.knowledge.server:startup:50 - fastapi startup success
+2025-06-17 21:20:04.843 | INFO     | fd78ee0b5e6e428bb7bfb2a5eed948a3 | src.knowledge.middlewares.base:dispatch:35 - --> POST /v1/knowledge/nodes/paginated_search 127.0.0.1
+2025-06-17 21:20:04.845 | INFO     | fd78ee0b5e6e428bb7bfb2a5eed948a3 | src.knowledge.middlewares.base:dispatch:44 - --> Request Body: {'name': '咳嗽', 'distance': 1.45, 'category': '症状', 'pageNo': 1, 'limit': 10}
+2025-06-17 21:21:03.577 | INFO     | fd78ee0b5e6e428bb7bfb2a5eed948a3 | src.knowledge.middlewares.base:dispatch:56 - <-- Response: [Status: 200, Type: _StreamingResponse, Headers: {'content-length': '29407', 'content-type': 'application/json', 'access-control-allow-origin': '*', 'access-control-allow-credentials': 'true', 'x-response-time': '58.73s'}]
+2025-06-17 21:21:03.577 | INFO     | fd78ee0b5e6e428bb7bfb2a5eed948a3 | src.knowledge.middlewares.base:dispatch:100 - <-- 200 /v1/knowledge/nodes/paginated_search (took: 58.73s)
+2025-06-17 21:21:17.098 | INFO     | af4d0961975b40568b19f76b9999859c | src.knowledge.middlewares.base:dispatch:35 - --> POST /v1/knowledge/nodes/paginated_search 127.0.0.1
+2025-06-17 21:21:17.099 | INFO     | af4d0961975b40568b19f76b9999859c | src.knowledge.middlewares.base:dispatch:44 - --> Request Body: {'name': '感冒', 'distance': 1.45, 'category': '症状', 'pageNo': 1, 'limit': 10}
+2025-06-17 21:21:23.622 | INFO     | af4d0961975b40568b19f76b9999859c | src.knowledge.middlewares.base:dispatch:56 - <-- Response: [Status: 200, Type: _StreamingResponse, Headers: {'content-length': '554', 'content-type': 'application/json', 'access-control-allow-origin': '*', 'access-control-allow-credentials': 'true', 'x-response-time': '6.52s'}]
+2025-06-17 21:21:23.622 | INFO     | af4d0961975b40568b19f76b9999859c | src.knowledge.middlewares.base:dispatch:100 - <-- 200 /v1/knowledge/nodes/paginated_search (took: 6.52s)
+2025-06-17 21:21:56.715 | INFO     | ff3de0ed845740db905395a693c036d0 | src.knowledge.middlewares.base:dispatch:35 - --> GET /v1/knowledge/nodes/1949881/relationships 127.0.0.1
+2025-06-17 21:21:57.497 | INFO     | ff3de0ed845740db905395a693c036d0 | src.knowledge.middlewares.base:dispatch:56 - <-- Response: [Status: 200, Type: _StreamingResponse, Headers: {'content-length': '19125', 'content-type': 'application/json', 'x-response-time': '0.78s'}]
+2025-06-17 21:21:57.497 | INFO     | ff3de0ed845740db905395a693c036d0 | src.knowledge.middlewares.base:dispatch:100 - <-- 200 /v1/knowledge/nodes/1949881/relationships (took: 0.78s)