"""Knowledge base service: document CRUD + simple RAG search.""" import json import re from sqlalchemy.orm import Session from typing import List, Optional from app.models.knowledge import KnowledgeDoc class KnowledgeService: @staticmethod def list_docs(db, tenant_id, project_id=None): q = db.query(KnowledgeDoc).filter(KnowledgeDoc.tenant_id == tenant_id) if project_id: q = q.filter( (KnowledgeDoc.project_id == project_id) | (KnowledgeDoc.project_id == None) ) return q.order_by(KnowledgeDoc.created_at.desc()).all() @staticmethod def get_doc(db, tenant_id, doc_id): return db.query(KnowledgeDoc).filter( KnowledgeDoc.id == doc_id, KnowledgeDoc.tenant_id == tenant_id ).first() @staticmethod def create_doc(db, tenant_id, req): keywords = KnowledgeService._extract_keywords(req.content + " " + req.title + " " + req.tags) doc = KnowledgeDoc( tenant_id=tenant_id, project_id=req.project_id, title=req.title, content=req.content, doc_type=req.doc_type, source=req.source, tags=req.tags, keywords=json.dumps(keywords, ensure_ascii=False), ) db.add(doc) db.commit() db.refresh(doc) return doc @staticmethod def update_doc(db, tenant_id, doc_id, content=None, title=None): doc = KnowledgeService.get_doc(db, tenant_id, doc_id) if not doc: return None if content is not None: doc.content = content if title is not None: doc.title = title doc.keywords = json.dumps( KnowledgeService._extract_keywords(doc.content + " " + doc.title), ensure_ascii=False, ) db.commit() db.refresh(doc) return doc @staticmethod def delete_doc(db, tenant_id, doc_id): doc = KnowledgeService.get_doc(db, tenant_id, doc_id) if not doc: return False db.delete(doc) db.commit() return True @staticmethod def search(db, tenant_id, query, project_id=None, limit=5): """Simple keyword-based search (MVP RAG without vector DB).""" docs = KnowledgeService.list_docs(db, tenant_id, project_id) query_lower = query.lower() query_terms = set(re.findall(r'\w+', query_lower)) scored = [] for doc in docs: content_lower = doc.content.lower() title_lower = doc.title.lower() keywords = set(json.loads(doc.keywords or "[]")) score = 0.0 # Title match (high weight) for term in query_terms: if term in title_lower: score += 3.0 if term in keywords: score += 2.0 count = content_lower.count(term) score += count * 0.5 if score > 0: snippet = doc.content[:200] + "..." if len(doc.content) > 200 else doc.content scored.append({ "doc_id": doc.id, "title": doc.title, "snippet": snippet, "score": round(score, 2), }) scored.sort(key=lambda x: x["score"], reverse=True) return scored[:limit] @staticmethod def _extract_keywords(text): """Extract keywords from text (simple: Chinese 2-4 char segments + English words).""" # English words en_words = re.findall(r'[a-zA-Z]{2,20}', text) # Chinese segments (2-4 chars) cn_segs = re.findall(r'[\u4e00-\u9fff]{2,4}', text) # Deduplicate and limit all_kw = list(set(en_words + cn_segs))[:20] return all_kw