Merge branch 'main' of https://github.com/xerrors/Yuxi-Know into main

This commit is contained in:
Wenjie Zhang 2025-12-03 21:15:46 +08:00
commit d80bb13b7e
10 changed files with 156 additions and 69 deletions

View File

@ -15,12 +15,12 @@
- 同名文件处理逻辑:遇到同名文件则在上传区域提示,是否删除旧文件 - 同名文件处理逻辑:遇到同名文件则在上传区域提示,是否删除旧文件
- conversation 待修改为异步的版本 - conversation 待修改为异步的版本
- DBManager 需要将数据库修改为异步的aiosqlite或者异步mysql缓存使用Redis存储 - DBManager 需要将数据库修改为异步的aiosqlite或者异步mysql缓存使用Redis存储
- agent 状态中的文件区域,新增可以下载
### Bugs ### Bugs
- 部分异常状态下,智能体的模型名称出现重叠[#279](https://github.com/xerrors/Yuxi-Know/issues/279) - 部分异常状态下,智能体的模型名称出现重叠[#279](https://github.com/xerrors/Yuxi-Know/issues/279)
- DeepSeek 官方接口适配会出现问题 - DeepSeek 官方接口适配会出现问题
- 目前的知识库的图片存在公开访问风险 - 目前的知识库的图片存在公开访问风险
- 深度分析智能体需要考虑上下文超限的问题
### 新增 ### 新增
- 优化知识库详情页面,更加简洁清晰 - 优化知识库详情页面,更加简洁清晰
@ -34,6 +34,8 @@
- 新增自定义模型支持、新增 dashscope rerank/embeddings 模型的支持 - 新增自定义模型支持、新增 dashscope rerank/embeddings 模型的支持
- 新增文档解析的图片支持,已支持 MinerU Officical、Docs、Markdown Zip格式 - 新增文档解析的图片支持,已支持 MinerU Officical、Docs、Markdown Zip格式
- 新增暗色模式支持并调整整体 UI[#343](https://github.com/xerrors/Yuxi-Know/pull/343) - 新增暗色模式支持并调整整体 UI[#343](https://github.com/xerrors/Yuxi-Know/pull/343)
- agent 状态中的文件区域,新增可以下载
- 移除 Chroma 的支持,当前版本标记为移除
### 修复 ### 修复
- 修复重排序模型实际未生效的问题 - 修复重排序模型实际未生效的问题

View File

@ -87,10 +87,12 @@ async def create_database(
"""创建知识库""" """创建知识库"""
logger.debug( logger.debug(
f"Create database {database_name} with kb_type {kb_type}, " f"Create database {database_name} with kb_type {kb_type}, "
f"additional_params {additional_params}, llm_info {llm_info}" f"additional_params {additional_params}, llm_info {llm_info}, "
f"embed_model_name {embed_model_name}"
) )
try: try:
additional_params = {**(additional_params or {})} additional_params = {**(additional_params or {})}
additional_params["auto_generate_questions"] = False # 默认不生成问题
def normalize_reranker_config(kb: str, params: dict) -> None: def normalize_reranker_config(kb: str, params: dict) -> None:
reranker_cfg = params.get("reranker_config") reranker_cfg = params.get("reranker_config")
@ -112,12 +114,12 @@ async def create_database(
if not isinstance(reranker_cfg, Mapping): if not isinstance(reranker_cfg, Mapping):
raise HTTPException(status_code=400, detail="reranker_config must be an object") raise HTTPException(status_code=400, detail="reranker_config must be an object")
enabled = bool(reranker_cfg.get("enabled", False)) reranker_enabled = bool(reranker_cfg.get("enabled", False))
model = (reranker_cfg.get("model") or "").strip() model = (reranker_cfg.get("model") or "").strip()
recall_top_k = max(1, int(reranker_cfg.get("recall_top_k", 50))) recall_top_k = max(1, int(reranker_cfg.get("recall_top_k", 50)))
final_top_k = max(1, int(reranker_cfg.get("final_top_k", 10))) final_top_k = max(1, int(reranker_cfg.get("final_top_k", 10)))
if enabled: if reranker_enabled:
if not model: if not model:
raise HTTPException(status_code=400, detail="reranker_config.model is required when enabled") raise HTTPException(status_code=400, detail="reranker_config.model is required when enabled")
if model not in config.reranker_names: if model not in config.reranker_names:
@ -132,7 +134,7 @@ async def create_database(
model = model if model in config.reranker_names else "" model = model if model in config.reranker_names else ""
params["reranker_config"] = { params["reranker_config"] = {
"enabled": enabled, "enabled": reranker_enabled,
"model": model, "model": model,
"recall_top_k": recall_top_k, "recall_top_k": recall_top_k,
"final_top_k": final_top_k, "final_top_k": final_top_k,

@ -0,0 +1 @@
Subproject commit 057e2000be7b56823239815b0fe7c7fc0dbced96

@ -0,0 +1 @@
Subproject commit 6a0367834ea0fb5e5c94b9711e3e2756966789ea

View File

@ -29,7 +29,7 @@ class EmbedModelInfo(BaseModel):
dimension: int = Field(..., description="向量维度") dimension: int = Field(..., description="向量维度")
base_url: str = Field(..., description="API 基础 URL") base_url: str = Field(..., description="API 基础 URL")
api_key: str = Field(..., description="API Key 或环境变量名") api_key: str = Field(..., description="API Key 或环境变量名")
model_id: str | None = Field(None, description="可选的模型 ID")
class RerankerInfo(BaseModel): class RerankerInfo(BaseModel):
"""重排序模型配置""" """重排序模型配置"""
@ -158,42 +158,49 @@ DEFAULT_CHAT_MODEL_PROVIDERS: dict[str, ChatModelProvider] = {
DEFAULT_EMBED_MODELS: dict[str, EmbedModelInfo] = { DEFAULT_EMBED_MODELS: dict[str, EmbedModelInfo] = {
"siliconflow/BAAI/bge-m3": EmbedModelInfo( "siliconflow/BAAI/bge-m3": EmbedModelInfo(
model_id="siliconflow/BAAI/bge-m3",
name="BAAI/bge-m3", name="BAAI/bge-m3",
dimension=1024, dimension=1024,
base_url="https://api.siliconflow.cn/v1/embeddings", base_url="https://api.siliconflow.cn/v1/embeddings",
api_key="SILICONFLOW_API_KEY", api_key="SILICONFLOW_API_KEY",
), ),
"siliconflow/Pro/BAAI/bge-m3": EmbedModelInfo( "siliconflow/Pro/BAAI/bge-m3": EmbedModelInfo(
model_id="siliconflow/Pro/BAAI/bge-m3",
name="Pro/BAAI/bge-m3", name="Pro/BAAI/bge-m3",
dimension=1024, dimension=1024,
base_url="https://api.siliconflow.cn/v1/embeddings", base_url="https://api.siliconflow.cn/v1/embeddings",
api_key="SILICONFLOW_API_KEY", api_key="SILICONFLOW_API_KEY",
), ),
"siliconflow/Qwen/Qwen3-Embedding-0.6B": EmbedModelInfo( "siliconflow/Qwen/Qwen3-Embedding-0.6B": EmbedModelInfo(
model_id="siliconflow/Qwen/Qwen3-Embedding-0.6B",
name="Qwen/Qwen3-Embedding-0.6B", name="Qwen/Qwen3-Embedding-0.6B",
dimension=1024, dimension=1024,
base_url="https://api.siliconflow.cn/v1/embeddings", base_url="https://api.siliconflow.cn/v1/embeddings",
api_key="SILICONFLOW_API_KEY", api_key="SILICONFLOW_API_KEY",
), ),
"vllm/Qwen/Qwen3-Embedding-0.6B": EmbedModelInfo( "vllm/Qwen/Qwen3-Embedding-0.6B": EmbedModelInfo(
model_id="vllm/Qwen/Qwen3-Embedding-0.6B",
name="Qwen3-Embedding-0.6B", name="Qwen3-Embedding-0.6B",
dimension=1024, dimension=1024,
base_url="http://localhost:8000/v1/embeddings", base_url="http://localhost:8000/v1/embeddings",
api_key="no_api_key", api_key="no_api_key",
), ),
"ollama/nomic-embed-text": EmbedModelInfo( "ollama/nomic-embed-text": EmbedModelInfo(
model_id="ollama/nomic-embed-text",
name="nomic-embed-text", name="nomic-embed-text",
dimension=768, dimension=768,
base_url="http://localhost:11434/api/embed", base_url="http://localhost:11434/api/embed",
api_key="no_api_key", api_key="no_api_key",
), ),
"ollama/bge-m3": EmbedModelInfo( "ollama/bge-m3": EmbedModelInfo(
model_id="ollama/bge-m3",
name="bge-m3", name="bge-m3",
dimension=1024, dimension=1024,
base_url="http://localhost:11434/api/embed", base_url="http://localhost:11434/api/embed",
api_key="no_api_key", api_key="no_api_key",
), ),
"dashscope/text-embedding-v4": EmbedModelInfo( "dashscope/text-embedding-v4": EmbedModelInfo(
model_id="dashscope/text-embedding-v4",
name="text-embedding-v4", name="text-embedding-v4",
dimension=1024, dimension=1024,
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings",

View File

@ -7,6 +7,7 @@ from typing import Any
from pymilvus import Collection, CollectionSchema, DataType, FieldSchema, connections, db, utility from pymilvus import Collection, CollectionSchema, DataType, FieldSchema, connections, db, utility
from src import config
from src.knowledge.base import KnowledgeBase from src.knowledge.base import KnowledgeBase
from src.knowledge.indexing import process_file_to_markdown from src.knowledge.indexing import process_file_to_markdown
from src.knowledge.utils.kb_utils import ( from src.knowledge.utils.kb_utils import (
@ -91,10 +92,14 @@ class MilvusKB(KnowledgeBase):
"""创建 Milvus 集合""" """创建 Milvus 集合"""
logger.info(f"Creating Milvus collection for {db_id}") logger.info(f"Creating Milvus collection for {db_id}")
if db_id not in self.databases_meta: if not (metadata := self.databases_meta.get(db_id)):
raise ValueError(f"Database {db_id} not found") raise ValueError(f"Database {db_id} not found")
embed_info = self.databases_meta[db_id].get("embed_info", {}) # embed_info = metadata.get("embed_info", {})
if not (embed_info := metadata.get("embed_info")):
logger.error(f"Embedding info not found for database {db_id}, using default model")
embed_info = config.embed_model_names[config.embed_model]
collection_name = db_id collection_name = db_id
try: try:
@ -117,8 +122,8 @@ class MilvusKB(KnowledgeBase):
except Exception: except Exception:
# 创建新集合 # 创建新集合
embedding_dim = getattr(embed_info, "dimension", 1024) if embed_info else 1024 embedding_dim = embed_info.get("dimension", 1024)
model_name = getattr(embed_info, "name", "default") if embed_info else "default" model_name = embed_info.get("name", "default")
# 定义集合Schema # 定义集合Schema
fields = [ fields = [
@ -142,7 +147,7 @@ class MilvusKB(KnowledgeBase):
index_params = {"metric_type": "COSINE", "index_type": "IVF_FLAT", "params": {"nlist": 1024}} index_params = {"metric_type": "COSINE", "index_type": "IVF_FLAT", "params": {"nlist": 1024}}
collection.create_index("embedding", index_params) collection.create_index("embedding", index_params)
logger.info(f"Created new Milvus collection: {collection_name}") logger.info(f"Created new Milvus collection: {collection_name}: {model_name=}, {embedding_dim=}")
return collection return collection
@ -154,25 +159,29 @@ class MilvusKB(KnowledgeBase):
except Exception as e: except Exception as e:
logger.warning(f"Failed to load collection into memory: {e}") logger.warning(f"Failed to load collection into memory: {e}")
def _get_async_embedding_function(self, embed_info: dict): def _get_async_embedding(self, embed_info: dict):
"""获取 embedding 函数""" """获取 embedding 函数"""
# 检查是否有 model_id 字段,优先使用 select_embedding_model
if embed_info and "model_id" in embed_info:
from src.models.embed import select_embedding_model
return select_embedding_model(embed_info["model_id"])
# 使用原有的逻辑(兼容模式))
config_dict = get_embedding_config(embed_info) config_dict = get_embedding_config(embed_info)
embedding_model = OtherEmbedding( return OtherEmbedding(
model=config_dict.get("model"), model=config_dict.get("model"),
base_url=config_dict.get("base_url"), base_url=config_dict.get("base_url"),
api_key=config_dict.get("api_key"), api_key=config_dict.get("api_key"),
) )
def _get_async_embedding_function(self, embed_info: dict):
"""获取 embedding 函数"""
embedding_model = self._get_async_embedding(embed_info)
return partial(embedding_model.abatch_encode, batch_size=40) return partial(embedding_model.abatch_encode, batch_size=40)
def _get_embedding_function(self, embed_info: dict): def _get_embedding_function(self, embed_info: dict):
"""获取 embedding 函数""" """获取 embedding 函数"""
config_dict = get_embedding_config(embed_info) embedding_model = self._get_async_embedding(embed_info)
embedding_model = OtherEmbedding(
model=config_dict.get("model"),
base_url=config_dict.get("base_url"),
api_key=config_dict.get("api_key"),
)
return partial(embedding_model.batch_encode, batch_size=40) return partial(embedding_model.batch_encode, batch_size=40)

View File

@ -246,16 +246,12 @@ class KnowledgeBaseManager:
db_id = db_info["db_id"] db_id = db_info["db_id"]
async with self._metadata_lock: async with self._metadata_lock:
# 准备 additional_params包含 auto_generate_questions
saved_params = kwargs.copy()
saved_params["auto_generate_questions"] = False
self.global_databases_meta[db_id] = { self.global_databases_meta[db_id] = {
"name": database_name, "name": database_name,
"description": description, "description": description,
"kb_type": kb_type, "kb_type": kb_type,
"created_at": utc_isoformat(), "created_at": utc_isoformat(),
"additional_params": saved_params, "additional_params": kwargs.copy(),
} }
self._save_global_metadata() self._save_global_metadata()

View File

@ -247,15 +247,23 @@ def get_embedding_config(embed_info: dict) -> dict:
try: try:
if embed_info: if embed_info:
# 处理 embed_info 可能是字典或 EmbedModelInfo 对象的情况 # 优先检查是否有 model_id 字段
if hasattr(embed_info, "name"): if "model_id" in embed_info:
from src.models.embed import select_embedding_model
model = select_embedding_model(embed_info["model_id"])
config_dict["model"] = model.model
config_dict["api_key"] = model.api_key
config_dict["base_url"] = model.base_url
config_dict["dimension"] = getattr(model, "dimension", 1024)
elif hasattr(embed_info, "name"):
# EmbedModelInfo 对象 # EmbedModelInfo 对象
config_dict["model"] = embed_info.name config_dict["model"] = embed_info.name
config_dict["api_key"] = os.getenv(embed_info.api_key) or embed_info.api_key config_dict["api_key"] = os.getenv(embed_info.api_key) or embed_info.api_key
config_dict["base_url"] = embed_info.base_url config_dict["base_url"] = embed_info.base_url
config_dict["dimension"] = embed_info.dimension config_dict["dimension"] = embed_info.dimension
else: else:
# 字典形式 # 字典形式(保持向后兼容)
config_dict["model"] = embed_info["name"] config_dict["model"] = embed_info["name"]
config_dict["api_key"] = os.getenv(embed_info["api_key"]) or embed_info["api_key"] config_dict["api_key"] = os.getenv(embed_info["api_key"]) or embed_info["api_key"]
config_dict["base_url"] = embed_info["base_url"] config_dict["base_url"] = embed_info["base_url"]

View File

@ -11,7 +11,7 @@ from src.utils import get_docker_safe_url, hashstr, logger
class BaseEmbeddingModel(ABC): class BaseEmbeddingModel(ABC):
def __init__(self, model=None, name=None, dimension=None, url=None, base_url=None, api_key=None): def __init__(self, model=None, name=None, dimension=None, url=None, base_url=None, api_key=None, model_id=None):
""" """
Args: Args:
model: 模型名称冗余设计同name model: 模型名称冗余设计同name
@ -140,6 +140,7 @@ class OllamaEmbedding(BaseEmbeddingModel):
payload = {"model": self.model, "input": message} payload = {"model": self.model, "input": message}
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
try: try:
print(f"\n\n\nOllama Embedding request: {payload}\n\n\n")
response = await client.post(self.base_url, json=payload, timeout=60) response = await client.post(self.base_url, json=payload, timeout=60)
response.raise_for_status() response.raise_for_status()
result = response.json() result = response.json()

View File

@ -14,15 +14,23 @@
<h3>知识库类型<span style="color: var(--color-error-500)">*</span></h3> <h3>知识库类型<span style="color: var(--color-error-500)">*</span></h3>
<div class="kb-type-cards"> <div class="kb-type-cards">
<div <div
v-for="(typeInfo, typeKey) in supportedKbTypes" v-for="(typeInfo, typeKey) in orderedKbTypes"
:key="typeKey" :key="typeKey"
class="kb-type-card" class="kb-type-card"
:class="{ active: newDatabase.kb_type === typeKey }" :class="{ active: newDatabase.kb_type === typeKey }"
:data-type="typeKey"
@click="handleKbTypeChange(typeKey)" @click="handleKbTypeChange(typeKey)"
> >
<div class="card-header"> <div class="card-header">
<component :is="getKbTypeIcon(typeKey)" class="type-icon" /> <component :is="getKbTypeIcon(typeKey)" class="type-icon" />
<span class="type-title">{{ getKbTypeLabel(typeKey) }}</span> <span class="type-title">{{ getKbTypeLabel(typeKey) }}</span>
<a-tooltip
v-if="typeKey === 'chroma'"
title="Chroma 已标记为弃用状态,建议使用 Milvus 替代。同时会在下个正式版本中移除。"
placement="top"
>
<span class="deprecated-badge">弃用</span>
</a-tooltip>
</div> </div>
<div class="card-description">{{ typeInfo.description }}</div> <div class="card-description">{{ typeInfo.description }}</div>
</div> </div>
@ -71,7 +79,7 @@
<a-textarea <a-textarea
v-model:value="newDatabase.description" v-model:value="newDatabase.description"
placeholder="新建知识库描述" placeholder="新建知识库描述"
:auto-size="{ minRows: 5, maxRows: 10 }" :auto-size="{ minRows: 3, maxRows: 10 }"
/> />
<h3 style="margin-top: 20px;">隐私设置</h3> <h3 style="margin-top: 20px;">隐私设置</h3>
@ -158,6 +166,18 @@
<p>正在加载知识库...</p> <p>正在加载知识库...</p>
</div> </div>
<!-- 空状态显示 -->
<div v-else-if="!databases || databases.length === 0" class="empty-state">
<h3 class="empty-title">暂无知识库</h3>
<p class="empty-description">创建您的第一个知识库开始管理文档和知识</p>
<a-button type="primary" size="large" @click="state.openNewDatabaseModel = true">
<template #icon>
<PlusOutlined />
</template>
创建知识库
</a-button>
</div>
<!-- 数据库列表 --> <!-- 数据库列表 -->
<div v-else class="databases"> <div v-else class="databases">
<div <div
@ -213,7 +233,7 @@ import { useRouter, useRoute } from 'vue-router';
import { useConfigStore } from '@/stores/config'; import { useConfigStore } from '@/stores/config';
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { Database, Zap, FileDigit, Waypoints, Building2 } from 'lucide-vue-next'; import { Database, Zap, FileDigit, Waypoints, Building2 } from 'lucide-vue-next';
import { LockOutlined, InfoCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons-vue'; import { LockOutlined, InfoCircleOutlined, QuestionCircleOutlined, PlusOutlined } from '@ant-design/icons-vue';
import { databaseApi, typeApi } from '@/apis/knowledge_api'; import { databaseApi, typeApi } from '@/apis/knowledge_api';
import HeaderComponent from '@/components/HeaderComponent.vue'; import HeaderComponent from '@/components/HeaderComponent.vue';
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue'; import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue';
@ -256,7 +276,7 @@ const createEmptyDatabaseForm = () => ({
name: '', name: '',
description: '', description: '',
embed_model_name: configStore.config?.embed_model, embed_model_name: configStore.config?.embed_model,
kb_type: 'chroma', kb_type: 'milvus',
is_private: false, is_private: false,
storage: '', storage: '',
language: 'English', language: 'English',
@ -295,6 +315,27 @@ const llmModelSpec = computed(() => {
// //
const supportedKbTypes = ref({}) const supportedKbTypes = ref({})
// Chroma
const orderedKbTypes = computed(() => {
const types = { ...supportedKbTypes.value }
const ordered = {}
const chromaData = types.chroma
// Chroma
Object.keys(types).forEach(key => {
if (key !== 'chroma') {
ordered[key] = types[key]
}
})
// Chroma
if (chromaData) {
ordered.chroma = chromaData
}
return ordered
})
// //
const loadSupportedKbTypes = async () => { const loadSupportedKbTypes = async () => {
try { try {
@ -368,24 +409,6 @@ const getKbTypeIcon = (type) => {
return icons[type] || Database return icons[type] || Database
} }
// const getKbTypeDescription = (type) => {
// const descriptions = {
// lightrag: '🔥 ',
// chroma: ' ',
// milvus: '🚀 '
// }
// return descriptions[type] || ''
// }
const getKbTypeAlertType = (type) => {
const types = {
lightrag: 'info',
chroma: 'success',
milvus: 'warning'
}
return types[type] || 'info'
}
const getKbTypeColor = (type) => { const getKbTypeColor = (type) => {
const colors = { const colors = {
lightrag: 'purple', lightrag: 'purple',
@ -395,7 +418,6 @@ const getKbTypeColor = (type) => {
return colors[type] || 'blue' return colors[type] || 'blue'
} }
// //
const formatCreatedTime = (createdAt) => { const formatCreatedTime = (createdAt) => {
if (!createdAt) return '' if (!createdAt) return ''
@ -680,24 +702,12 @@ onMounted(() => {
border-color: var(--main-color); border-color: var(--main-color);
} }
// &.active {
&:nth-child(1):hover, border-color: var(--main-color);
&:nth-child(1).active { background: var(--main-10);
border-color: var(--color-accent-100); .type-icon { color: var(--main-color); }
.type-icon { color: var(--color-accent-500); }
} }
&:nth-child(2):hover,
&:nth-child(2).active {
border-color: var(--color-warning-100);
.type-icon { color: var(--color-warning-500); }
}
&:nth-child(3):hover,
&:nth-child(3).active {
border-color: var(--color-error-100);
.type-icon { color: var(--color-error-500); }
}
.card-header { .card-header {
display: flex; display: flex;
align-items: center; align-items: center;
@ -725,6 +735,26 @@ onMounted(() => {
margin-bottom: 0; margin-bottom: 0;
// min-height: 40px; // min-height: 40px;
} }
.deprecated-badge {
background: var(--color-error-100);
color: var(--color-error-600);
font-size: 10px;
font-weight: 600;
padding: 2px 6px;
border-radius: 4px;
margin-left: auto;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: help;
transition: all 0.2s ease;
&:hover {
background: var(--color-error-200);
color: var(--color-error-700);
}
}
} }
} }
@ -900,8 +930,6 @@ onMounted(() => {
font-weight: 400; font-weight: 400;
flex: 1; flex: 1;
} }
} }
.database-empty { .database-empty {
@ -913,6 +941,38 @@ onMounted(() => {
color: var(--gray-900); color: var(--gray-900);
} }
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100px 20px;
text-align: center;
.empty-title {
font-size: 20px;
font-weight: 600;
color: var(--gray-900);
margin: 0 0 12px 0;
letter-spacing: -0.02em;
}
.empty-description {
font-size: 14px;
color: var(--gray-600);
margin: 0 0 32px 0;
line-height: 1.5;
max-width: 320px;
}
.ant-btn {
height: 44px;
padding: 0 24px;
font-size: 15px;
font-weight: 500;
}
}
.database-container { .database-container {
padding: 0; padding: 0;
} }