Files
rag/services/outline.py
lacerate551 100d1a06eb init: RAG 知识库服务初始提交
- 后端 API(Flask + Gunicorn)
- RAG 引擎(混合检索 + 云端 Reranker + 引用溯源)
- 文档解析(MinerU + 多格式支持)
- Docker 生产部署配置
- 排除前端项目、敏感配置、模型文件
2026-06-04 17:35:27 +08:00

987 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
纲要生成与关联推荐服务
功能:
1. GKPT-MIND-020 自动化纲要生成
- AI提取制度文件章节结构
- 生成思维导图数据
- 支持多种格式导出
2. GKPT-READ-005 关联推荐
- 基于向量相似度推荐相关文档
- 支持标签匹配
- 综合排序
"""
import json
import os
import hashlib
import logging
from datetime import datetime
from typing import Optional, List, Dict, Any, Tuple
from dataclasses import dataclass, asdict, field
from data.db import get_connection, init_databases
from core.llm_utils import call_llm, parse_json_from_response
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# ==================== 数据类定义 ====================
@dataclass
class OutlineNode:
"""纲要节点"""
id: str = ""
title: str = ""
summary: str = ""
level: int = 1
order: int = 1
page: int = 0
children: List['OutlineNode'] = field(default_factory=list)
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"id": self.id,
"title": self.title,
"summary": self.summary,
"level": self.level,
"order": self.order,
"page": self.page,
"children": [child.to_dict() for child in self.children]
}
@classmethod
def from_dict(cls, data: Dict) -> 'OutlineNode':
"""从字典创建"""
node = cls(
id=data.get("id", ""),
title=data.get("title", ""),
summary=data.get("summary", ""),
level=data.get("level", 1),
order=data.get("order", 1),
page=data.get("page", 0)
)
for child_data in data.get("children", []):
node.children.append(cls.from_dict(child_data))
return node
@dataclass
class DocumentOutline:
"""文档纲要"""
document_id: str = ""
document_name: str = ""
total_pages: int = 0
content_hash: str = ""
generated_at: str = ""
outline: List[OutlineNode] = field(default_factory=list)
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"document_id": self.document_id,
"document_name": self.document_name,
"total_pages": self.total_pages,
"content_hash": self.content_hash,
"generated_at": self.generated_at,
"outline": [node.to_dict() for node in self.outline]
}
@classmethod
def from_dict(cls, data: Dict) -> 'DocumentOutline':
"""从字典创建"""
return cls(
document_id=data.get("document_id", ""),
document_name=data.get("document_name", ""),
total_pages=data.get("total_pages", 0),
content_hash=data.get("content_hash", ""),
generated_at=data.get("generated_at", ""),
outline=[OutlineNode.from_dict(n) for n in data.get("outline", [])]
)
@dataclass
class Recommendation:
"""推荐结果"""
document_id: str = ""
document_name: str = ""
summary: str = ""
similarity: float = 0.0
tag_score: float = 0.0
final_score: float = 0.0
tags: List[str] = field(default_factory=list)
reason: str = "" # 推荐理由
def to_dict(self) -> Dict:
return asdict(self)
# ==================== 数据库管理 ====================
class OutlineDB:
"""纲要缓存数据库"""
def __init__(self):
init_databases()
self._init_db()
def _init_db(self):
"""初始化数据库表"""
with get_connection("knowledge") as conn:
cursor = conn.cursor()
# 纲要缓存表
cursor.execute("""
CREATE TABLE IF NOT EXISTS outline_cache (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id TEXT NOT NULL UNIQUE,
document_name TEXT,
total_pages INTEGER DEFAULT 0,
content_hash TEXT NOT NULL,
outline_json TEXT NOT NULL,
generated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# 文档向量缓存表
cursor.execute("""
CREATE TABLE IF NOT EXISTS document_vectors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id TEXT NOT NULL UNIQUE,
document_name TEXT,
vector_hash TEXT,
vector_json TEXT NOT NULL,
tags_json TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# 推荐缓存表
cursor.execute("""
CREATE TABLE IF NOT EXISTS recommendation_cache (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id TEXT NOT NULL,
recommendations_json TEXT NOT NULL,
generated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# 创建索引
cursor.execute("CREATE INDEX IF NOT EXISTS idx_outline_doc ON outline_cache(document_id)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_vector_doc ON document_vectors(document_id)")
# ==================== 纲要缓存 ====================
def save_outline(self, outline: DocumentOutline) -> int:
"""保存纲要"""
with get_connection("knowledge") as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT OR REPLACE INTO outline_cache
(document_id, document_name, total_pages, content_hash, outline_json, generated_at)
VALUES (?, ?, ?, ?, ?, ?)
""", (
outline.document_id,
outline.document_name,
outline.total_pages,
outline.content_hash,
json.dumps(outline.to_dict(), ensure_ascii=False),
outline.generated_at
))
return cursor.lastrowid
def get_outline(self, document_id: str) -> Optional[DocumentOutline]:
"""获取纲要"""
with get_connection("knowledge") as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT document_id, document_name, total_pages, content_hash,
outline_json, generated_at
FROM outline_cache WHERE document_id = ?
""", (document_id,))
row = cursor.fetchone()
if not row:
return None
outline_data = json.loads(row[4])
return DocumentOutline.from_dict(outline_data)
def delete_outline(self, document_id: str) -> bool:
"""删除纲要缓存"""
with get_connection("knowledge") as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM outline_cache WHERE document_id = ?", (document_id,))
return cursor.rowcount > 0
def list_outlines(self, limit: int = 50) -> List[Dict]:
"""获取纲要列表"""
with get_connection("knowledge") as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT document_id, document_name, total_pages, generated_at
FROM outline_cache ORDER BY generated_at DESC LIMIT ?
""", (limit,))
rows = cursor.fetchall()
return [
{
"document_id": row[0],
"document_name": row[1],
"total_pages": row[2],
"generated_at": row[3]
}
for row in rows
]
# ==================== 文档向量 ====================
def save_document_vector(self, document_id: str, document_name: str,
vector: List[float], tags: List[str] = None) -> int:
"""保存文档向量"""
with get_connection("knowledge") as conn:
cursor = conn.cursor()
vector_hash = hashlib.md5(str(vector[:10]).encode()).hexdigest()[:8]
cursor.execute("""
INSERT OR REPLACE INTO document_vectors
(document_id, document_name, vector_hash, vector_json, tags_json, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
""", (
document_id,
document_name,
vector_hash,
json.dumps(vector),
json.dumps(tags or [], ensure_ascii=False),
datetime.now().isoformat()
))
return cursor.lastrowid
def get_document_vector(self, document_id: str) -> Optional[Dict]:
"""获取文档向量"""
with get_connection("knowledge") as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT document_id, document_name, vector_json, tags_json
FROM document_vectors WHERE document_id = ?
""", (document_id,))
row = cursor.fetchone()
if not row:
return None
return {
"document_id": row[0],
"document_name": row[1],
"vector": json.loads(row[2]),
"tags": json.loads(row[3]) if row[3] else []
}
def get_all_document_vectors(self) -> List[Dict]:
"""获取所有文档向量"""
with get_connection("knowledge") as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT document_id, document_name, vector_json, tags_json
FROM document_vectors
""")
rows = cursor.fetchall()
return [
{
"document_id": row[0],
"document_name": row[1],
"vector": json.loads(row[2]),
"tags": json.loads(row[3]) if row[3] else []
}
for row in rows
]
# ==================== 推荐缓存 ====================
def save_recommendations(self, document_id: str, recommendations: List[Recommendation]) -> int:
"""保存推荐结果"""
with get_connection("knowledge") as conn:
cursor = conn.cursor()
# 先删除旧缓存
cursor.execute("DELETE FROM recommendation_cache WHERE document_id = ?", (document_id,))
cursor.execute("""
INSERT INTO recommendation_cache (document_id, recommendations_json, generated_at)
VALUES (?, ?, ?)
""", (
document_id,
json.dumps([r.to_dict() for r in recommendations], ensure_ascii=False),
datetime.now().isoformat()
))
return cursor.lastrowid
def get_recommendations(self, document_id: str) -> Optional[List[Recommendation]]:
"""获取推荐结果"""
with get_connection("knowledge") as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT recommendations_json FROM recommendation_cache WHERE document_id = ?
""", (document_id,))
row = cursor.fetchone()
if not row:
return None
return [Recommendation(**r) for r in json.loads(row[0])]
# ==================== 纲要生成服务 ====================
class OutlineGenerator:
"""纲要生成服务"""
def __init__(self, db: OutlineDB, documents_path: str = "./documents"):
self.db = db
self.documents_path = documents_path
self.llm_client = None
self.embedding_model = None
self._init_llm()
def _init_llm(self):
"""初始化LLM客户端"""
try:
from config import API_KEY, BASE_URL, MODEL
from openai import OpenAI
self.llm_client = OpenAI(api_key=API_KEY, base_url=BASE_URL)
self.model = MODEL
logger.info("LLM客户端初始化成功")
except ImportError:
logger.warning("未找到LLM配置纲要生成功能受限")
self.llm_client = None
def generate_outline(self, document_id: str, force: bool = False) -> DocumentOutline:
"""
生成文档纲要
Args:
document_id: 文档ID相对路径如 public/差旅管理办法.txt
force: 是否强制重新生成
Returns:
DocumentOutline
"""
# 1. 检查缓存
if not force:
cached = self.db.get_outline(document_id)
if cached:
# 检查内容是否变化
current_hash = self._get_document_hash(document_id)
if current_hash == cached.content_hash:
logger.info(f"使用缓存的纲要: {document_id}")
return cached
# 2. 获取文档内容
document_content = self._read_document(document_id)
if not document_content:
raise ValueError(f"文档不存在或无法读取: {document_id}")
document_name = os.path.basename(document_id)
# 3. 使用LLM提取结构
outline_data = self._extract_structure(document_content, document_name)
# 4. 构建纲要对象
outline = DocumentOutline(
document_id=document_id,
document_name=document_name,
total_pages=0, # 可以后续计算
content_hash=self._get_document_hash(document_id),
generated_at=datetime.now().isoformat(),
outline=[OutlineNode.from_dict(n) for n in outline_data.get("children", [])]
)
# 5. 保存缓存
self.db.save_outline(outline)
logger.info(f"纲要生成完成: {document_id}")
return outline
def _read_document(self, document_id: str) -> Optional[str]:
"""读取文档内容"""
# 处理不同的文档格式
file_path = os.path.join(self.documents_path, document_id)
if not os.path.exists(file_path):
logger.error(f"文档不存在: {file_path}")
return None
ext = os.path.splitext(file_path)[1].lower()
try:
if ext == '.txt':
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
elif ext == '.pdf':
# 使用 pdfplumber 提取文本
try:
import pdfplumber
text_parts = []
with pdfplumber.open(file_path) as pdf:
for page in pdf.pages:
text = page.extract_text()
if text:
text_parts.append(text)
return '\n'.join(text_parts)
except ImportError:
logger.warning("pdfplumber未安装无法读取PDF")
return None
elif ext in ['.docx', '.doc']:
# 使用 python-docx 提取文本
try:
from docx import Document
doc = Document(file_path)
return '\n'.join([p.text for p in doc.paragraphs if p.text.strip()])
except ImportError:
logger.warning("python-docx未安装无法读取Word文档")
return None
elif ext == '.xlsx':
# 使用 openpyxl 提取文本
try:
from openpyxl import load_workbook
wb = load_workbook(file_path)
text_parts = []
for sheet in wb.worksheets:
for row in sheet.iter_rows(values_only=True):
text_parts.extend([str(cell) for cell in row if cell])
return '\n'.join(text_parts)
except ImportError:
logger.warning("openpyxl未安装无法读取Excel")
return None
else:
# 尝试作为文本读取
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
except Exception as e:
logger.error(f"读取文档失败: {e}")
return None
def _get_document_hash(self, document_id: str) -> str:
"""计算文档哈希"""
file_path = os.path.join(self.documents_path, document_id)
if not os.path.exists(file_path):
return ""
with open(file_path, 'rb') as f:
return hashlib.md5(f.read()).hexdigest()
def _extract_structure(self, content: str, document_name: str) -> Dict:
"""使用LLM提取文档结构"""
if not self.llm_client:
# 返回基本结构
return {
"title": document_name,
"summary": "无法生成摘要LLM未配置",
"children": []
}
# 限制内容长度
max_length = 8000
if len(content) > max_length:
content = content[:max_length] + "\n...(内容已截断)"
prompt = f"""请分析以下制度文档,提取章节结构和核心要点。
文档名称:{document_name}
文档内容:
{content}
请按以下格式返回JSON
{{
"title": "文档标题",
"summary": "文档概述50字以内",
"children": [
{{
"id": "1",
"title": "第一章 一级标题",
"summary": "本章核心要点30字以内",
"level": 1,
"order": 1,
"children": [
{{
"id": "1.1",
"title": "1.1 二级标题",
"summary": "核心要点",
"level": 2,
"order": 1,
"children": []
}}
]
}}
]
}}
要求:
1. 识别文档的一级、二级、三级标题(如有)
2. 每个章节提取核心要点不超过30字
3. 保持层级关系最多3层
4. 只返回JSON不要有其他内容"""
try:
result_text = call_llm(
self.llm_client,
prompt=prompt,
model=self.model,
temperature=0.3,
max_tokens=3000
)
if not result_text:
logger.error("LLM提取结构返回空")
return {
"title": document_name,
"summary": "生成失败: LLM返回空",
"children": []
}
# 清理可能的markdown标记
if result_text.startswith("```json"):
result_text = result_text[7:]
if result_text.startswith("```"):
result_text = result_text[3:]
if result_text.endswith("```"):
result_text = result_text[:-3]
result_text = result_text.strip()
return json.loads(result_text)
except Exception as e:
logger.error(f"LLM提取结构失败: {e}")
return {
"title": document_name,
"summary": f"生成失败: {str(e)}",
"children": []
}
def export_outline(self, outline: DocumentOutline, format: str = "json") -> str:
"""
导出纲要
Args:
outline: 纲要对象
format: 导出格式 (json/markdown/markmap)
Returns:
导出内容
"""
if format == "json":
return json.dumps(outline.to_dict(), ensure_ascii=False, indent=2)
elif format == "markdown":
return self._export_markdown(outline)
elif format == "markmap":
# markmap 格式(可渲染为思维导图的 Markdown
return self._export_markmap(outline)
else:
raise ValueError(f"不支持的导出格式: {format}")
def _export_markdown(self, outline: DocumentOutline, node: OutlineNode = None, level: int = 0) -> str:
"""导出为 Markdown 格式"""
lines = []
if node is None:
# 根级别
lines.append(f"# {outline.document_name}\n")
lines.append(f"> 生成时间: {outline.generated_at}\n")
for child in outline.outline:
lines.append(self._export_markdown(outline, child, 1))
else:
# 节点级别
prefix = "#" * (level + 1)
lines.append(f"{prefix} {node.title}\n")
if node.summary:
lines.append(f"{node.summary}\n")
for child in node.children:
lines.append(self._export_markdown(outline, child, level + 1))
return "\n".join(lines)
def _export_markmap(self, outline: DocumentOutline) -> str:
"""导出为 markmap 格式(思维导图 Markdown"""
lines = [f"# {outline.document_name}"]
def render_node(node: OutlineNode, level: int):
indent = " " * level
lines.append(f"{indent}- {node.title}")
if node.summary:
lines.append(f"{indent} - *{node.summary}*")
for child in node.children:
render_node(child, level + 1)
for node in outline.outline:
render_node(node, 1)
return "\n".join(lines)
def batch_generate(self, document_ids: List[str], force: bool = False) -> Dict[str, DocumentOutline]:
"""
批量生成纲要
Args:
document_ids: 文档ID列表
force: 是否强制重新生成
Returns:
{document_id: DocumentOutline}
"""
results = {}
for doc_id in document_ids:
try:
results[doc_id] = self.generate_outline(doc_id, force)
except Exception as e:
logger.error(f"生成纲要失败 {doc_id}: {e}")
results[doc_id] = None
return results
# ==================== 关联推荐服务 ====================
class RecommendationService:
"""关联推荐服务"""
def __init__(self, db: OutlineDB, documents_path: str = "./documents",
chroma_collection=None, embedding_model=None):
self.db = db
self.documents_path = documents_path
self.chroma_collection = chroma_collection
self.embedding_model = embedding_model
def get_recommendations(self, document_id: str, top_k: int = 5,
use_cache: bool = True) -> List[Recommendation]:
"""
获取关联推荐
Args:
document_id: 当前文档ID
top_k: 返回数量
use_cache: 是否使用缓存
Returns:
推荐列表
"""
# 1. 检查缓存
if use_cache:
cached = self.db.get_recommendations(document_id)
if cached:
logger.info(f"使用缓存的推荐: {document_id}")
return cached[:top_k]
# 2. 获取当前文档向量
current_vector = self._get_or_compute_vector(document_id)
if current_vector is None:
logger.warning(f"无法获取文档向量: {document_id}")
return []
current_doc = self.db.get_document_vector(document_id)
current_tags = current_doc.get("tags", []) if current_doc else []
# 3. 检索相似文档
similar_docs = self._search_similar(current_vector, top_k * 3, exclude_id=document_id)
# 4. 计算综合得分
recommendations = []
for doc in similar_docs:
# 标签匹配得分
doc_tags = doc.get("tags", [])
tag_overlap = len(set(current_tags) & set(doc_tags))
tag_score = min(tag_overlap * 0.15, 0.3) # 最高0.3
# 综合得分
similarity = doc.get("similarity", 0)
final_score = similarity * 0.7 + tag_score
# 推荐理由
reasons = []
if similarity > 0.8:
reasons.append("内容高度相似")
elif similarity > 0.6:
reasons.append("内容相关")
if tag_overlap > 0:
reasons.append(f"包含{tag_overlap}个相同标签")
recommendation = Recommendation(
document_id=doc.get("document_id", ""),
document_name=doc.get("document_name", ""),
summary=doc.get("summary", "")[:100] if doc.get("summary") else "",
similarity=round(similarity, 3),
tag_score=round(tag_score, 3),
final_score=round(final_score, 3),
tags=doc_tags[:5],
reason="".join(reasons) if reasons else "相关推荐"
)
recommendations.append(recommendation)
# 5. 排序并截取
recommendations.sort(key=lambda x: x.final_score, reverse=True)
results = recommendations[:top_k]
# 6. 缓存结果
if results:
self.db.save_recommendations(document_id, results)
return results
def _get_or_compute_vector(self, document_id: str) -> Optional[List[float]]:
"""获取或计算文档向量"""
# 检查缓存
cached = self.db.get_document_vector(document_id)
if cached and cached.get("vector"):
return cached["vector"]
# 计算向量
if not self.embedding_model:
logger.warning("嵌入模型未初始化,无法计算向量")
return None
# 读取文档内容
file_path = os.path.join(self.documents_path, document_id)
if not os.path.exists(file_path):
return None
try:
# 使用 _read_document 方法读取文档(支持多种格式)
content = self._read_document(file_path)
if not content:
logger.warning(f"无法读取文档内容: {document_id}")
return None
# 计算向量
vector = self.embedding_model.encode(content[:5000]) # 限制长度
# 缓存
doc_name = os.path.basename(document_id)
self.db.save_document_vector(document_id, doc_name, vector.tolist())
return vector.tolist()
except Exception as e:
logger.error(f"计算文档向量失败: {e}")
return None
def _search_similar(self, query_vector: List[float], top_k: int,
exclude_id: str = None) -> List[Dict]:
"""搜索相似文档"""
if not self.chroma_collection:
# 使用数据库缓存
all_docs = self.db.get_all_document_vectors()
results = []
import numpy as np
query_vec = np.array(query_vector)
for doc in all_docs:
if exclude_id and doc["document_id"] == exclude_id:
continue
doc_vec = np.array(doc["vector"])
# 余弦相似度
similarity = np.dot(query_vec, doc_vec) / (
np.linalg.norm(query_vec) * np.linalg.norm(doc_vec)
)
results.append({
"document_id": doc["document_id"],
"document_name": doc["document_name"],
"similarity": float(similarity),
"tags": doc.get("tags", [])
})
results.sort(key=lambda x: x["similarity"], reverse=True)
return results[:top_k]
# 使用 ChromaDB
try:
results = self.chroma_collection.query(
query_embeddings=[query_vector],
n_results=top_k + 1 # 多取一个,排除自己
)
docs = []
for i, doc_id in enumerate(results.get("ids", [[]])[0]):
if exclude_id and doc_id == exclude_id:
continue
metadata = results.get("metadatas", [[]])[0][i] if results.get("metadatas") else {}
distance = results.get("distances", [[]])[0][i] if results.get("distances") else 0
# 转换距离为相似度
similarity = 1 - distance if distance < 1 else 0
docs.append({
"document_id": doc_id,
"document_name": metadata.get("source", doc_id),
"similarity": similarity,
"tags": metadata.get("tags", "").split(",") if metadata.get("tags") else []
})
return docs[:top_k]
except Exception as e:
logger.error(f"相似文档检索失败: {e}")
return []
def compute_all_vectors(self) -> int:
"""计算所有文档的向量"""
if not self.embedding_model:
logger.warning("嵌入模型未初始化")
return 0
count = 0
for root, dirs, files in os.walk(self.documents_path):
for file in files:
if file.endswith(('.txt', '.pdf', '.docx', '.md')):
document_id = os.path.relpath(
os.path.join(root, file),
self.documents_path
).replace("\\", "/")
try:
self._get_or_compute_vector(document_id)
count += 1
except Exception as e:
logger.error(f"计算向量失败 {document_id}: {e}")
logger.info(f"计算了 {count} 个文档的向量")
return count
# ==================== 便捷函数 ====================
def create_services(documents_path: str = "./documents",
chroma_collection=None,
embedding_model=None) -> Tuple[OutlineDB, OutlineGenerator, RecommendationService]:
"""
创建服务实例
Args:
documents_path: 文档目录
chroma_collection: ChromaDB集合
embedding_model: 嵌入模型
Returns:
(数据库实例, 纲要生成服务, 推荐服务)
"""
db = OutlineDB()
outline_generator = OutlineGenerator(db, documents_path)
recommendation_service = RecommendationService(
db, documents_path, chroma_collection, embedding_model
)
return db, outline_generator, recommendation_service
# ==================== 使用示例 ====================
if __name__ == "__main__":
import sys
# 设置编码
if sys.platform == 'win32':
sys.stdout.reconfigure(encoding='utf-8')
print("=" * 60)
print("纲要生成与关联推荐服务测试")
print("=" * 60)
# 创建服务
db, outline_svc, rec_svc = create_services(
documents_path="./documents"
)
# 测试纲要生成
print("\n[1] 测试纲要生成...")
# 查找一个测试文档
documents_path = "./documents"
test_doc = None
for root, dirs, files in os.walk(documents_path):
for file in files:
if file.endswith('.txt'):
test_doc = os.path.relpath(
os.path.join(root, file),
documents_path
).replace("\\", "/")
break
if test_doc:
break
if test_doc:
print(f" 测试文档: {test_doc}")
try:
outline = outline_svc.generate_outline(test_doc)
print(f" 标题: {outline.document_name}")
print(f" 章节数: {len(outline.outline)}")
for node in outline.outline[:3]:
print(f" - {node.title}: {node.summary[:30]}...")
# 测试导出
print("\n[2] 测试导出...")
md_content = outline_svc.export_outline(outline, "markdown")
print(f" Markdown 导出长度: {len(md_content)} 字符")
markmap_content = outline_svc.export_outline(outline, "markmap")
print(f" Markmap 导出长度: {len(markmap_content)} 字符")
except Exception as e:
print(f" 纲要生成失败: {e}")
else:
print(" 未找到测试文档")
# 测试推荐服务
print("\n[3] 测试关联推荐...")
if test_doc:
try:
recommendations = rec_svc.get_recommendations(test_doc, top_k=3)
print(f" 推荐数量: {len(recommendations)}")
for rec in recommendations:
print(f" - {rec.document_name} (相似度: {rec.similarity}, 理由: {rec.reason})")
except Exception as e:
print(f" 推荐失败: {e}")
# 列出纲要缓存
print("\n[4] 测试纲要缓存...")
outlines = db.list_outlines()
print(f" 缓存数量: {len(outlines)}")
print("\n" + "=" * 60)
print("测试完成")
print("=" * 60)