Files

102 lines
4.9 KiB
Python

"""Training-map library/category scope, shared by retrieval, full reading and estimates."""
from sqlmodel import select
from fastapi import HTTPException
from .models import KnowledgeBase, KnowledgeCategory, KnowledgeFile
from .rag import store
from .rag.quarantine import file_exclusions
def normalize_scope(session, kb_ids, mapping):
if not isinstance(mapping, dict):
raise HTTPException(422, '知识库分类选择格式无效')
selected = set(kb_ids)
result = {}
for key, values in mapping.items():
try:
kid = int(key)
except (ValueError, TypeError):
raise HTTPException(422, '知识库编号无效') from None
if str(kid) != str(key) or kid not in selected:
raise HTTPException(422, '分类选择包含未选中的知识库')
if not isinstance(values, list) or not values:
raise HTTPException(422, '请至少选择一个分类,或切换为全部分类')
categories = []
for value in values:
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
raise HTTPException(422, '分类编号无效')
cat = session.get(KnowledgeCategory, value) if value else None
if value and (not cat or cat.kb_id != kid):
raise HTTPException(422, '所选分类已删除或不属于对应知识库,请重新选择')
categories.append(value)
result[str(kid)] = sorted(set(categories))
return {key: result[key] for key in sorted(result, key=int)}
def file_scope(session, kb_id, mapping):
"""None means whole library; an empty set means no matching files (never whole library)."""
excluded = file_exclusions(session, kb_id)
values = (mapping or {}).get(str(kb_id))
if values is None:
if not excluded:
return None
registered = session.exec(select(KnowledgeFile.file_name).where(KnowledgeFile.kb_id == kb_id)).all()
return (set(store.count_by_file(kb_id)) | set(registered)) - excluded
normalize_scope(session, [kb_id], {str(kb_id): values})
cats = session.exec(select(KnowledgeCategory).where(KnowledgeCategory.kb_id == kb_id)).all()
included = set(values)
changed = True
while changed:
changed = False
for cat in cats:
if cat.parent_id in included and cat.id not in included:
included.add(cat.id); changed = True
files = session.exec(select(KnowledgeFile).where(KnowledgeFile.kb_id == kb_id)).all()
return {file.file_name for file in files
if (file.category_id if file.category_id is not None else file.ai_category_id or 0) in included} - excluded
def category_options(session, kb_id):
counts = store.count_by_file(kb_id)
files = session.exec(select(KnowledgeFile).where(KnowledgeFile.kb_id == kb_id)).all()
cats = session.exec(select(KnowledgeCategory).where(KnowledgeCategory.kb_id == kb_id)
.order_by(KnowledgeCategory.sort_order, KnowledgeCategory.id)).all()
# Read the tree and files once; repeated per-category queries make large
# libraries noticeably slow when their category drawer first opens.
parents = {cat.id: cat.parent_id for cat in cats}
names_by_category = {cat.id: set() for cat in cats}
unclassified = set()
for file in files:
current = file.category_id if file.category_id is not None else file.ai_category_id or 0
if not current:
unclassified.add(file.file_name)
visited = set()
while current in parents and current not in visited:
visited.add(current)
names_by_category[current].add(file.file_name)
current = parents[current]
categories = []
for cat in cats:
names = names_by_category[cat.id]
categories.append({'id': cat.id, 'name': cat.name, 'parent_id': cat.parent_id,
'file_count': len(names), 'chunk_count': sum(counts.get(name, 0) for name in names)})
registered = {file.file_name for file in files}
return {'categories': categories, 'unclassified': {'file_count': len(unclassified),
'chunk_count': sum(counts.get(name, 0) for name in unclassified)},
'unregistered_index_files': len(set(counts) - registered)}
def scope_counts(session, kb_ids, mapping):
normalized = normalize_scope(session, kb_ids, mapping or {})
result = []
for kid in dict.fromkeys(kb_ids):
kb = session.get(KnowledgeBase, kid)
allowed = file_scope(session, kid, normalized)
if allowed is None:
result.append({'kb_id': kid, 'file_count': max(0, kb.file_count or 0),
'chunk_count': max(0, kb.chunk_count or 0), 'chunk_target': kb.chunk_target})
else:
counts = store.count_by_file(kid)
result.append({'kb_id': kid, 'file_count': len(allowed),
'chunk_count': sum(counts.get(name, 0) for name in allowed), 'chunk_target': kb.chunk_target})
return result