Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 2 additions & 11 deletions backend/package/yuxi/agents/backends/knowledge_base_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,14 @@


async def resolve_visible_knowledge_bases_for_context(context) -> list[dict[str, Any]]:
from yuxi.knowledge.runtime import knowledge_base
from yuxi.services.knowledge.tools import visible_knowledge_bases

uid = getattr(context, "uid", None)
if not uid:
setattr(context, "_visible_knowledge_bases", [])
return []

summaries = await knowledge_base.get_databases_by_uid(str(uid))
databases = [
{
"kb_id": summary.kb_id,
"name": summary.name,
"description": summary.description,
"kb_type": summary.kb_type,
}
for summary in summaries
]
databases = await visible_knowledge_bases(str(uid))
enabled_knowledges = getattr(context, "knowledges", None)
if enabled_knowledges is not None:
enabled_ids = {str(value).strip() for value in enabled_knowledges if str(value).strip()}
Expand Down
165 changes: 46 additions & 119 deletions backend/package/yuxi/agents/toolkits/kbs/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
OpenInputSchema,
SearchInputSchema,
)
from yuxi.services.knowledge import tools as knowledge_tools
from yuxi.utils import logger

# ========== 通用知识库工具 ==========
Expand Down Expand Up @@ -71,15 +72,7 @@ async def list_kbs(dummy: str, runtime: ToolRuntime) -> str:
if not available_kbs:
return "当前没有可访问的知识库"

# 格式化输出(包含名称和描述)
return [
{
"kb_id": kb.get("kb_id"),
"name": kb.get("name", ""),
"description": kb.get("description") or "无描述",
}
for kb in available_kbs
]
return knowledge_tools.list_kbs(available_kbs)


class GetMindmapInput(BaseModel):
Expand All @@ -101,43 +94,11 @@ async def get_mindmap(kb_name: str, runtime: ToolRuntime) -> str:
Returns:
知识库的思维导图结构(文本格式)
"""
if not kb_name:
return "请提供知识库名称"

visible_kbs = await _resolve_visible_knowledge_bases_for_query(runtime)
target_info = next((kb for kb in visible_kbs if kb.get("name") == kb_name), None)
if not target_info:
return f"知识库 '{kb_name}' 不存在或当前会话未启用"
target_kb_id = target_info["kb_id"]

try:
from yuxi.repositories.knowledge_base_repository import KnowledgeBaseRepository

kb_repo = KnowledgeBaseRepository()
kb = await kb_repo.get_by_kb_id(target_kb_id)

if kb is None:
return f"知识库 {target_info['name']} 不存在"

mindmap_data = kb.mindmap

if not mindmap_data:
return f"知识库 {target_info['name']} 还没有生成思维导图。"

# 将思维导图数据转换为文本格式
def mindmap_to_text(node, level=0):
"""递归将思维导图JSON转换为层级文本"""
indent = " " * level
text = f"{indent}- {node.get('content', '')}\n"
for child in node.get("children", []):
text += mindmap_to_text(child, level + 1)
return text

mindmap_text = f"知识库 {target_info['name']} 的思维导图结构:\n\n"
mindmap_text += mindmap_to_text(mindmap_data)

return mindmap_text

return await knowledge_tools.get_mindmap(kb_name, visible_kbs)
except knowledge_tools.KnowledgeToolError as e:
return str(e)
except Exception as e:
logger.error(f"获取思维导图失败: {e}")
return f"获取思维导图失败: {str(e)}"
Expand All @@ -153,19 +114,17 @@ async def query_kb(kb_id: str, query_text: str, file_name: str | None = None, ru
当用户需要查询具体内容时使用此工具。kb_id 是知识库资源 ID,也就是 kb_id;返回结果中的
file_id 可继续用于 find_kb_document 或 open_kb_document。
"""
if not kb_id:
return "请提供 kb_id"
if not query_text:
return "请提供查询内容"

visible_kbs = await _resolve_visible_knowledge_bases_for_query(runtime)
target_kb_id, target_error = _find_query_target(kb_id=kb_id, visible_kbs=visible_kbs)
if target_error:
return target_error

try:
kwargs = {"file_name": file_name} if file_name else {}
return await _get_knowledge_base().retrieve(target_kb_id, query_text, **kwargs)
return await knowledge_tools.query_kb(
kb_id,
query_text,
visible_kbs,
file_name=file_name,
kb_service=_get_knowledge_base(),
)
except knowledge_tools.KnowledgeToolError as e:
return str(e)
except Exception as e:
logger.error(f"检索失败: {e}")
return f"检索失败: {str(e)}"
Expand All @@ -188,26 +147,19 @@ async def open_kb_document(
当 query_kb 返回的片段不足以回答问题,或需要查看某个文档的上下文时使用。
kb_id 是知识库资源 ID,也就是 kb_id;file_id 是知识库文件 ID。
"""
normalized_kb_id = str(kb_id or "").strip()
normalized_file_id = str(file_id or "").strip()
if not normalized_kb_id:
return "请提供 kb_id"
if not normalized_file_id:
return "请提供 file_id"

visible_kbs = await _resolve_visible_knowledge_bases_for_query(runtime)
target_kb_id, target_error = _find_query_target(kb_id=normalized_kb_id, visible_kbs=visible_kbs)
if target_error:
return target_error

try:
start_offset = int(line) - 1 if line is not None else int(offset or 0)
return await _get_knowledge_base().open_document(
target_kb_id,
normalized_file_id,
offset=start_offset,
limit=window_size,
return await knowledge_tools.open_kb_document(
kb_id,
file_id,
visible_kbs,
line=line,
offset=offset,
window_size=window_size,
kb_service=_get_knowledge_base(),
)
except knowledge_tools.KnowledgeToolError as e:
return str(e)
except Exception as e:
logger.error(f"打开知识库文档失败: {e}")
return f"打开知识库文档失败: {str(e)}"
Expand All @@ -231,30 +183,21 @@ async def find_kb_document(

当 query_kb 已找到候选文件,但需要在该文件内定位术语、指标、章节或实体时使用。
"""
normalized_kb_id = str(kb_id or "").strip()
normalized_file_id = str(file_id or "").strip()
if not normalized_kb_id:
return "请提供 kb_id"
if not normalized_file_id:
return "请提供 file_id"
if not patterns:
return "请提供 patterns"

visible_kbs = await _resolve_visible_knowledge_bases_for_query(runtime)
target_kb_id, target_error = _find_query_target(kb_id=normalized_kb_id, visible_kbs=visible_kbs)
if target_error:
return target_error

try:
return await _get_knowledge_base().find_in_document(
target_kb_id,
normalized_file_id,
return await knowledge_tools.find_kb_document(
kb_id,
file_id,
patterns,
visible_kbs,
use_regex=use_regex,
case_sensitive=case_sensitive,
max_windows=max_windows,
window_size=window_size,
kb_service=_get_knowledge_base(),
)
except knowledge_tools.KnowledgeToolError as e:
return str(e)
except Exception as e:
logger.error(f"知识库文档内检索失败: {e}")
return f"知识库文档内检索失败: {str(e)}"
Expand Down Expand Up @@ -292,30 +235,18 @@ async def search_file(
Returns:
匹配的文件列表和分页信息
"""
if not kb_name and not query:
return "请提供知识库名称或搜索关键词,不能同时为空"

visible_kbs = await _resolve_visible_knowledge_bases_for_query(runtime)
if not visible_kbs:
return "无法获取当前会话可访问的知识库"

if kb_name:
target_kbs = [kb for kb in visible_kbs if kb.get("name") == kb_name]
if not target_kbs:
return f"知识库 '{kb_name}' 不存在或当前会话未启用"
else:
target_kbs = visible_kbs

knowledge_base = _get_knowledge_base()
searchable_kbs = [kb for kb in target_kbs if knowledge_base.database_type_supports_documents(kb.get("kb_type"))]
if not searchable_kbs:
return "当前匹配的知识库只支持检索,不支持文件搜索"
return await knowledge_base.search_document_files(
searchable_kbs,
query=query,
offset=offset,
limit=limit,
)
try:
return await knowledge_tools.search_file(
visible_kbs,
kb_name=kb_name,
query=query,
offset=offset,
limit=limit,
kb_service=_get_knowledge_base(),
)
except knowledge_tools.KnowledgeToolError as e:
return str(e)


class DownloadKBFileInput(BaseModel):
Expand Down Expand Up @@ -439,14 +370,10 @@ def _find_query_target(
visible_kbs: list[dict[str, Any]],
) -> tuple[str | None, str | None]:
"""校验 kb_id 在当前会话可见知识库内,返回 (kb_id, error)。"""
if not visible_kbs:
return None, "无法获取当前会话可访问的知识库"

normalized_kb_id = str(kb_id or "").strip()
visible_kb_ids = {str(kb.get("kb_id") or "").strip() for kb in visible_kbs}
if normalized_kb_id not in visible_kb_ids:
return None, f"知识库资源 '{normalized_kb_id}' 不存在或当前会话未启用"
return normalized_kb_id, None
try:
return knowledge_tools.require_visible_kb(kb_id, visible_kbs), None
except knowledge_tools.KnowledgeToolError as exc:
return None, str(exc)


def _runtime_sandbox_scope(runtime: ToolRuntime | None) -> tuple[str, str, str, str] | None:
Expand Down
17 changes: 10 additions & 7 deletions backend/package/yuxi/knowledge/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,10 @@ def _build_find_file_windows(
lines = content.splitlines()
flags = 0 if case_sensitive else re.IGNORECASE
if use_regex:
matchers = [re.compile(pattern, flags) for pattern in patterns]
try:
matchers = [re.compile(pattern, flags) for pattern in patterns]
except re.error as exc:
raise ValueError(f"无效正则表达式: {exc}") from exc

def line_matches(line: str) -> bool:
return any(matcher.search(line) for matcher in matchers)
Expand Down Expand Up @@ -716,13 +719,13 @@ async def open_file_content(self, kb_id: str, file_id: str, offset: int = 0, lim
try:
file_meta = await self._load_file_meta(kb_id, file_id)
except ValueError as exc:
raise Exception(f"文件不存在: {file_id}") from exc
raise ValueError(f"文件不存在: {file_id}") from exc
if file_meta.get("is_folder"):
raise Exception(f"文件 {file_id} 是文件夹")
raise ValueError(f"文件 {file_id} 是文件夹")

markdown_file = file_meta.get("markdown_file")
if not markdown_file:
raise Exception(f"文件 {file_id} 没有解析后的 Markdown 内容")
raise ValueError(f"文件 {file_id} 没有解析后的 Markdown 内容")

content = await self._read_markdown_from_minio(markdown_file)
return self._build_open_file_window(content, offset=offset, limit=limit)
Expand All @@ -741,13 +744,13 @@ async def find_file_content(
try:
file_meta = await self._load_file_meta(kb_id, file_id)
except ValueError as exc:
raise Exception(f"文件不存在: {file_id}") from exc
raise ValueError(f"文件不存在: {file_id}") from exc
if file_meta.get("is_folder"):
raise Exception(f"文件 {file_id} 是文件夹")
raise ValueError(f"文件 {file_id} 是文件夹")

markdown_file = file_meta.get("markdown_file")
if not markdown_file:
raise Exception(f"文件 {file_id} 没有解析后的 Markdown 内容")
raise ValueError(f"文件 {file_id} 没有解析后的 Markdown 内容")

content = await self._read_markdown_from_minio(markdown_file)
return self._build_find_file_windows(
Expand Down
31 changes: 26 additions & 5 deletions backend/package/yuxi/repositories/agent_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,23 +153,29 @@ async def ensure_preset(self, preset: AgentPreset, *, created_by: str | None = N

async def list_visible(self, *, user: User, include_subagent_definitions: bool = False) -> list[Agent]:
"""列出用户可见的主智能体,只有显式请求时才包含子智能体定义。"""
visibility_user = await self._visibility_user(user)
if visibility_user is None:
return []
stmt = select(Agent)
if not include_subagent_definitions:
stmt = stmt.where(Agent.is_subagent.is_(False))
result = await self.db.execute(stmt.order_by(Agent.is_default.desc(), Agent.id.asc()))
agents = list(result.scalars().all())
if user.role == "superadmin":
if visibility_user.role == "superadmin":
return agents
return [agent for agent in agents if user_can_access_agent(user, agent)]
return [agent for agent in agents if user_can_access_agent(visibility_user, agent)]

async def list_visible_subagents(self, *, user: User) -> list[Agent]:
visibility_user = await self._visibility_user(user)
if visibility_user is None:
return []
result = await self.db.execute(
select(Agent).where(Agent.is_subagent.is_(True)).order_by(Agent.name.asc(), Agent.id.asc())
)
agents = list(result.scalars().all())
if user.role == "superadmin":
if visibility_user.role == "superadmin":
return agents
return [agent for agent in agents if user_can_access_agent(user, agent)]
return [agent for agent in agents if user_can_access_agent(visibility_user, agent)]

async def get_by_slug(self, slug: str) -> Agent | None:
result = await self.db.execute(select(Agent).where(Agent.slug == slug))
Expand All @@ -183,10 +189,13 @@ async def get_visible_by_slug(
self, *, slug: str, user: User, kind: Literal["main", "subagent", "any"] = "main"
) -> Agent | None:
"""按 slug 读取用户可见智能体,并按入口语义过滤主/子智能体。"""
visibility_user = await self._visibility_user(user)
if visibility_user is None:
return None
agent = await self.get_by_slug(slug)
if not agent:
return None
if not user_can_access_agent(user, agent):
if not user_can_access_agent(visibility_user, agent):
return None
if kind == "any":
return agent
Expand All @@ -196,6 +205,18 @@ async def get_visible_by_slug(
return agent if agent.is_subagent else None
raise ValueError(f"未知智能体入口类型: {kind}")

async def _visibility_user(self, user: User) -> User | None:
"""终端用户只借用 Key 用户的 Agent 可见性,执行 UID 保持不变。"""
if getattr(user, "user_kind", "human") != "end_user":
return user
return await self.db.scalar(
select(User).where(
User.id == user.owner_user_id,
User.user_kind == "human",
User.is_deleted == 0,
)
)

async def get_default(self) -> Agent | None:
result = await self.db.execute(select(Agent).where(Agent.is_default.is_(True)))
return result.scalar_one_or_none()
Expand Down
Loading
Loading