style: fix ruff check

This commit is contained in:
Wenjie Zhang 2025-05-24 11:29:45 +08:00
parent 911f88d7eb
commit a6857c3e21
39 changed files with 134 additions and 478 deletions

View File

@ -41,10 +41,10 @@ dependencies = [
]
[tool.ruff]
line-length = 140 # 代码最大行宽
select = [ # 选择的规则
lint.select = [ # 选择的规则
"F",
"E",
"W",
"UP",
]
ignore = ["F401"] # 忽略的规则
lint.ignore = ["F401"] # 忽略的规则

View File

@ -1,35 +0,0 @@
dashscope>=1.20.3
PyMuPDF==1.23.26
peft>=0.11.1
FlagEmbedding>=1.3.2
Flask>=3.0.3
Flask_Cors>=4.0.1
llama_index>=0.11.8
openai>=1.44.1
paddleocr>=2.8.1
paddlepaddle>=2.4.0
pandas>=2.2.2
Pillow>=10.4.0
pymilvus>=2.4.4
python-dotenv>=1.0.1
PyYAML>=6.0.2
qianfan>=0.4.7
torch>=2.4.0
tqdm>=4.66.1
zhipuai>=2.1.2
neo4j>=5.22.0
sentencepiece>=0.1.99
llama-index-readers-file
opencv-python-headless
docx2txt
uvicorn[standard]
fastapi
python-multipart
tavily-python
rapidocr_onnxruntime
langchain
langsmith
langgraph
langchain-openai
langchain-community
PyJWT>=2.10.1

View File

@ -17,4 +17,4 @@ outputs = llm.generate(prompts, sampling_params)
for output in outputs:
prompt = output.prompt
generated_text = output.outputs[0].text
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")

View File

@ -15,4 +15,4 @@ chat_response = client.chat.completions.create(
{"role": "user", "content": "Tell me a joke."},
]
)
print("Chat response:", chat_response)
print("Chat response:", chat_response)

View File

@ -65,4 +65,4 @@ class DBManager:
session.close()
# 创建全局数据库管理器实例
db_manager = DBManager()
db_manager = DBManager()

View File

@ -1,3 +1,3 @@
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
Base = declarative_base()

View File

@ -103,4 +103,4 @@ class KnowledgeNode(Base):
"start_char_idx": self.start_char_idx,
"end_char_idx": self.end_char_idx,
"metadata": self.meta_info or {} # 确保映射正确
}
}

View File

@ -18,4 +18,4 @@ class Thread(Base):
update_at = Column(DateTime, default=func.now(), onupdate=func.now(), comment="更新时间")
description = Column(String(255), nullable=True, comment="描述")
status = Column(Integer, default=1, comment="状态")
status = Column(Integer, default=1, comment="状态")

View File

@ -52,4 +52,4 @@ class OperationLog(Base):
"details": self.details,
"ip_address": self.ip_address,
"timestamp": self.timestamp.isoformat() if self.timestamp else None
}
}

View File

@ -1,9 +1,8 @@
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel
from typing import List, Optional
from sqlalchemy.orm import Session
from datetime import datetime, timedelta
from datetime import datetime
from server.db_manager import db_manager
from server.models.user_model import User, OperationLog
@ -27,16 +26,16 @@ class UserCreate(BaseModel):
role: str = "user"
class UserUpdate(BaseModel):
username: Optional[str] = None
password: Optional[str] = None
role: Optional[str] = None
username: str | None = None
password: str | None = None
role: str | None = None
class UserResponse(BaseModel):
id: int
username: str
role: str
created_at: str
last_login: Optional[str] = None
last_login: str | None = None
class InitializeAdmin(BaseModel):
username: str
@ -202,7 +201,7 @@ async def create_user(
return new_user.to_dict()
# 路由:获取所有用户(管理员权限)
@auth.get("/users", response_model=List[UserResponse])
@auth.get("/users", response_model=list[UserResponse])
async def read_users(
skip: int = 0,
limit: int = 100,
@ -344,4 +343,4 @@ async def delete_user(
db.delete(user)
db.commit()
return {"success": True, "message": "用户已删除"}
return {"success": True, "message": "用户已删除"}

View File

@ -1,15 +1,13 @@
from fastapi import Request, Body, Depends
from fastapi import APIRouter
from fastapi import Request, Body
base = APIRouter()
from src import config, retriever, knowledge_base, graph_base
from src.utils import logger
from server.utils.auth_middleware import get_admin_user, get_superadmin_user
from server.models.user_model import User
base = APIRouter()
@base.get("/")
async def route_index():
return {"message": "You Got It!"}
@ -49,7 +47,7 @@ def get_log(current_user: User = Depends(get_admin_user)):
from src.utils.logging_config import LOG_FILE
from collections import deque
with open(LOG_FILE, 'r') as f:
with open(LOG_FILE) as f:
last_lines = deque(f, maxlen=1000)
log = ''.join(last_lines)

View File

@ -8,7 +8,6 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from langchain_core.messages import AIMessageChunk, HumanMessage
from sqlalchemy.orm import Session
from typing import List, Optional
from pydantic import BaseModel
from src import executor, config, retriever
@ -270,7 +269,7 @@ async def save_agent_config(
if result:
return {"success": True, "message": f"智能体 {agent_name} 配置已保存"}
else:
raise HTTPException(status_code=500, detail=f"保存智能体配置失败")
raise HTTPException(status_code=500, detail="保存智能体配置失败")
except Exception as e:
logger.error(f"保存智能体配置出错: {e}, {traceback.format_exc()}")
@ -318,18 +317,18 @@ async def get_agent_config(
# ==================== 线程管理 API ====================
class ThreadCreate(BaseModel):
title: Optional[str] = None
title: str | None = None
agent_id: str
description: Optional[str] = None
metadata: Optional[dict] = None
description: str | None = None
metadata: dict | None = None
class ThreadResponse(BaseModel):
id: str
user_id: str
agent_id: str
title: Optional[str]
description: Optional[str]
title: str | None = None
description: str | None = None
create_at: str
update_at: str
@ -366,9 +365,9 @@ async def create_thread(
}
@chat.get("/threads", response_model=List[ThreadResponse])
@chat.get("/threads", response_model=list[ThreadResponse])
async def list_threads(
agent_id: Optional[str] = None,
agent_id: str | None = None,
db: Session = Depends(get_db),
current_user: User = Depends(get_required_user)
):
@ -420,8 +419,8 @@ async def delete_thread(
class ThreadUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
title: str | None = None
description: str | None = None
@chat.put("/thread/{thread_id}", response_model=ThreadResponse)

View File

@ -1,7 +1,6 @@
import os
import asyncio
import traceback
from typing import List, Optional
from fastapi import APIRouter, File, UploadFile, HTTPException, Depends, Body, Form, Query
from src.utils import logger, hashstr
@ -25,7 +24,7 @@ async def get_databases(current_user: User = Depends(get_admin_user)):
async def create_database(
database_name: str = Body(...),
description: str = Body(...),
dimension: Optional[int] = Body(None),
dimension: int | None = Body(None),
current_user: User = Depends(get_admin_user)
):
logger.debug(f"Create database {database_name}")
@ -53,19 +52,19 @@ async def query_test(query: str = Body(...), meta: dict = Body(...), current_use
return result
@data.post("/file-to-chunk")
async def file_to_chunk(files: List[str] = Body(...), params: dict = Body(...), current_user: User = Depends(get_admin_user)):
async def file_to_chunk(files: list[str] = Body(...), params: dict = Body(...), current_user: User = Depends(get_admin_user)):
logger.debug(f"File to chunk: {files} {params=}")
result = await knowledge_base.file_to_chunk(files, params=params)
return result
@data.post("/url-to-chunk")
async def url_to_chunk(urls: List[str] = Body(...), params: dict = Body(...), current_user: User = Depends(get_admin_user)):
async def url_to_chunk(urls: list[str] = Body(...), params: dict = Body(...), current_user: User = Depends(get_admin_user)):
logger.debug(f"Url to chunk: {urls}")
result = await knowledge_base.url_to_chunk(urls, params=params)
return result
@data.post("/add-by-file")
async def create_document_by_file(db_id: str = Body(...), files: List[str] = Body(...), current_user: User = Depends(get_admin_user)):
async def create_document_by_file(db_id: str = Body(...), files: list[str] = Body(...), current_user: User = Depends(get_admin_user)):
logger.debug(f"Add document in {db_id} by file: {files}")
try:
await knowledge_base.add_files(db_id, files)
@ -113,7 +112,7 @@ async def get_document_info(db_id: str, file_id: str, current_user: User = Depen
@data.post("/upload")
async def upload_file(
file: UploadFile = File(...),
db_id: Optional[str] = Query(None),
db_id: str | None = Query(None),
current_user: User = Depends(get_admin_user)
):
if not file.filename:
@ -170,7 +169,7 @@ async def get_graph_nodes(kgdb_name: str, num: int, current_user: User = Depends
return {"result": graph_base.format_general_results(result), "message": "success"}
@data.post("/graph/add-by-jsonl")
async def add_graph_entity(file_path: str = Body(...), kgdb_name: Optional[str] = Body(None), current_user: User = Depends(get_admin_user)):
async def add_graph_entity(file_path: str = Body(...), kgdb_name: str | None = Body(None), current_user: User = Depends(get_admin_user)):
if not config.enable_knowledge_graph:
return {"message": "知识图谱未启用", "status": "failed"}

View File

@ -1 +1 @@
# utils包初始化文件
# utils包初始化文件

View File

@ -1,5 +1,4 @@
from typing import Optional, List, Callable
from fastapi import Depends, HTTPException, status, APIRouter
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from jose import JWTError, jwt
@ -30,7 +29,7 @@ def get_db():
db.close()
# 获取当前用户
async def get_current_user(token: Optional[str] = Depends(oauth2_scheme), db: Session = Depends(get_db)):
async def get_current_user(token: str | None = Depends(oauth2_scheme), db: Session = Depends(get_db)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的凭证",
@ -66,7 +65,7 @@ async def get_current_user(token: Optional[str] = Depends(oauth2_scheme), db: Se
return user
# 获取已登录用户抛出401如果未登录
async def get_required_user(user: Optional[User] = Depends(get_current_user)):
async def get_required_user(user: User | None = Depends(get_current_user)):
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,

View File

@ -2,17 +2,17 @@ from dotenv import load_dotenv
load_dotenv("src/.env")
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor # noqa: E402
executor = ThreadPoolExecutor()
from src.config import Config
from src.config import Config # noqa: E402
config = Config()
from src.core import KnowledgeBase
from src.core import KnowledgeBase # noqa: E402
knowledge_base = KnowledgeBase()
from src.core import GraphDatabase
from src.core import GraphDatabase # noqa: E402
graph_base = GraphDatabase()
from src.core.retriever import Retriever
retriever = Retriever()
from src.core.retriever import Retriever # noqa: E402
retriever = Retriever()

View File

@ -39,4 +39,4 @@ __all__ = ["agent_manager"]
if __name__ == "__main__":
pass
pass

View File

@ -1,4 +1,4 @@
from .graph import ChatbotAgent
from .configuration import ChatbotConfiguration
__all__ = ["ChatbotAgent", "ChatbotConfiguration"]
__all__ = ["ChatbotAgent", "ChatbotConfiguration"]

View File

@ -63,7 +63,6 @@ class ChatbotAgent(BaseAgent):
if self.graph:
return self.graph
conf = self.config_schema.from_runnable_config(config_schema, agent_name=self.name)
workflow = StateGraph(State, config_schema=self.config_schema)
workflow.add_node("chatbot", self.llm_call)
workflow.add_node("tools", ToolNode(tools=list(get_all_tools().values())))

View File

@ -1,4 +1,4 @@
from .graph import ReActAgent
from .configuration import ReActConfiguration
__all__ = ["ReActAgent", "ReActConfiguration"]
__all__ = ["ReActAgent", "ReActConfiguration"]

View File

@ -2,24 +2,12 @@ import os
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.prebuilt import create_react_agent
model = ChatOpenAI(model="glm-4-plus",
api_key=os.getenv("ZHIPUAI_API_KEY"),
base_url="https://open.bigmodel.cn/api/paas/v4/",
temperature=0)
# For this tutorial we will use custom tool that returns pre-defined values for weather in two cities (NYC & SF)
from typing import Literal, Annotated
from langchain_core.tools import tool, StructuredTool
tools = []
from langgraph.prebuilt import create_react_agent
graph = create_react_agent(model, tools=tools, checkpointer=InMemorySaver())

View File

@ -4,8 +4,7 @@ import os
import yaml
import uuid
from pathlib import Path
from typing import Type, Annotated, Optional, TypedDict
from enum import Enum
from typing import Annotated, TypedDict
from abc import abstractmethod
from dataclasses import dataclass, fields, field
@ -33,7 +32,7 @@ class Configuration(dict):
@classmethod
def from_runnable_config(
cls, config: Optional[RunnableConfig] = None, agent_name: str = None
cls, config: RunnableConfig | None = None, agent_name: str | None = None
) -> Configuration:
"""Create a Configuration instance from a RunnableConfig object.
@ -58,18 +57,18 @@ class Configuration(dict):
# 合并三级配置,注意优先级
merged_config = {}
for field in _fields:
for config_field in _fields:
# 1. 默认使用类默认值
if hasattr(instance, field):
merged_config[field] = getattr(instance, field)
if hasattr(instance, config_field):
merged_config[config_field] = getattr(instance, config_field)
# 2. 如果文件配置中有此字段,则覆盖
if field in file_config:
merged_config[field] = file_config[field]
if config_field in file_config:
merged_config[config_field] = file_config[config_field]
# 3. 如果运行时配置中有此字段,则覆盖
if field in configurable:
merged_config[field] = configurable[field]
if config_field in configurable:
merged_config[config_field] = configurable[config_field]
# 创建并返回配置实例
# logger.debug(f"合并配置: {merged_config}")
@ -82,7 +81,7 @@ class Configuration(dict):
file_config = {}
if os.path.exists(config_file_path):
try:
with open(config_file_path, 'r', encoding='utf-8') as f:
with open(config_file_path, encoding='utf-8') as f:
file_config = yaml.safe_load(f) or {}
# logger.info(f"从文件加载智能体 {agent_name} 配置: {file_config}")
except Exception as e:
@ -158,7 +157,7 @@ class Configuration(dict):
},
)
class BaseAgent():
class BaseAgent:
"""
定义一个基础 Agent 各类 graph 继承
@ -245,4 +244,4 @@ class BaseAgent():
必须确保在编译时设置 checkpointer否则将无法获取历史记录
例如: graph = workflow.compile(checkpointer=sqlite_checkpointer)
"""
pass
pass

View File

@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import datetime, timezone, UTC
import asyncio
from src.models import select_model
@ -57,5 +57,5 @@ async def agent_cli(agent: BaseAgent, config: RunnableConfig = None):
print(f"Tool: {msg.content}")
def get_cur_time_with_utc():
return datetime.now(tz=timezone.utc).isoformat()
return datetime.now(tz=UTC).isoformat()

View File

@ -50,7 +50,7 @@ class Config(SimpleConfig):
self.add_item("enable_reranker", default=False, des="是否开启重排序")
self.add_item("enable_knowledge_base", default=False, des="是否开启知识库")
self.add_item("enable_knowledge_graph", default=False, des="是否开启知识图谱")
self.add_item("enable_web_search", default=False, des="是否开启网页搜索(注:现阶段会根据 TAVILY_API_KEY 自动开启,无法手动配置,将会在下个版本移除此配置项)")
self.add_item("enable_web_search", default=False, des="是否开启网页搜索(注:现阶段会根据 TAVILY_API_KEY 自动开启,无法手动配置,将会在下个版本移除此配置项)") # noqa: E501
# 默认智能体配置
self.add_item("default_agent_id", default="", des="默认智能体ID")
# 模型配置
@ -60,7 +60,7 @@ class Config(SimpleConfig):
self.add_item("model_name", default="Qwen/Qwen2.5-7B-Instruct", des="模型名称")
self.add_item("embed_model", default="siliconflow/BAAI/bge-m3", des="Embedding 模型", choices=list(self.embed_model_names.keys()))
self.add_item("reranker", default="siliconflow/BAAI/bge-reranker-v2-m3", des="Re-Ranker 模型", choices=list(self.reranker_names.keys()))
self.add_item("reranker", default="siliconflow/BAAI/bge-reranker-v2-m3", des="Re-Ranker 模型", choices=list(self.reranker_names.keys())) # noqa: E501
self.add_item("model_local_paths", default={}, des="本地模型路径")
self.add_item("use_rewrite_query", default="on", des="重写查询", choices=["off", "on", "hyde"])
self.add_item("device", default="cuda", des="运行本地模型的设备", choices=["cpu", "cuda"])
@ -92,12 +92,12 @@ class Config(SimpleConfig):
models.yaml models.private.yml 中更新 MODEL_NAMES
"""
with open(Path("src/static/models.yaml"), 'r', encoding='utf-8') as f:
with open(Path("src/static/models.yaml"), encoding='utf-8') as f:
_models = yaml.safe_load(f)
# 尝试打开一个 models.private.yml 文件,用来覆盖 models.yaml 中的配置
try:
with open(Path("src/static/models.private.yml"), 'r', encoding='utf-8') as f:
with open(Path("src/static/models.private.yml"), encoding='utf-8') as f:
_models_private = yaml.safe_load(f)
except FileNotFoundError:
_models_private = {}
@ -129,7 +129,8 @@ class Config(SimpleConfig):
if os.path.exists(self.model_dir):
logger.debug(f"MODEL_DIR {self.model_dir} 下面的文件夹: {os.listdir(self.model_dir)}")
else:
logger.warning(f"提醒MODEL_DIR {self.model_dir} 不存在,如果未配置,请忽略,如果配置了,请检查是否配置正确,比如 docker-compose 文件中的映射")
logger.warning(f"提醒MODEL_DIR {self.model_dir} 不存在,如果未配置,请忽略,如果配置了,请检查是否配置正确;"
"比如 docker-compose 文件中的映射")
# 检查模型提供商是否存在
@ -177,7 +178,7 @@ class Config(SimpleConfig):
if self.filename is not None and os.path.exists(self.filename):
if self.filename.endswith(".json"):
with open(self.filename, 'r') as f:
with open(self.filename) as f:
content = f.read()
if content:
local_config = json.loads(content)
@ -186,7 +187,7 @@ class Config(SimpleConfig):
print(f"{self.filename} is empty.")
elif self.filename.endswith(".yaml"):
with open(self.filename, 'r') as f:
with open(self.filename) as f:
content = f.read()
if content:
local_config = yaml.safe_load(content)

View File

@ -1,3 +1,3 @@
from .history import *
from .history import HistoryManager
from .knowledgebase import KnowledgeBase
from .graphbase import GraphDatabase

View File

@ -189,7 +189,7 @@ class GraphDatabase:
# 筛选出没有embedding的节点
nodes_without_embedding = session.execute_read(_get_nodes_without_embedding, all_entities)
if not nodes_without_embedding:
logger.info(f"所有实体已有embedding无需重新计算")
logger.info("所有实体已有embedding无需重新计算")
return
logger.info(f"需要为{len(nodes_without_embedding)}/{len(all_entities)}个实体计算embedding")
@ -200,7 +200,11 @@ class GraphDatabase:
for i in range(0, total_entities, max_batch_size):
batch_entities = nodes_without_embedding[i:i+max_batch_size]
logger.debug(f"Processing entities batch {i//max_batch_size + 1}/{(total_entities-1)//max_batch_size + 1} ({len(batch_entities)} entities)")
logger.debug(
f"Processing entities batch "
f"{i//max_batch_size + 1}/{(total_entities-1)//max_batch_size + 1} "
f"({len(batch_entities)} entities)"
)
# 批量获取嵌入向量
batch_embeddings = await self.aget_embedding(batch_entities)
@ -221,7 +225,7 @@ class GraphDatabase:
logger.info(f"Start adding entity to {kgdb_name} with {file_path}")
def read_triples(file_path):
with open(file_path, 'r', encoding='utf-8') as file:
with open(file_path, encoding='utf-8') as file:
for line in file:
if line.strip():
yield json.loads(line.strip())
@ -478,7 +482,7 @@ class GraphDatabase:
try:
graph_info = self.get_graph_info(graph_name)
if graph_info is None:
logger.error(f"图数据库信息为空,无法保存")
logger.error("图数据库信息为空,无法保存")
return False
info_file_path = os.path.join(self.work_dir, "graph_info.json")
@ -521,7 +525,7 @@ class GraphDatabase:
logger.debug(f"图数据库信息文件不存在:{info_file_path}")
return False
with open(info_file_path, 'r', encoding='utf-8') as f:
with open(info_file_path, encoding='utf-8') as f:
graph_info = json.load(f)
# 更新对象属性

View File

@ -1,7 +1,6 @@
from src.utils.prompts import get_system_prompt
from src.utils.logging_config import logger
class HistoryManager():
class HistoryManager:
def __init__(self, history=None, system_prompt=None):
self.messages = []
@ -47,4 +46,4 @@ class HistoryManager():
for message in self.messages:
msg = message["content"].replace('\n', ' ')
history_str += f"\n{message['role']}: {msg}"
return history_str
return history_str

View File

@ -73,11 +73,11 @@ class KnowledgeBase:
return
from src.models.embedding import get_embedding_model
self.embed_model = get_embedding_model(config)
self.embed_model = get_embedding_model()
if config.enable_reranker:
from src.models.rerank_model import get_reranker
self.reranker = get_reranker(config)
self.reranker = get_reranker()
if not self.connect_to_milvus():
raise ConnectionError("Failed to connect to Milvus")
@ -472,10 +472,7 @@ class KnowledgeBase:
async def add_chunks(self, db_id, file_chunks):
"""添加分块"""
db = self.get_kb_by_id(db_id)
if db["embed_model"] != self.embed_model.embed_model_fullname:
logger.error(f"Embed model not match, {db['embed_model']} != {self.embed_model.embed_model_fullname}")
return {"message": f"Embed model not match, cur: {self.embed_model.embed_model_fullname}, req: {db['embed_model']}", "status": "failed"}
assert self.check_embed_model(db_id), f"Embed model not match, {db['embed_model']} != {self.embed_model.embed_model_fullname}"
for file_id, chunk_info in file_chunks.items():
# 在数据库中创建文件记录
@ -505,10 +502,7 @@ class KnowledgeBase:
async def add_files(self, db_id, files, params=None):
db = self.get_kb_by_id(db_id)
if not self.check_embed_model(db_id):
logger.error(f"Embed model not match, {db['embed_model']} != {self.embed_model.embed_model_fullname}")
return {"message": f"Embed model not match, cur: {self.embed_model.embed_model_fullname}, req: {db['embed_model']}", "status": "failed"}
assert self.check_embed_model(db_id), f"Embed model not match, {db['embed_model']} != {self.embed_model.embed_model_fullname}"
# Preprocessing the files to the queue
new_files = await self.file_to_chunk(files, params=params)
@ -625,11 +619,11 @@ class KnowledgeBase:
"embed_model": db["embed_model"],
}
else:
logger.warning((
logger.warning(
f"无法将知识库 {db['name']} 转换为 Tools, 因为向量模型不匹配,"
f"当前向量模型: {self.embed_model.embed_model_fullname}"
f"知识库向量模型: {db['embed_model']}"
))
)
return retrievers
################################
@ -648,7 +642,8 @@ class KnowledgeBase:
logger.info(f"Successfully connected to Milvus at {uri}")
return True
except MilvusException as e:
logger.error(f"Failed to connect to Milvus: {e},请检查 milvus 的容器是否正常运行,如果已退出,请重新启动 `docker restart milvus-standalone-dev`")
logger.error(f"Failed to connect to Milvus: {e},请检查 milvus 的容器是否正常运行。")
logger.error("如果已退出,请重新启动 `docker restart milvus-standalone-dev`。")
return False
def get_collection_names(self):
@ -781,4 +776,4 @@ def gen_filename_from_url(url):
if len(filename) > 100:
filename = filename[:97] + "..."
filename = filename.replace('/', '_')
return filename
return filename

View File

@ -21,7 +21,7 @@ def migrate_json_to_sqlite():
try:
# 读取旧的JSON文件
with open(json_path, 'r', encoding='utf-8') as f:
with open(json_path, encoding='utf-8') as f:
old_data = json.load(f)
if not old_data or not isinstance(old_data, dict) or not old_data.get("databases"):
@ -36,7 +36,7 @@ def migrate_json_to_sqlite():
logger.info(f"正在迁移知识库: {db_info.get('name')} (ID: {db_id})")
# 创建知识库
db_dict = db_manager.create_database(
db_manager.create_database(
db_id=db_id,
name=db_info.get('name', '未命名知识库'),
description=db_info.get('description', ''),
@ -81,4 +81,4 @@ def migrate_json_to_sqlite():
except Exception as e:
logger.error(f"迁移过程中出错: {e}")
return False
return False

View File

@ -13,7 +13,7 @@ class Retriever:
def _load_models(self):
if config.enable_reranker:
self.reranker = get_reranker(config)
self.reranker = get_reranker()
if config.enable_web_search:
from src.utils.web_search import WebSearcher
@ -168,7 +168,7 @@ class Retriever:
entities = []
if refs["meta"].get("use_graph"):
from src.utils.prompts import entity_extraction_prompt_template as entity_template
from src.utils.prompts import keywords_prompt_template as entity_template
# from src.utils.prompts import keywords_prompt_template as entity_templat|e
entity_extraction_prompt = entity_template.format(text=query)
entities = model.predict(entity_extraction_prompt).content.split("<->")

View File

@ -4,7 +4,7 @@ from openai import OpenAI
from src.utils import logger, get_docker_safe_url
from langchain_openai import ChatOpenAI
class OpenAIBase():
class OpenAIBase:
def __init__(self, api_key, base_url, model_name, chat_open_ai=None, **kwargs):
self.api_key = api_key
self.base_url = base_url
@ -61,49 +61,6 @@ class OpenAIBase():
logger.error(f"Error getting models: {e}")
return []
def _get_model_by_model_url(self, model_url):
"""
Refs: https://docs.together.ai/reference/models-1
Return: [
{
"id": "meta-llama/Meta-Llama-3-70B-Instruct-Turbo",
"object": "model",
"created": 0,
"type": "chat",
"running": false,
"display_name": "Meta Llama 3 70B Instruct Turbo",
"organization": "Meta",
"link": "https://huggingface.co/meta-llama/Meta-Llama-3-70B-Instruct",
"license": "Llama-3 (Other)",
"context_length": 8192,
"config": {
"chat_template": "{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}",
"stop": [
"<|eot_id|>"
],
"bos_token": "<|begin_of_text|>",
"eos_token": "<|end_of_text|>"
},
"pricing": {
"hourly": 0,
"input": 0.88,
"output": 0.88,
"base": 0,
"finetune": 0
}
},
]
"""
headers = {
"accept": "application/json",
"authorization": f"Bearer {self.api_key}"
}
response = requests.get(model_url, headers=headers)
return response.json()
class OpenModel(OpenAIBase):
def __init__(self, model_name=None):
@ -214,4 +171,4 @@ class DashScope(OpenAIBase):
if __name__ == "__main__":
pass
pass

View File

@ -2,7 +2,6 @@ import os
import json
import requests
import asyncio
import json
from abc import abstractmethod
from zhipuai import ZhipuAI
from langchain_huggingface import HuggingFaceEmbeddings
@ -68,7 +67,7 @@ class BaseEmbeddingModel:
return data
class LocalEmbeddingModel(BaseEmbeddingModel):
def __init__(self, config, **kwargs):
def __init__(self, **kwargs):
info = config.embed_model_names[config.embed_model]
self.model = config.model_local_paths.get(info["name"], info.get("local_path"))
@ -104,7 +103,7 @@ class LocalEmbeddingModel(BaseEmbeddingModel):
return await self.model.aembed_documents(message)
def encode_queries(self, queries):
logger.warning(f"Huggingface Model 不支持批量 encode queries因此使用训练实现")
logger.warning("Huggingface Model 不支持批量 encode queries因此使用训练实现")
data = []
for q in queries:
data.append(self.predict(q))
@ -114,7 +113,7 @@ class LocalEmbeddingModel(BaseEmbeddingModel):
class ZhipuEmbedding(BaseEmbeddingModel):
def __init__(self, config) -> None:
def __init__(self) -> None:
self.config = config
self.model = config.embed_model_names[config.embed_model]["name"]
self.dimension = config.embed_model_names[config.embed_model]["dimension"]
@ -131,7 +130,7 @@ class ZhipuEmbedding(BaseEmbeddingModel):
class OllamaEmbedding(BaseEmbeddingModel):
def __init__(self, config) -> None:
def __init__(self) -> None:
self.info = config.embed_model_names[config.embed_model]
self.model = self.info["name"]
self.url = self.info.get("url", "http://localhost:11434/api/embed")
@ -155,7 +154,7 @@ class OllamaEmbedding(BaseEmbeddingModel):
class OtherEmbedding(BaseEmbeddingModel):
def __init__(self, config) -> None:
def __init__(self) -> None:
self.info = config.embed_model_names[config.embed_model]
self.embed_model_fullname = config.embed_model
self.dimension = self.info.get("dimension", None)
@ -182,27 +181,28 @@ class OtherEmbedding(BaseEmbeddingModel):
"input": message,
}
def get_embedding_model(config):
def get_embedding_model():
if not config.enable_knowledge_base:
return None
provider, model_name = config.embed_model.split('/', 1)
assert config.embed_model in config.embed_model_names.keys(), f"Unsupported embed model: {config.embed_model}, only support {config.embed_model_names.keys()}"
support_embed_models = config.embed_model_names.keys()
assert config.embed_model in support_embed_models, f"Unsupported embed model: {config.embed_model}, only support {support_embed_models}"
logger.debug(f"Loading embedding model {config.embed_model}")
if provider == "local":
model = LocalEmbeddingModel(config)
model = LocalEmbeddingModel()
elif provider == "zhipu":
model = ZhipuEmbedding(config)
model = ZhipuEmbedding()
elif provider == "ollama":
model = OllamaEmbedding(config)
model = OllamaEmbedding()
else:
model = OtherEmbedding(config)
model = OtherEmbedding()
return model
def handle_local_model(paths, model_name, default_path):
model_path = paths.get(model_name, default_path)
return model_path
return model_path

View File

@ -9,7 +9,7 @@ from src.utils.logging_config import logger
class LocalReranker(FlagReranker):
def __init__(self, config, **kwargs):
def __init__(self, **kwargs):
model_info = config.reranker_names[config.reranker]
model_name_or_path = config.model_local_paths.get(model_info["name"], model_info.get("local_path"))
model_name_or_path = model_name_or_path or model_info["name"]
@ -22,8 +22,8 @@ class LocalReranker(FlagReranker):
def sigmoid(x):
return 1 / (1 + np.exp(-x))
class SiliconFlowReranker():
def __init__(self, config, **kwargs):
class SiliconFlowReranker:
def __init__(self, **kwargs):
self.url = "https://api.siliconflow.cn/v1/rerank"
self.model = config.reranker_names[config.reranker]["name"]
@ -58,13 +58,14 @@ class SiliconFlowReranker():
"max_chunks_per_doc": max_length,
}
def get_reranker(config):
assert config.reranker in config.reranker_names.keys(), f"Unsupported Reranker: {config.reranker}, only support {config.reranker_names.keys()}"
def get_reranker():
support_rerankers = config.reranker_names.keys()
assert config.reranker in support_rerankers, f"Unsupported Reranker: {config.reranker}, only support {support_rerankers}"
provider, model_name = config.reranker.split('/', 1)
if provider == "local":
return LocalReranker(config)
return LocalReranker()
elif provider == "siliconflow":
return SiliconFlowReranker(config)
return SiliconFlowReranker()
else:
raise ValueError(f"Unsupported Reranker: {config.reranker}, only support {config.reranker_names.keys()}")

View File

@ -2,4 +2,4 @@ from src.plugins._ocr import OCRPlugin
ocr = OCRPlugin()
__all__ = ["ocr"]
__all__ = ["ocr"]

View File

@ -24,7 +24,7 @@ class OCRPlugin:
def load_model(self):
"""加载 OCR 模型"""
logger.info(f"加载 OCR 模型,仅在第一次调用时加载")
logger.info("加载 OCR 模型,仅在第一次调用时加载")
model_dir = os.path.join(os.getenv("MODEL_DIR", ""), "SWHL/RapidOCR")
det_model_dir = os.path.join(model_dir, "PP-OCRv4/ch_PP-OCRv4_det_infer.onnx")
rec_model_dir = os.path.join(model_dir, "PP-OCRv4/ch_PP-OCRv4_rec_infer.onnx")
@ -75,7 +75,7 @@ class OCRPlugin:
text = '\n'.join([line[1] for line in result])
return text
else:
logger.warning(f"OCR未能识别出文本内容")
logger.warning("OCR未能识别出文本内容")
return ""
except Exception as e:
@ -214,7 +214,7 @@ def plainreader(file_path):
"""读取普通文本文件并返回text文本"""
assert os.path.exists(file_path), "File not found"
with open(file_path, "r") as f:
with open(file_path) as f:
text = f.read()
return text

View File

@ -1,249 +0,0 @@
# readmore: https://github.com/zjunlp/DeepKE/blob/main/example/llm/OneKE.md
import os
import json
import torch
import dotenv
from transformers import (
AutoConfig,
AutoTokenizer,
AutoModelForCausalLM,
GenerationConfig,
BitsAndBytesConfig
)
from src.utils import logger
dotenv.load_dotenv()
instruction_mapper = {
'NERzh': "你是专门进行实体抽取的专家。请从input中抽取出符合schema定义的实体不存在的实体类型返回空列表。请按照JSON字符串的格式回答。",
'REzh': "你是专门进行关系抽取的专家。请从input中抽取出符合schema定义的关系三元组。请按照JSON字符串的格式回答。",
'EEzh': "你是专门进行事件提取的专家。请从input中抽取出符合schema定义的事件不存在的事件返回空列表不存在的论元返回NAN如果论元存在多值请返回列表。请按照JSON字符串的格式回答。",
'EETzh': "你是专门进行事件提取的专家。请从input中抽取出符合schema定义的事件类型及事件触发词不存在的事件返回空列表。请按照JSON字符串的格式回答。",
'EEAzh': "你是专门进行事件论元提取的专家。请从input中抽取出符合schema定义的事件论元及论元角色不存在的论元返回NAN或空字典如果论元存在多值请返回列表。请按照JSON字符串的格式回答。",
'KGzh': '你是一个图谱实体知识结构化专家。根据输入实体类型(entity type)的schema描述从文本中抽取出相应的实体实例和其属性信息不存在的属性不输出, 属性存在多值就返回列表并输出为可解析的json格式。',
'NERen': "You are an expert in named entity recognition. Please extract entities that match the schema definition from the input. Return an empty list if the entity type does not exist. Please respond in the format of a JSON string.",
'REen': "You are an expert in relationship extraction. Please extract relationship triples that match the schema definition from the input. Return an empty list for relationships that do not exist. Please respond in the format of a JSON string.",
'EEen': "You are an expert in event extraction. Please extract events from the input that conform to the schema definition. Return an empty list for events that do not exist, and return NAN for arguments that do not exist. If an argument has multiple values, please return a list. Respond in the format of a JSON string.",
'EETen': "You are an expert in event extraction. Please extract event types and event trigger words from the input that conform to the schema definition. Return an empty list for non-existent events. Please respond in the format of a JSON string.",
'EEAen': "You are an expert in event argument extraction. Please extract event arguments and their roles from the input that conform to the schema definition, which already includes event trigger words. If an argument does not exist, return NAN or an empty dictionary. Please respond in the format of a JSON string.",
'KGen': 'You are an expert in structured knowledge systems for graph entities. Based on the schema description of the input entity type, you extract the corresponding entity instances and their attribute information from the text. Attributes that do not exist should not be output. If an attribute has multiple values, a list should be returned. The results should be output in a parsable JSON format.',
}
split_num_mapper = {
'NER':6, 'RE':4, 'EE':4, 'EET':4, 'EEA':4, 'KG':1
}
class OneKE:
def __init__(self, config=None):
self.config = config
model_name_or_path = config.model_local_paths.get('zjunlp/OneKE', "zjunlp/OneKE")
logger.info(f"Loading KGC model OneKE from {model_name_or_path}")
model_config = AutoConfig.from_pretrained(model_name_or_path, trust_remote_code=True)
self.tokenizer = AutoTokenizer.from_pretrained(model_name_or_path, trust_remote_code=True)
self.quantization_config=BitsAndBytesConfig(
load_in_4bit=True,
llm_int8_threshold=6.0,
llm_int8_has_fp16_weight=False,
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
)
self.generate_config = GenerationConfig(
max_length=1024,
max_new_tokens=512,
return_dict_in_generate=True
)
self.model = AutoModelForCausalLM.from_pretrained(
model_name_or_path,
config=model_config,
device_map="auto",
quantization_config=self.quantization_config,
torch_dtype=torch.bfloat16,
trust_remote_code=True,
)
self.model.eval()
def construct_input(self, text, schema, task, language="zh", use_split=False):
if use_split:
split_num = split_num_mapper[task]
if isinstance(schema, dict):
schema_keys = list(schema.keys())
schema_keys = [schema_keys[i:i+split_num] for i in range(0, len(schema_keys), split_num)]
schema = [{k: schema[k] for k in keys} for keys in schema_keys]
else:
schema = [schema[i:i+split_num] for i in range(0, len(schema), split_num)]
else:
schema = [schema]
sintructs = []
for s in schema:
sintruct = json.dumps({
"instruction": instruction_mapper[task+language],
"schema": s,
"input": text,
}, ensure_ascii=False)
sintructs.append(sintruct)
return sintructs
def predict(self, text, schema, task, language="zh", use_split=False):
sintructs = self.construct_input(text, schema, task, language, use_split)
outputs = []
for sintruct in sintructs:
input_ids = self.tokenizer.encode(sintruct, return_tensors="pt").to(self.model.device)
input_length = input_ids.size(1)
generation_output = self.model.generate(
input_ids=input_ids,
generation_config=self.generate_config,
pad_token_id=self.tokenizer.eos_token_id
)
generation_output = generation_output.sequences[0]
generation_output = generation_output[input_length:]
output = self.tokenizer.decode(generation_output, skip_special_tokens=True)
outputs.append(output)
return outputs
def processing_text_to_kg(self, text_or_path, output_path):
for chunk in read_and_process_chars(text_or_path):
text = chunk
schema = [
{
"entity_type": "食品",
"attributes": {
"名称": "食品的名称,包括品牌名、通用名称或专业化学名",
"分类": "食品所属的类型,例如水果、蔬菜、肉类、谷物、调料、添加剂、益生菌等",
"成分": "食品的主要成分,详细列出包括天然成分、添加剂、保鲜剂、营养强化剂等",
"营养价值": "食品的营养成分,概括其提供的能量和主要营养素,如蛋白质、脂肪、碳水化合物、维生素和矿物质",
"加工方式": "食品的处理或制备方法,包括日常烹饪、加工处理及实验室制备方式等",
"作用或食用效果": "食品对健康或身体的影响,可能的功效或用途"
}
}
]
task = "KG"
output = self.predict(text=text, schema=schema, task=task, language="zh")
formatted_output = parse_and_format_output(output=output, task_type=task)
with open(output_path, 'a+', encoding='utf-8') as f:
for entry in formatted_output:
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
print(f"预测结果已添加到 {output_path} 文件中。")
return output_path
def read_and_process_chars(file_path, char_size=512, overlap_size=100):
buffer = ""
with open(file_path, 'r', encoding='utf-8') as file:
while True:
chunk = file.read(char_size)
if not chunk: # 文件读取完毕
if buffer:
yield buffer
break
chunk = chunk.replace('\n', '').replace('\r', '') # 去除换行符
buffer += chunk
while len(buffer) >= char_size:
yield buffer[:char_size]
buffer = buffer[char_size - overlap_size:]
def parse_and_format_output(output, task_type):
formatted_output = []
for entry in output:
try:
# Check if the entry is a valid JSON string
if isinstance(entry, str) and entry.strip().startswith('{') and entry.strip().endswith('}'):
parsed_entry = json.loads(entry)
if task_type == "KG":
for entity_type, entities in parsed_entry.items():
for entity_name, attributes in entities.items():
for attribute_name, attribute_values in attributes.items():
if isinstance(attribute_values, list):
for value in attribute_values:
formatted_output.append({
"h": entity_name,
"t": value,
"r": attribute_name
})
else:
formatted_output.append({
"h": entity_name,
"t": attribute_values,
"r": attribute_name
})
elif task_type == "RE":
for relation_type, pairs in parsed_entry.items():
for pair in pairs:
formatted_output.append({
"h": pair["subject"],
"t": pair["object"],
"r": relation_type
})
else:
raise json.JSONDecodeError("Invalid JSON format", entry, 0)
except json.JSONDecodeError as e:
print(f"JSONDecodeError: {e} - Skipping entry")
continue
except TypeError as e:
print(f"TypeError: {e} - Skipping entry")
continue
except AttributeError as e:
print(f"AttributeError: {e} - Skipping entry")
continue
return formatted_output
if __name__ == "__main__":
oneke = OneKE()
oneke.processing_text_to_kg("asdasdadadsad", 'kg.jsonl')
file_path = ''
output_path = ''
text = ""
task = "KG"
for chunk in read_and_process_chars(file_path):
text = chunk
# schema = {
# "定义": "描述食品的起源、传统制作方法、文化象征意义或者描述食品或相关事物的定义或含义。包括食品的来源、特点及其在特定文化或背景下的意义。",
# "组成成分": "描述食品的组成部分或成分,包括主要成分、微量成分、添加剂等,揭示食品的化学或物理组成。",
# "功能": "描述食品或其成分的功能或作用,包括其对人体健康的影响、在烹饪中的用途、药用价值等。",
# "属性": "描述食品或其成分的特性或属性,揭示其独特的营养价值、口感特点、保存方式等,包括食品的营养价值、口感特点(如口感丰富、清淡等)、保存方式(如冷藏、冷冻、干燥等)",
# "种类": "描述食品或相关事物的种类或类别,揭示其分类体系、不同类型的特点及其在具体应用中的区别。"
# }
schema = [
{
"entity_type": "食品",
"attributes": {
"名称": "食品的名称,包括品牌名、通用名称或专业化学名",
"分类": "食品所属的类型,例如水果、蔬菜、肉类、谷物、调料、添加剂、益生菌等",
"成分": "食品的主要成分,详细列出包括天然成分、添加剂、保鲜剂、营养强化剂等",
"营养价值": "食品的营养成分,概括其提供的能量和主要营养素,如蛋白质、脂肪、碳水化合物、维生素和矿物质",
"加工方式": "食品的处理或制备方法,包括日常烹饪、加工处理及实验室制备方式等",
"作用或食用效果": "食品对健康或身体的影响,可能的功效或用途"
}
}
]
output = oneke.predict(text=text, schema=schema, task=task, language="zh")
formatted_output = parse_and_format_output(output=output, task_type=task)
with open(output_path, 'a+', encoding='utf-8') as f:
for entry in formatted_output:
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
print(f"预测结果已添加到 {output_path} 文件中。")

View File

@ -38,4 +38,4 @@ def get_docker_safe_url(base_url):
base_url = base_url.replace("http://localhost", "http://host.docker.internal")
base_url = base_url.replace("http://127.0.0.1", "http://host.docker.internal")
logger.info(f"Running in docker, using {base_url} as base url")
return base_url
return base_url

View File

@ -99,4 +99,4 @@ def migrate_knowledge_db():
if __name__ == "__main__":
# 当作为脚本运行时执行迁移
result = migrate_knowledge_db()
print(f"迁移{'成功' if result else '失败'}")
print(f"迁移{'成功' if result else '失败'}")

View File

@ -1,8 +1,7 @@
import asyncio
import aiohttp
import time
import json
from typing import List
async def make_request(session: aiohttp.ClientSession, request_id: int) -> dict:
"""发送单个请求到API"""
@ -17,6 +16,7 @@ async def make_request(session: aiohttp.ClientSession, request_id: int) -> dict:
try:
async with session.post(url, json=payload) as response:
result = await response.json()
print(f"请求 {request_id} 结果: {result}")
end_time = time.time()
duration = end_time - start_time
print(f"请求 {request_id} 完成时间: {time.strftime('%H:%M:%S', time.localtime(end_time))} (耗时: {duration:.2f}秒)")
@ -42,13 +42,13 @@ async def make_request(session: aiohttp.ClientSession, request_id: int) -> dict:
"error": str(e)
}
async def run_concurrent_test(num_requests: int = 10) -> List[dict]:
async def run_concurrent_test(num_requests: int = 10) -> list[dict]:
"""运行并发测试"""
async with aiohttp.ClientSession() as session:
tasks = [make_request(session, i) for i in range(num_requests)]
return await asyncio.gather(*tasks)
def analyze_results(results: List[dict]) -> None:
def analyze_results(results: list[dict]) -> None:
"""分析并打印测试结果"""
total_requests = len(results)
successful_requests = sum(1 for r in results if r["success"])
@ -62,7 +62,7 @@ def analyze_results(results: List[dict]) -> None:
else:
avg_time = max_time = min_time = 0
print(f"\n=== 并发测试结果 ===")
print("\n=== 并发测试结果 ===")
print(f"总请求数: {total_requests}")
print(f"成功请求: {successful_requests}")
print(f"失败请求: {failed_requests}")
@ -80,6 +80,9 @@ def analyze_results(results: List[dict]) -> None:
print("\n=== 请求时间线分析 ===")
sorted_results = sorted(results, key=lambda x: x["start_time"])
test_start_time = sorted_results[0]["start_time"]
test_end_time = sorted_results[-1]["end_time"]
print(f"测试开始时间: {time.strftime('%H:%M:%S', time.localtime(test_start_time))}")
print(f"测试结束时间: {time.strftime('%H:%M:%S', time.localtime(test_end_time))}")
print("\n时间线详情:")
print("请求ID 开始时间 结束时间 耗时(秒) 重叠请求数")