""" 知识库管理器 - 基础模块 包含: - 配置常量 - 数据类定义 - 辅助函数 - BM25Index 类 """ import os import json import pickle import logging from typing import List, Dict, Optional, Tuple from dataclasses import dataclass from pathlib import Path import numpy as np from rank_bm25 import BM25Okapi import jieba # 设置日志 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # ==================== 配置常量 ==================== # 向量存储基础路径(位于 knowledge/vector_store/) VECTOR_STORE_BASE_PATH = os.path.join( os.path.dirname(os.path.abspath(__file__)), "vector_store" ) # 向量库基础路径(ChromaDB 数据存储) CHROMA_DB_BASE_PATH = os.path.join(VECTOR_STORE_BASE_PATH, "chroma") # BM25 索引基础路径 BM25_INDEX_BASE_PATH = os.path.join(VECTOR_STORE_BASE_PATH, "bm25") # 向量库元数据文件 KB_METADATA_FILE = "kb_metadata.json" # 预定义的公开知识库名称 PUBLIC_KB_NAME = "public_kb" # 默认部门列表 DEFAULT_DEPARTMENTS = ["finance", "hr", "tech", "operation", "marketing"] # 部门名称映射 DEPARTMENT_NAME_MAP = { "财务部": "finance", "财务": "finance", "人事部": "hr", "人事": "hr", "人力资源部": "hr", "人力资源": "hr", "技术部": "tech", "技术": "tech", "研发部": "tech", "研发": "tech", "运营部": "operation", "运营": "operation", "市场部": "marketing", "市场": "marketing", "法务部": "legal", "法务": "legal", "行政部": "admin", "行政": "admin", "finance": "finance", "hr": "hr", "tech": "tech", "operation": "operation", "marketing": "marketing", "legal": "legal", "admin": "admin", } # ==================== 数据结构 ==================== @dataclass class CollectionInfo: """向量库信息""" name: str display_name: str document_count: int = 0 created_at: str = "" department: str = "" description: str = "" @dataclass class SearchResult: """检索结果""" ids: List[str] documents: List[str] metadatas: List[dict] distances: List[float] collection_name: str = "" # ==================== 辅助函数 ==================== def _get_doc_type(filename: str) -> str: """ 根据文件扩展名判断文档类型 Args: filename: 文件名,包含扩展名 Returns: 文档类型字符串: 'pdf' | 'word' | 'excel' | 'ppt' | 'other' Example: >>> _get_doc_type("report.pdf") 'pdf' >>> _get_doc_type("data.xlsx") 'excel' """ ext = Path(filename).suffix.lower() type_map = { '.pdf': 'pdf', '.docx': 'word', '.doc': 'word', '.xlsx': 'excel', '.xls': 'excel', '.pptx': 'ppt', '.ppt': 'ppt', } return type_map.get(ext, 'other') def _extract_figure_number(caption: str, section: str = '') -> str: """ 从 caption 或 section 中提取图号(增强版) 支持格式: - 图2.4, 图2-4 - Fig.2.4, Fig 2.4, Figure 2.4 - (图2) Args: caption: 图片标题/说明文字 section: 章节信息(可选) Returns: 图号字符串,如 "2.4";未找到返回空字符串 Example: >>> _extract_figure_number("图2.4 系统架构图") '2.4' >>> _extract_figure_number("参见Figure 3.1所示") '3.1' """ import re text = f"{caption} {section}" patterns = [ r'图\s*(\d+[\.\-]\d+)', r'Fig\.?\s*(\d+[\.\-]\d+)', r'Figure\s*(\d+[\.\-]\d+)', r'[((]\s*图\s*(\d+)\s*[))]', ] for pattern in patterns: match = re.search(pattern, text, re.IGNORECASE) if match: return match.group(1).replace('-', '.') return "" def normalize_department_name(department: str) -> str: """ 将部门名称标准化为英文标识 支持中文部门名(如"财务部")和英文标识(如"finance"), 返回符合 ChromaDB 命名规范的英文标识。 Args: department: 原始部门名称(中文或英文) Returns: 标准化的英文标识;无法识别时返回空字符串 Example: >>> normalize_department_name("财务部") 'finance' >>> normalize_department_name("tech") 'tech' """ if not department: return "" if department in DEPARTMENT_NAME_MAP: return DEPARTMENT_NAME_MAP[department] if department.replace("_", "").replace("-", "").isalnum() and department.isascii(): return department.lower() logger.warning(f"无法识别的部门名称: {department}") return "" def _extract_section(section_path: str, max_levels: int = 3) -> str: """ 动态截断章节路径,保留核心+末尾层级 用于在向量检索时提供简洁的章节上下文, 避免过长的章节路径影响语义匹配。 Args: section_path: 完整章节路径,用 '>' 分隔 max_levels: 最多保留的层级数(默认3级) Returns: 截断后的章节路径 Example: >>> _extract_section("第一章 > 1.1 概述 > 1.1.1 背景 > 1.1.1.1 详细说明") '1.1 概述 > 1.1.1 背景 > 1.1.1.1 详细说明' """ parts = [p.strip() for p in section_path.split('>') if p.strip()] if len(parts) <= max_levels: return ' > '.join(parts) return ' > '.join(parts[-max_levels:]) def _build_semantic_content_for_text(chunk, page_info: dict, doc_type: str) -> str: """ 构建语义增强内容(文本类型) 将原始文本切片转换为适合向量检索的语义增强格式, 包含标题、章节上下文和正文内容。 PDF 和 Word 差异化处理: - PDF: 有 text_level、bbox,标题识别准确 - Word: 无 text_level、bbox,依赖启发式识别 Args: chunk: 文档切片对象,含 title、content、text_level 属性 page_info: 页面信息字典,含 section_path、section 等 doc_type: 文档类型 ('pdf' | 'word' | 'excel' | 'ppt') Returns: 语义增强后的内容字符串,格式为: 标题 主题:章节路径 正文内容 """ parts = [] title = getattr(chunk, 'title', '') or '' if isinstance(title, list): title = ' '.join(str(t) for t in title if t) or '' if not isinstance(title, str): title = '' text_level = getattr(chunk, 'text_level', 0) if title and title.strip() and text_level > 0: parts.append(title.strip()) section = page_info.get('section_path', '') or page_info.get('section', '') if isinstance(section, list): section = ' > '.join(str(s) for s in section if s) or '' if not isinstance(section, str): section = '' if section and section.strip(): section = _extract_section(section, max_levels=3) parts.append(f"主题:{section}") content = chunk.content if hasattr(chunk, 'content') else page_info.get('text', '') if isinstance(content, list): content = '\n'.join(str(item) for item in content) parts.append(content) return "\n".join(parts) def _build_semantic_content_for_table(table_md: str, page_info: dict, chunk, doc_type: str) -> str: """ 构建语义增强内容(表格类型) 将 Markdown 表格转换为包含语义摘要的增强格式, 提取表头、字段、行数和示例数据,提升向量检索命中率。 Args: table_md: 表格的 Markdown 内容 page_info: 页面信息字典,含 section_path 等 chunk: 文档切片对象,含 title 属性 doc_type: 文档类型 ('pdf' | 'word' | 'excel' | 'ppt') Returns: 语义增强后的内容字符串,包含: 主题:章节路径 表格:标题 字段:表头列表 描述:行数和字段概要 示例:首行数据示例 """ parts = [] section = page_info.get('section_path', '') or page_info.get('section', '') if isinstance(section, list): section = ' > '.join(str(s) for s in section if s) or '' if not isinstance(section, str): section = '' if section and section.strip(): section = _extract_section(section, max_levels=3) parts.append(f"主题:{section}") caption = getattr(chunk, 'title', '') or '' if isinstance(caption, list): caption = ' '.join(str(c) for c in caption if c) or '' if caption and caption.strip() and caption != "表格": parts.append(f"表格:{caption.strip()}") # 检测是否是 HTML 格式,如果是则转换为 Markdown if '