chore: 清理违反底座规则的死代码并修正接口文档编号
- 删除生产死代码 app/service/knowledge_tool_service.py 与 app/infrastructure/milvus_knowledge_adapter.py:后者硬编码 Milvus 字段名, 违反 AGENTS.md §E,且仅被前者引用;生产检索链路实际走 knowledge_search_tool -> KnowledgeSearchService -> knowledge_schema 运行时探测。 - 删除上述两模块的单测,以及依赖 legacy 位置参数构造的 tests/unit/service/test_knowledge_retrieval.py。 - app/service/knowledge_retrieval_service.py 整文件回退底座版本, 移除 legacy 双构造与重复检索实现。 - docs/05-接口文档.md:客服画像候选改登记为 §8.5,恢复 §8.2 解析知识引用; 既有 §8.1-§8.4 编号全部保持,修复此前出现两个 8.3 的问题。 - app/model/profile.py:current_customer_id 改为普通可空列映射,与 alembic/baseline_generated.sql 及真实库一致;原 Computed 声明会让 ORM 把该列 从 INSERT 中排除,与「必须显式写入」的实际 schema 不符。 - 新增 docs/客服Agent接入底座扩展说明_v1.md,供集成分支评审逐项确认。 验证:pytest tests/unit tests/contract -> 1275 passed, 2 skipped, 0 failed; ruff check app tests tools alembic 通过;mypy app 通过(244 个源文件)。
This commit is contained in:
@@ -1,74 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from app.core.errors import ForbiddenAgentError, RecoverableAgentError
|
||||
from app.core.knowledge_contracts import ALLOWED_KNOWLEDGE_COLLECTIONS
|
||||
|
||||
|
||||
class MilvusKnowledgeClient:
|
||||
def __init__(self, uri: str, token: str | None = None) -> None:
|
||||
self._uri = uri
|
||||
self._token = token
|
||||
self._client: Any | None = None
|
||||
|
||||
async def _ensure_client(self) -> Any:
|
||||
if self._client is None:
|
||||
from pymilvus import AsyncMilvusClient # type: ignore[import-untyped]
|
||||
|
||||
self._client = AsyncMilvusClient(uri=self._uri, token=self._token)
|
||||
return self._client
|
||||
|
||||
async def search(
|
||||
self, collection: str, vector: list[float], top_k: int
|
||||
) -> list[dict[str, Any]]:
|
||||
if collection not in ALLOWED_KNOWLEDGE_COLLECTIONS:
|
||||
raise ForbiddenAgentError("未授权的知识集合")
|
||||
if len(vector) != 1024 or not 1 <= top_k <= 20:
|
||||
raise RecoverableAgentError("知识检索参数无效")
|
||||
try:
|
||||
client = await self._ensure_client()
|
||||
# Lite 重启后集合默认未加载;远程 Milvus 对重复加载保持幂等。
|
||||
load_collection = getattr(client, "load_collection", None)
|
||||
if load_collection is not None:
|
||||
await load_collection(collection_name=collection)
|
||||
batches = await client.search(
|
||||
collection_name=collection,
|
||||
data=[vector],
|
||||
limit=top_k,
|
||||
output_fields=["knowledge_id", "title", "snippet", "tags", "version"],
|
||||
search_params={"metric_type": "COSINE"},
|
||||
)
|
||||
except Exception as exc:
|
||||
raise RecoverableAgentError("知识检索不可用") from exc
|
||||
return [
|
||||
normalized
|
||||
for batch in batches
|
||||
for hit in batch
|
||||
if (normalized := self._normalize_hit(hit)) is not None
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _normalize_hit(hit: Any) -> dict[str, Any] | None:
|
||||
"""统一 Milvus SDK 的平铺与 entity 包装命中格式。"""
|
||||
raw = dict(hit)
|
||||
entity = raw.get("entity")
|
||||
fields = entity if isinstance(entity, dict) else raw
|
||||
knowledge_id = fields.get("knowledge_id")
|
||||
snippet = fields.get("snippet")
|
||||
score = raw.get("score", raw.get("distance", fields.get("score")))
|
||||
if (
|
||||
not isinstance(knowledge_id, str)
|
||||
or not isinstance(snippet, str)
|
||||
or not isinstance(score, (int, float))
|
||||
or isinstance(score, bool)
|
||||
):
|
||||
return None
|
||||
normalized: dict[str, Any] = {
|
||||
"knowledge_id": knowledge_id,
|
||||
"snippet": snippet,
|
||||
"score": float(score),
|
||||
}
|
||||
for field in ("title", "tags", "version"):
|
||||
value = fields.get(field)
|
||||
if value is not None:
|
||||
normalized[field] = value
|
||||
return normalized
|
||||
Reference in New Issue
Block a user