101 lines
3.8 KiB
Python
101 lines
3.8 KiB
Python
import json
|
|||
|
|
from datetime import UTC, datetime
|
||
|
|
|
||
|
|
from sqlalchemy import select
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
from sqlalchemy.sql.elements import ColumnElement
|
||
|
|
|
||
|
|
from app.core.knowledge_contracts import KnowledgeHit, KnowledgeQuery
|
||
|
|
from app.model.knowledge import FinKnowledgeMeta
|
||
|
|
|
||
|
|
|
||
|
|
class KnowledgeMysqlAuthority:
|
||
|
|
def __init__(self, session: AsyncSession) -> None:
|
||
|
|
self._session = session
|
||
|
|
|
||
|
|
async def filter_published(self, hits: tuple[KnowledgeHit, ...]) -> list[KnowledgeHit]:
|
||
|
|
ids = tuple(int(hit.knowledge_id) for hit in hits if hit.knowledge_id.isdecimal())
|
||
|
|
if not ids:
|
||
|
|
return []
|
||
|
|
rows = await self._session.scalars(
|
||
|
|
select(FinKnowledgeMeta).where(
|
||
|
|
FinKnowledgeMeta.id.in_(ids), *self._published_filters()
|
||
|
|
)
|
||
|
|
)
|
||
|
|
approved = {str(row.id): row for row in rows}
|
||
|
|
result: list[KnowledgeHit] = []
|
||
|
|
for hit in hits:
|
||
|
|
row = approved.get(hit.knowledge_id)
|
||
|
|
if row is None:
|
||
|
|
continue
|
||
|
|
answer = self.extract_answer(row.content_text).strip()
|
||
|
|
if answer:
|
||
|
|
result.append(hit.model_copy(update={
|
||
|
|
"answer": answer, "version": row.version, "title": row.title,
|
||
|
|
}))
|
||
|
|
return result
|
||
|
|
|
||
|
|
async def search_keyword(
|
||
|
|
self, query: KnowledgeQuery, collections: tuple[str, ...], top_k: int
|
||
|
|
) -> list[KnowledgeHit]:
|
||
|
|
"""向量服务不可用时,在原授权集合内执行受限的只读关键词检索。"""
|
||
|
|
keyword = self._keyword(query.query)
|
||
|
|
if not collections or not keyword:
|
||
|
|
return []
|
||
|
|
# 显式转义 LIKE 通配符,避免用户输入扩大关键词降级的匹配范围。
|
||
|
|
escaped_keyword = keyword.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||
|
|
rows = await self._session.scalars(
|
||
|
|
select(FinKnowledgeMeta)
|
||
|
|
.where(
|
||
|
|
FinKnowledgeMeta.milvus_collection.in_(collections),
|
||
|
|
*self._published_filters(),
|
||
|
|
FinKnowledgeMeta.content_text.like(f"%{escaped_keyword}%", escape="\\"),
|
||
|
|
)
|
||
|
|
.order_by(FinKnowledgeMeta.id.desc())
|
||
|
|
.limit(top_k)
|
||
|
|
)
|
||
|
|
result: list[KnowledgeHit] = []
|
||
|
|
for row in rows:
|
||
|
|
answer = self.extract_answer(row.content_text).strip()
|
||
|
|
if not answer:
|
||
|
|
continue
|
||
|
|
result.append(
|
||
|
|
KnowledgeHit(
|
||
|
|
knowledge_id=str(row.id),
|
||
|
|
collection=row.milvus_collection,
|
||
|
|
title=row.title,
|
||
|
|
snippet=answer[:300],
|
||
|
|
answer=answer,
|
||
|
|
version=row.version,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
return result
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _keyword(query: str) -> str:
|
||
|
|
"""压缩空白并限制关键词长度,避免降级查询承载无界输入。"""
|
||
|
|
return "".join(query.split())[:64]
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _published_filters() -> tuple[ColumnElement[bool], ...]:
|
||
|
|
today = datetime.now(UTC).date()
|
||
|
|
return (
|
||
|
|
FinKnowledgeMeta.review_status == "published",
|
||
|
|
FinKnowledgeMeta.status == "active",
|
||
|
|
(FinKnowledgeMeta.effective_date.is_(None))
|
||
|
|
| (FinKnowledgeMeta.effective_date <= today),
|
||
|
|
(FinKnowledgeMeta.expire_date.is_(None))
|
||
|
|
| (FinKnowledgeMeta.expire_date > today),
|
||
|
|
)
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def extract_answer(content_text: str) -> str:
|
||
|
|
try:
|
||
|
|
payload = json.loads(content_text)
|
||
|
|
except json.JSONDecodeError:
|
||
|
|
return content_text
|
||
|
|
answer = payload.get("answer") if isinstance(payload, dict) else None
|
||
|
|
if isinstance(answer, str):
|
||
|
|
return answer
|
||
|
|
return content_text
|