1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富 统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」; 同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。 2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本), 新增《文档规整方案与开发前待决事项-2026-09-17》。 3) 客服agent 四份交付文档首次纳入本分支。
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
|