feat: 对话历史页面租户分组展示功能
- 新增 ConversationHistoryManager.get_tenant_summary() 按租户聚合会话统计 - get_sessions_paginated() 和 get_conversation_analytics() 增加 tenant_id 过滤 - 新增 GET /api/conversations/tenants 租户汇总端点 - sessions 和 analytics API 端点支持 tenant_id 查询参数 - 前端实现租户卡片列表视图和租户详情会话表格视图 - 实现面包屑导航、搜索范围限定、统计面板上下文切换 - 会话删除后自动检测空租户并返回列表视图 - dashboard.html 添加租户视图 DOM 容器 - 交互模式与知识库租户分组视图保持一致
This commit is contained in:
@@ -5,7 +5,7 @@ from datetime import datetime
|
||||
import numpy as np
|
||||
from sklearn.feature_extraction.text import TfidfVectorizer
|
||||
from sklearn.metrics.pairwise import cosine_similarity
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import func, Integer
|
||||
|
||||
from ..core.database import db_manager
|
||||
from ..core.models import KnowledgeEntry, WorkOrder, Conversation
|
||||
@@ -162,24 +162,24 @@ class KnowledgeManager:
|
||||
logger.error(f"查找相似条目失败: {e}")
|
||||
return None
|
||||
|
||||
def search_knowledge(self, query: str, top_k: int = 3, verified_only: bool = True) -> List[Dict[str, Any]]:
|
||||
def search_knowledge(self, query: str, top_k: int = 3, verified_only: bool = True, tenant_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""搜索知识库 — 优先使用 embedding 语义检索,降级为关键词匹配"""
|
||||
try:
|
||||
# 尝试 embedding 语义检索
|
||||
if self.embedding_enabled:
|
||||
results = self._search_by_embedding(query, top_k, verified_only)
|
||||
results = self._search_by_embedding(query, top_k, verified_only, tenant_id=tenant_id)
|
||||
if results:
|
||||
return results
|
||||
logger.debug("Embedding 检索无结果,降级为关键词匹配")
|
||||
|
||||
# 降级:关键词匹配
|
||||
return self._search_by_keyword(query, top_k, verified_only)
|
||||
return self._search_by_keyword(query, top_k, verified_only, tenant_id=tenant_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"搜索知识库失败: {e}")
|
||||
return []
|
||||
|
||||
def _search_by_embedding(self, query: str, top_k: int = 3, verified_only: bool = True) -> List[Dict[str, Any]]:
|
||||
def _search_by_embedding(self, query: str, top_k: int = 3, verified_only: bool = True, tenant_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""基于 embedding 向量的语义检索"""
|
||||
try:
|
||||
query_vec = self.embedding_client.embed_text(query)
|
||||
@@ -205,6 +205,8 @@ class KnowledgeManager:
|
||||
KnowledgeEntry.id.in_(candidate_ids),
|
||||
KnowledgeEntry.is_active == True
|
||||
)
|
||||
if tenant_id is not None:
|
||||
query_filter = query_filter.filter(KnowledgeEntry.tenant_id == tenant_id)
|
||||
if verified_only:
|
||||
query_filter = query_filter.filter(KnowledgeEntry.is_verified == True)
|
||||
|
||||
@@ -212,10 +214,13 @@ class KnowledgeManager:
|
||||
|
||||
# 如果 verified_only 没结果,回退到全部
|
||||
if not entries and verified_only:
|
||||
entries = session.query(KnowledgeEntry).filter(
|
||||
fallback_filter = session.query(KnowledgeEntry).filter(
|
||||
KnowledgeEntry.id.in_(candidate_ids),
|
||||
KnowledgeEntry.is_active == True
|
||||
).all()
|
||||
)
|
||||
if tenant_id is not None:
|
||||
fallback_filter = fallback_filter.filter(KnowledgeEntry.tenant_id == tenant_id)
|
||||
entries = fallback_filter.all()
|
||||
|
||||
results = []
|
||||
for entry in entries:
|
||||
@@ -240,7 +245,7 @@ class KnowledgeManager:
|
||||
logger.error(f"Embedding 搜索失败: {e}")
|
||||
return []
|
||||
|
||||
def _search_by_keyword(self, query: str, top_k: int = 3, verified_only: bool = True) -> List[Dict[str, Any]]:
|
||||
def _search_by_keyword(self, query: str, top_k: int = 3, verified_only: bool = True, tenant_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""基于关键词的搜索(降级方案)"""
|
||||
try:
|
||||
with db_manager.get_session() as session:
|
||||
@@ -249,6 +254,9 @@ class KnowledgeManager:
|
||||
KnowledgeEntry.is_active == True
|
||||
)
|
||||
|
||||
if tenant_id is not None:
|
||||
query_filter = query_filter.filter(KnowledgeEntry.tenant_id == tenant_id)
|
||||
|
||||
# 如果只搜索已验证的知识库
|
||||
if verified_only:
|
||||
query_filter = query_filter.filter(KnowledgeEntry.is_verified == True)
|
||||
@@ -256,7 +264,10 @@ class KnowledgeManager:
|
||||
entries = query_filter.all()
|
||||
# 若已验证为空,则回退到全部活跃条目
|
||||
if not entries and verified_only:
|
||||
entries = session.query(KnowledgeEntry).filter(KnowledgeEntry.is_active == True).all()
|
||||
fallback_filter = session.query(KnowledgeEntry).filter(KnowledgeEntry.is_active == True)
|
||||
if tenant_id is not None:
|
||||
fallback_filter = fallback_filter.filter(KnowledgeEntry.tenant_id == tenant_id)
|
||||
entries = fallback_filter.all()
|
||||
|
||||
if not entries:
|
||||
logger.warning("知识库中没有活跃条目")
|
||||
@@ -334,10 +345,14 @@ class KnowledgeManager:
|
||||
answer: str,
|
||||
category: str,
|
||||
confidence_score: float = 0.5,
|
||||
is_verified: bool = False
|
||||
is_verified: bool = False,
|
||||
tenant_id: Optional[str] = None
|
||||
) -> bool:
|
||||
"""添加知识库条目"""
|
||||
try:
|
||||
# 确定 tenant_id:优先使用传入值,否则取配置默认值
|
||||
effective_tenant_id = tenant_id if tenant_id is not None else get_config().server.tenant_id
|
||||
|
||||
# 生成 embedding
|
||||
embedding_json = None
|
||||
text_for_embedding = question + " " + answer
|
||||
@@ -354,6 +369,7 @@ class KnowledgeManager:
|
||||
confidence_score=confidence_score,
|
||||
usage_count=0,
|
||||
is_verified=is_verified,
|
||||
tenant_id=effective_tenant_id,
|
||||
vector_embedding=embedding_json
|
||||
)
|
||||
session.add(entry)
|
||||
@@ -541,18 +557,23 @@ class KnowledgeManager:
|
||||
logger.error(f"删除知识库条目失败: {e}")
|
||||
return False
|
||||
|
||||
def get_knowledge_stats(self) -> Dict[str, Any]:
|
||||
def get_knowledge_stats(self, tenant_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""获取知识库统计信息"""
|
||||
try:
|
||||
with db_manager.get_session() as session:
|
||||
# 基础过滤条件
|
||||
base_filter = [KnowledgeEntry.is_active == True]
|
||||
if tenant_id is not None:
|
||||
base_filter.append(KnowledgeEntry.tenant_id == tenant_id)
|
||||
|
||||
# 只统计活跃(未删除)的条目
|
||||
total_entries = session.query(KnowledgeEntry).filter(
|
||||
KnowledgeEntry.is_active == True
|
||||
*base_filter
|
||||
).count()
|
||||
|
||||
# 统计已验证的条目
|
||||
verified_entries = session.query(KnowledgeEntry).filter(
|
||||
KnowledgeEntry.is_active == True,
|
||||
*base_filter,
|
||||
KnowledgeEntry.is_verified == True
|
||||
).count()
|
||||
|
||||
@@ -561,27 +582,100 @@ class KnowledgeManager:
|
||||
KnowledgeEntry.category,
|
||||
func.count(KnowledgeEntry.id)
|
||||
).filter(
|
||||
KnowledgeEntry.is_active == True
|
||||
*base_filter
|
||||
).group_by(KnowledgeEntry.category).all()
|
||||
|
||||
# 平均置信度(仅限活跃条目)
|
||||
avg_confidence = session.query(
|
||||
func.avg(KnowledgeEntry.confidence_score)
|
||||
).filter(
|
||||
KnowledgeEntry.is_active == True
|
||||
*base_filter
|
||||
).scalar() or 0.0
|
||||
|
||||
return {
|
||||
result = {
|
||||
"total_entries": total_entries,
|
||||
"active_entries": verified_entries, # 将 active_entries 复用为已验证数量,或前端相应修改
|
||||
"category_distribution": dict(category_stats),
|
||||
"average_confidence": float(avg_confidence)
|
||||
}
|
||||
|
||||
if tenant_id is not None:
|
||||
result["tenant_id"] = tenant_id
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取知识库统计失败: {e}")
|
||||
return {}
|
||||
|
||||
def get_tenant_summary(self) -> List[Dict[str, Any]]:
|
||||
"""按 tenant_id 聚合活跃知识条目,返回租户汇总列表。
|
||||
|
||||
返回格式: [
|
||||
{
|
||||
"tenant_id": "market_a",
|
||||
"entry_count": 42,
|
||||
"verified_count": 30,
|
||||
"category_distribution": {"FAQ": 20, "故障排查": 22}
|
||||
}, ...
|
||||
]
|
||||
按 entry_count 降序排列。
|
||||
"""
|
||||
try:
|
||||
with db_manager.get_session() as session:
|
||||
# 主聚合查询:按 tenant_id 统计 entry_count 和 verified_count
|
||||
summary_rows = session.query(
|
||||
KnowledgeEntry.tenant_id,
|
||||
func.count(KnowledgeEntry.id).label('entry_count'),
|
||||
func.sum(
|
||||
func.cast(KnowledgeEntry.is_verified, Integer)
|
||||
).label('verified_count')
|
||||
).filter(
|
||||
KnowledgeEntry.is_active == True
|
||||
).group_by(
|
||||
KnowledgeEntry.tenant_id
|
||||
).order_by(
|
||||
func.count(KnowledgeEntry.id).desc()
|
||||
).all()
|
||||
|
||||
if not summary_rows:
|
||||
return []
|
||||
|
||||
# 类别分布查询:按 tenant_id + category 统计
|
||||
category_rows = session.query(
|
||||
KnowledgeEntry.tenant_id,
|
||||
KnowledgeEntry.category,
|
||||
func.count(KnowledgeEntry.id).label('cat_count')
|
||||
).filter(
|
||||
KnowledgeEntry.is_active == True
|
||||
).group_by(
|
||||
KnowledgeEntry.tenant_id,
|
||||
KnowledgeEntry.category
|
||||
).all()
|
||||
|
||||
# 构建 tenant_id -> {category: count} 映射
|
||||
category_map: Dict[str, Dict[str, int]] = {}
|
||||
for row in category_rows:
|
||||
if row.tenant_id not in category_map:
|
||||
category_map[row.tenant_id] = {}
|
||||
category_map[row.tenant_id][row.category] = row.cat_count
|
||||
|
||||
# 组装结果
|
||||
result = []
|
||||
for row in summary_rows:
|
||||
result.append({
|
||||
"tenant_id": row.tenant_id,
|
||||
"entry_count": row.entry_count,
|
||||
"verified_count": int(row.verified_count or 0),
|
||||
"category_distribution": category_map.get(row.tenant_id, {})
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取租户汇总失败: {e}")
|
||||
return []
|
||||
|
||||
def update_usage_count(self, entry_ids: List[int]) -> bool:
|
||||
"""更新知识库条目的使用次数"""
|
||||
try:
|
||||
@@ -602,12 +696,15 @@ class KnowledgeManager:
|
||||
logger.error(f"更新知识库使用次数失败: {e}")
|
||||
return False
|
||||
|
||||
def get_knowledge_paginated(self, page: int = 1, per_page: int = 10, category_filter: str = '', verified_filter: str = '') -> Dict[str, Any]:
|
||||
def get_knowledge_paginated(self, page: int = 1, per_page: int = 10, category_filter: str = '', verified_filter: str = '', tenant_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""获取知识库条目(分页和过滤)"""
|
||||
try:
|
||||
with db_manager.get_session() as session:
|
||||
query = session.query(KnowledgeEntry).filter(KnowledgeEntry.is_active == True)
|
||||
|
||||
if tenant_id is not None:
|
||||
query = query.filter(KnowledgeEntry.tenant_id == tenant_id)
|
||||
|
||||
if category_filter:
|
||||
query = query.filter(KnowledgeEntry.category == category_filter)
|
||||
if verified_filter:
|
||||
|
||||
Reference in New Issue
Block a user